Air-Gapped Deployment¶
What Is an Air-Gapped Deployment¶
An air-gapped deployment Plan allows you to package your Helm chart as a self-contained installer that your customers can deploy onto their own Kubernetes clusters. The Plan specification defines deployment requirements, API parameters, lifecycle hooks, and Helm chart configuration.
Note
An installer Plan can include multiple Helm chart resources and multiple container image registry copy resources. Model each Helm release as its own services[] entry, and model each source registry or repository copy path as its own internal image sync service.
Note
The installer assumes that the customer running it has kubectl, helm, and a valid kubeconfig for the target cluster.
Configuring the deployment block¶
Set the product name and the air-gapped deployment requirements:
name: My Application
deployment:
requirements:
k8sVersion: ">=1.30.0" # optional
onPremDeployment:
# AWS account that hosts the installer artifacts
AwsAccountId: '<your-aws-account-id>'
AwsBootstrapRoleAccountArn: 'arn:aws:iam::<your-aws-account-id>:role/omnistrate-bootstrap-role'
| Field | Description |
|---|---|
name | Display name for your installer product. |
requirements.k8sVersion | (Optional) Minimum Kubernetes version required on the target cluster. |
onPremDeployment.AwsAccountId | The AWS account ID that hosts the installer artifacts. This is the account where Omnistrate stores and retrieves installer resources. |
onPremDeployment.AwsBootstrapRoleAccountArn | The IAM role ARN in the installer artifact hosting account that Omnistrate assumes during installer packaging operations. |
Helper user script¶
You can use the onPremInstallerTools.helperUserScript field to inject common reusable bash functions into the installer artifact. These functions are then available to all action hooks (validate, pre-install, post-install, backup), avoiding duplication across hooks:
onPremInstallerTools:
helperUserScript: |
#!/bin/bash
log_error() {
echo "Error: $1" > /tmp/error.log
}
Or reference an external file:
Defining services¶
Under the services key, define one or more services. Each service maps to a deployable unit (typically a Helm release).
Configuring API parameters¶
API parameters are the user-facing configuration inputs shown during deployment. Add them under apiParameters for each service. For more details, see Deployment API Params.
apiParameters:
- name: releaseName
key: releaseName
type: String
required: false
modifiable: false
export: true
description: "Helm release name"
defaultValue: "my-app"
- name: namespace
key: namespace
type: String
required: false
modifiable: false
export: true
defaultValue: "my-app"
description: "Kubernetes namespace to deploy into"
- name: adminPassword
key: adminPassword
type: Password
required: true
modifiable: false
export: true
description: "Admin password for the application"
Parameter field reference¶
| Field | Description |
|---|---|
name | Human-readable display name. |
key | Internal reference key (used in {{ $var.<key> }} templates). |
type | Data type: String, Boolean, Password, etc. |
required | Whether the user must provide a value. |
modifiable | Whether the value can be changed after initial deployment. |
export | Whether the value is passed to Helm and hooks. |
defaultValue | Pre-filled default (user can override). |
description | Tooltip or help text shown in the UI. |
options | (Optional) Restrict input to an enumerated list of values. |
Tip
Use type: Password for secrets so the value is masked in the UI.
Configuring action hooks¶
Action hooks run shell commands at specific points in the deployment lifecycle. Add them under actionHooks. For more details, see Action Hooks.
actionHooks:
- scope: CLUSTER
type: VALIDATE
commandTemplate: "echo 'Running validation checks...'"
- scope: CLUSTER
type: PRE_INSTALL
commandTemplate: "echo 'Preparing environment...'"
- scope: CLUSTER
type: POST_INSTALL
commandTemplate: "echo 'Post-install configuration...'"
- scope: CLUSTER
type: BACKUP
commandTemplate: "echo 'Creating backup...'"
| Hook Type | When it runs |
|---|---|
VALIDATE | Before and after installation to verify the cluster meets requirements. |
PRE_INSTALL | After validation but before Helm install/upgrade. |
POST_INSTALL | After Helm install/upgrade completes successfully. |
BACKUP | Before upgrades to snapshot the current state. |
For longer scripts, reference external files:
Configuring the Helm chart¶
The helmChartConfiguration block tells Omnistrate which Helm chart to deploy and how to pass values:
helmChartConfiguration:
chartName: my-app
chartVersion: 1.0.0
chartRepoName: my-repo
chartRepoURL: https://charts.example.com/
releaseName: "{{ $var.releaseName }}"
namespace: "{{ $var.namespace }}"
| Field | Description |
|---|---|
chartName | Name of the Helm chart. |
chartVersion | Chart version to install. |
chartRepoName | Logical name for the Helm repository. |
chartRepoURL | URL of the Helm chart repository (HTTPS or OCI). |
releaseName | Helm release name (use {{ $var.releaseName }} to reference a parameter). |
namespace | Kubernetes namespace (use {{ $var.namespace }} to reference a parameter). |
Passing values with chartValues¶
Map API parameters into your Helm chart's values.yaml structure using the {{ $var.<key> }} template syntax:
chartValues:
config:
adminPassword: "{{ $var.adminPassword }}"
domain: "{{ $var.dnsEndpoint }}"
ingress:
enabled: true
hosts:
- host: "{{ $var.dnsEndpoint }}"
paths:
- path: /
pathType: Prefix
Platform-specific values with layeredChartValues¶
Use layeredChartValues to conditionally apply different Helm values based on the target platform or other parameter values. For more details, see Layered Chart Values.
layeredChartValues:
- scope:
"{{ $var.onprem_platform }}": "EKS"
values:
{{ $file:./chart_value_templates/aws/values.yaml }}
- scope:
"{{ $var.onprem_platform }}": "AKS"
values:
{{ $file:./chart_value_templates/azure/values.yaml }}
- scope:
"{{ $var.onprem_platform }}": "GKE"
values:
{{ $file:./chart_value_templates/gcp/values.yaml }}
- scope:
"{{ $var.onprem_platform }}": "GENERIC"
values:
{{ $file:./chart_value_templates/generic/values.yaml }}
The scope block acts as a conditional — the values are only applied when all conditions match. {{ $var.onprem_platform }} is a built-in system variable set automatically based on the customer's cluster type.
Configuring container image registry copy¶
If your Helm chart references container images stored in a private registry (for example, Docker Hub with authentication, a private ECR, or any registry requiring credentials), the installer needs to know how to pull those images at build time so they can be copied into the customer's target environment. Without this configuration, the customer's cluster may fail to pull images if it has no direct access to your source registry.
The containerImagesRegistryCopyConfiguration block defines where to pull images from (source registry + credentials) and where to push them to (the customer's private registry). This is essential for:
- Air-gapped or restricted environments — the target cluster has no internet access
- Private source registries — your images require authentication to pull
- Registry locality — customers want images in their own registry for performance, compliance, or security reasons
How it works¶
The installer uses a two-service pattern: a dedicated image sync service handles the registry copy, and your main application service depends on it. This ensures all images are available in the customer's registry before the Helm chart is installed.
┌─────────────────────┐ ┌─────────────────────┐
│ ImageSync Service │──────▶│ MyApp Service │
│ (internal: true) │ │ (dependsOn: │
│ │ │ - ImageSync) │
│ Copies images from │ │ │
│ source → target │ │ Installs Helm chart│
│ registry │ │ (images now local) │
└─────────────────────┘ └─────────────────────┘
Image discovery with autoDiscoverImagesTag¶
The installer needs to know which images to copy. Rather than listing every image manually, you can optionally add autoDiscoverImagesTag to your helmChartConfiguration. This tells the installer to look for the matching annotation in the Helm chart's root Chart.yaml metadata and automatically discover all container images referenced by the chart.
In the installer spec, set the tag value:
helmChartConfiguration:
chartName: my-app
chartVersion: 1.0.0
chartRepoName: my-repo
chartRepoURL: oci://registry-1.docker.io/my-org
# ...other fields...
autoDiscoverImagesTag: "my-org.com/images"
In your Helm chart's Chart.yaml, add the corresponding annotation. Each entry includes a name and an image reference. You can use Helm's {{.Chart.AppVersion}} template to keep image tags in sync with the chart version:
apiVersion: v2
name: my-app
version: 1.0.0
appVersion: 2.5.0
annotations:
my-org.com/images: |
- name: app-server
image: docker.io/my-org/app-server:{{.Chart.AppVersion}}
- name: app-worker
image: docker.io/my-org/app-worker:{{.Chart.AppVersion}}
- name: mongodb
image: docker.io/my-org/mirror-mongodb:7.0-build-335
- name: postgresql-repmgr
image: docker.io/my-org/mirror-postgresql-repmgr:14.15.0-debian-12-r2
- name: redis
image: docker.io/my-org/mirror-redis-server:7.4-build-248
- name: rabbitmq
image: docker.io/my-org/mirror-rabbitmq:4.0.5-build-175
At build time, the installer reads the Chart.yaml annotations, matches the autoDiscoverImagesTag key, and extracts the listed container images for the registry copy operation. This keeps the image list in sync with your chart — update Chart.yaml when you change images, and the installer picks up the changes automatically.
Warning
Using autoDiscoverImagesTag requires a properly configured image registry copy resource (containerImagesRegistryCopyConfiguration) in your spec. The auto-discovery feature reads image references from the chart metadata and feeds them into the registry copy pipeline — without a configured image sync service, there is nowhere to copy the discovered images. See Defining the image sync service for setup details.
Tip
The autoDiscoverImagesTag field is optional. If you omit it, you can manually specify the images to copy using the images list in containerImagesRegistryCopyConfiguration instead (see the end-to-end example).
Note
If your Helm chart is in a private OCI registry, you also need authProvider to authenticate when pulling the chart itself:
Pull modes¶
The pullMode field controls when and how images are transferred:
| Mode | Behavior | Best for |
|---|---|---|
INSTALLER_EMBED | Images are downloaded from the source registry at build time and packaged into the installer artifact. At install time, images are pushed from the local artifact to the target registry. No internet access required during installation. | Air-gapped environments, large-scale deployments where you want deterministic installs. Requires more disk space in the installer artifact. |
RUNTIME_PULL | Images are pulled from the source registry and pushed to the target registry at install/upgrade time. The installer environment must have network access to both registries. | Connected environments where disk space is a concern, or when you always want the latest image digests. |
Defining the image sync service¶
Create a separate service with internal: true (hidden from end users) that owns the containerImagesRegistryCopyConfiguration:
services:
- name: ImageSync
internal: true
apiParameters:
- name: Private Image Registry URL
key: privateRegistryUrl
description: "Customer's private registry where images will be pushed"
type: String
required: true
export: true
modifiable: true
- name: Pull Mode
key: pullMode
description: "Image sync strategy"
type: String
required: true
export: true
modifiable: true
defaultValue: INSTALLER_EMBED
options:
- INSTALLER_EMBED
- RUNTIME_PULL
containerImagesRegistryCopyConfiguration:
pullMode: "{{ $var.pullMode }}"
pullSource:
registryURL: "docker.io"
repositoryName: "my-org"
credentials:
username: "{{ $secret.REGISTRY_USERNAME }}"
password: "{{ $secret.REGISTRY_PASSWORD }}"
pushTarget:
registryURL: "{{ $var.privateRegistryUrl }}"
repositoryName: "my-org"
Configuration reference¶
| Field | Description |
|---|---|
pullMode | INSTALLER_EMBED or RUNTIME_PULL (see Pull modes above). |
pullSource.registryURL | Source registry to pull images from (e.g., docker.io, ghcr.io). |
pullSource.repositoryName | Repository or organization name in the source registry (e.g., my-org). |
pullSource.credentials | (Optional) Credentials for authenticating to the source registry. Use {{ $secret.<name> }} to reference Omnistrate-managed secrets. Required if the source registry is private. |
pushTarget.registryURL | Customer's target private registry. Typically references an API parameter (e.g., {{ $var.privateRegistryUrl }}). |
pushTarget.repositoryName | Repository or organization name in the target registry. Usually mirrors the source. |
Wiring the main service with dependsOn¶
Your main application service must declare dependsOn to ensure image sync completes first. Additionally, if the main service needs to reference the same parameters (e.g., privateRegistryUrl, pullMode), use parameterDependencyMap to link them across services rather than duplicating values. For more details on dependencies, see Resource Dependencies.
- name: MyApp
dependsOn:
- ImageSync
apiParameters:
- name: Private Image Registry URL
key: privateRegistryUrl
description: "Private registry URL"
type: String
required: true
export: true
modifiable: true
parameterDependencyMap:
ImageSync: privateRegistryUrl # passes this value to ImageSync's privateRegistryUrl
- name: Pull Mode
key: pullMode
type: String
required: true
export: true
defaultValue: INSTALLER_EMBED
options:
- INSTALLER_EMBED
- RUNTIME_PULL
parameterDependencyMap:
ImageSync: pullMode # passes this value to ImageSync's pullMode
helmChartConfiguration:
chartName: my-app
# ...
autoDiscoverImagesTag: "my-org.com/images"
With parameterDependencyMap, the user only fills in the value once (on the main service), and it is automatically forwarded to the internal ImageSync service. The format is:
Disabling image sync¶
You can make image sync optional by adding a boolean parameter and using the disable field on the image sync service:
- name: ImageSync
internal: true
disable: "{{ $var.skipImageSync }}"
apiParameters:
- name: Skip Image Sync
key: skipImageSync
type: Boolean
required: false
export: true
defaultValue: 'false'
modifiable: false
# ...other parameters...
containerImagesRegistryCopyConfiguration:
# ...
When skipImageSync is set to true, the entire image copy step is skipped. Use parameterDependencyMap on the main service to let the user control this toggle:
# In the main service's apiParameters:
- name: Skip Image Sync
key: skipImageSync
type: Boolean
required: false
export: true
defaultValue: 'false'
modifiable: false
parameterDependencyMap:
ImageSync: skipImageSync
End-to-end example¶
Here is a complete two-service setup with image registry copy:
services:
# 1. Internal image sync service
- name: DockerIO
internal: true
apiParameters:
- name: Skip Custom Image Registry
key: skipCustomImageRegistry
type: Boolean
required: false
export: true
defaultValue: 'true'
modifiable: false
- name: Private Image Registry URL
key: privateRegistryUrl
type: String
required: true
export: true
modifiable: true
- name: Pull Mode
key: pullMode
type: String
required: true
export: true
modifiable: true
defaultValue: INSTALLER_EMBED
options:
- INSTALLER_EMBED
- RUNTIME_PULL
disable: "{{ $var.skipCustomImageRegistry }}"
containerImagesRegistryCopyConfiguration:
pullMode: "{{ $var.pullMode }}"
pullSource:
registryURL: "docker.io"
repositoryName: "my-org"
credentials:
username: "{{ $secret.DOCKERHUB_USERNAME }}"
password: "{{ $secret.DOCKERHUB_PASSWORD }}"
pushTarget:
registryURL: "{{ $var.privateRegistryUrl }}"
repositoryName: "my-org"
images:
- imageName: "my-image"
imageTag: "my-tag"
# 2. Main application service
- name: MyApp
dependsOn:
- DockerIO
apiParameters:
- name: Skip Custom Image Registry
key: skipCustomImageRegistry
type: Boolean
required: false
export: true
defaultValue: 'false'
modifiable: false
parameterDependencyMap:
DockerIO: skipCustomImageRegistry
- name: Private Image Registry URL
key: privateRegistryUrl
type: String
required: true
export: true
modifiable: true
parameterDependencyMap:
DockerIO: privateRegistryUrl
- name: Pull Mode
key: pullMode
type: String
required: true
export: true
defaultValue: INSTALLER_EMBED
options:
- INSTALLER_EMBED
- RUNTIME_PULL
parameterDependencyMap:
DockerIO: pullMode
# ...other app parameters...
helmChartConfiguration:
chartName: my-app
chartVersion: 1.0.0
chartRepoName: my-org
chartRepoURL: oci://registry-1.docker.io/my-org
releaseName: "{{ $var.releaseName }}"
namespace: "{{ $var.namespace }}"
autoDiscoverImagesTag: "my-org.com/images"
authProvider:
username: '{{ $secret.DOCKERHUB_USERNAME }}'
password: '{{ $secret.DOCKERHUB_PASSWORD }}'
# ...chartValues, layeredChartValues, etc.
Advanced: Multiple Helm releases and registries¶
Complex installers often need to install prerequisite Helm charts before the main application. For example, you may want the installer to sync images, install ingress-nginx, install cert-manager, and then install your application chart. Model this as a graph of services:
- Use one internal image sync service for each source registry or repository copy path.
- Use one Helm service for each Helm release.
- Use
dependsOnto order image sync before the Helm release that consumes those images. - Use
parameterDependencyMapto pass shared inputs, such asprivateRegistryUrl,pullMode, and skip flags, from the main application service to internal services. - Use
disablewhen an API parameter can decide that a resource should not be included in the installer plan. - Use a validation hook with runtime skip when the decision depends on the customer's cluster state, such as detecting that a prerequisite Helm release already exists.
Example service graph¶
DockerHubImages -----> MainApp
RegistryK8sImages --> IngressNginx --> MainApp
QuayJetstackImages --> CertManager --> MainApp
The image sync services are hidden from the user with internal: true. The prerequisite Helm services can also be internal when they are implementation details of the installer.
The examples below use one prerequisiteAlreadyInstalled flag to keep the snippets compact. In a production spec, use separate flags when prerequisites can be present independently.
Shared parameters¶
YAML anchors are optional, but they keep multi-service installer specs readable:
x-api-skip-private-registry: &api-skip-private-registry
name: Skip Private Image Registry
key: skipPrivateRegistry
description: Skip copying images into a private registry
type: Boolean
required: false
export: true
defaultValue: 'false'
modifiable: false
x-api-private-registry-url: &api-private-registry-url
name: Private Image Registry URL
key: privateRegistryUrl
description: Private registry where installer images are pushed
type: String
required: true
export: true
modifiable: true
x-api-pull-mode: &api-pull-mode
name: Pull Mode
key: pullMode
description: Image sync strategy
type: String
required: true
export: true
modifiable: true
defaultValue: INSTALLER_EMBED
options:
- INSTALLER_EMBED
- RUNTIME_PULL
x-api-prereq-exists: &api-prereq-exists
name: Prerequisite Already Installed
key: prerequisiteAlreadyInstalled
description: Skip prerequisite image sync and Helm install when the prerequisite already exists
type: Boolean
required: false
export: true
defaultValue: 'false'
modifiable: true
Multiple image sync services¶
Create one registry copy service per source registry or repository layout. This lets each registry keep its own pullSource, credentials, image list, and target repository.
services:
- name: DockerHubImages
internal: true
apiParameters:
- <<: *api-skip-private-registry
- <<: *api-private-registry-url
- <<: *api-pull-mode
disable: "{{ $var.skipPrivateRegistry }}"
containerImagesRegistryCopyConfiguration:
pullMode: "{{ $var.pullMode }}"
pullSource:
registryURL: "docker.io"
repositoryName: "my-org"
credentials:
username: "{{ $secret.DOCKERHUB_USERNAME }}"
password: "{{ $secret.DOCKERHUB_PASSWORD }}"
pushTarget:
registryURL: "{{ $var.privateRegistryUrl }}"
repositoryName: "my-org"
- name: RegistryK8sImages
internal: true
apiParameters:
- <<: *api-prereq-exists
- <<: *api-skip-private-registry
- <<: *api-private-registry-url
- <<: *api-pull-mode
disable: '{{ $func.if($func.equals($var.skipPrivateRegistry, "true"), "true", $var.prerequisiteAlreadyInstalled) }}'
containerImagesRegistryCopyConfiguration:
pullMode: "{{ $var.pullMode }}"
pullSource:
registryURL: "registry.k8s.io"
repositoryName: "ingress-nginx"
pushTarget:
registryURL: "{{ $var.privateRegistryUrl }}"
repositoryName: "ingress-nginx"
images:
- imageName: "controller"
imageTag: "v1.15.1"
- imageName: "kube-webhook-certgen"
imageTag: "v1.6.9"
- name: QuayJetstackImages
internal: true
apiParameters:
- <<: *api-prereq-exists
- <<: *api-skip-private-registry
- <<: *api-private-registry-url
- <<: *api-pull-mode
disable: '{{ $func.if($func.equals($var.skipPrivateRegistry, "true"), "true", $var.prerequisiteAlreadyInstalled) }}'
containerImagesRegistryCopyConfiguration:
pullMode: "{{ $var.pullMode }}"
pullSource:
registryURL: "quay.io"
repositoryName: "jetstack"
pushTarget:
registryURL: "{{ $var.privateRegistryUrl }}"
repositoryName: "jetstack"
images:
- imageName: "cert-manager-controller"
imageTag: "v1.21.0"
- imageName: "cert-manager-webhook"
imageTag: "v1.21.0"
- imageName: "cert-manager-cainjector"
imageTag: "v1.21.0"
- imageName: "cert-manager-startupapicheck"
imageTag: "v1.21.0"
Multiple Helm services¶
Each Helm release gets its own service and its own helmChartConfiguration. Use dependsOn to make each chart wait for the image sync service it needs.
- name: IngressNginx
internal: true
dependsOn:
- RegistryK8sImages
apiParameters:
- <<: *api-prereq-exists
parameterDependencyMap:
RegistryK8sImages: prerequisiteAlreadyInstalled
- <<: *api-skip-private-registry
parameterDependencyMap:
RegistryK8sImages: skipPrivateRegistry
- <<: *api-private-registry-url
parameterDependencyMap:
RegistryK8sImages: privateRegistryUrl
- <<: *api-pull-mode
parameterDependencyMap:
RegistryK8sImages: pullMode
disable: "{{ $var.prerequisiteAlreadyInstalled }}"
actionHooks:
- scope: CLUSTER
type: VALIDATE
commandTemplate: |
export INTERNAL_HELM_PHASE=validate
{{ $file:./custom_scripts/ingress_nginx_runtime_skip.sh }}
- scope: CLUSTER
type: POST_INSTALL
commandTemplate: |
export INTERNAL_HELM_PHASE=post-install
{{ $file:./custom_scripts/ingress_nginx_runtime_skip.sh }}
helmChartConfiguration:
chartName: ingress-nginx
chartVersion: 4.15.1
chartRepoName: ingress-nginx
chartRepoURL: https://kubernetes.github.io/ingress-nginx
releaseName: ingress-nginx
namespace: ingress-nginx
runtimeConfiguration:
wait: true
waitForJobs: true
layeredChartValues:
- scope:
"{{ $var.skipPrivateRegistry }}": 'true'
values:
global:
image:
registry: registry.k8s.io
- scope:
"{{ $var.skipPrivateRegistry }}": 'false'
values:
global:
image:
registry: "{{ $var.privateRegistryUrl }}"
- name: CertManager
internal: true
dependsOn:
- QuayJetstackImages
apiParameters:
- <<: *api-prereq-exists
parameterDependencyMap:
QuayJetstackImages: prerequisiteAlreadyInstalled
- <<: *api-skip-private-registry
parameterDependencyMap:
QuayJetstackImages: skipPrivateRegistry
- <<: *api-private-registry-url
parameterDependencyMap:
QuayJetstackImages: privateRegistryUrl
- <<: *api-pull-mode
parameterDependencyMap:
QuayJetstackImages: pullMode
disable: "{{ $var.prerequisiteAlreadyInstalled }}"
actionHooks:
- scope: CLUSTER
type: VALIDATE
commandTemplate: |
export INTERNAL_HELM_PHASE=validate
{{ $file:./custom_scripts/cert_manager_runtime_skip.sh }}
- scope: CLUSTER
type: POST_INSTALL
commandTemplate: |
export INTERNAL_HELM_PHASE=post-install
{{ $file:./custom_scripts/cert_manager_runtime_skip.sh }}
helmChartConfiguration:
chartName: cert-manager
chartVersion: v1.21.0
chartRepoName: jetstack
chartRepoURL: https://charts.jetstack.io
releaseName: cert-manager
namespace: cert-manager
runtimeConfiguration:
wait: true
waitForJobs: true
layeredChartValues:
- scope:
"{{ $var.skipPrivateRegistry }}": 'true'
values:
crds:
enabled: true
imageRegistry: quay.io
imageNamespace: jetstack
- scope:
"{{ $var.skipPrivateRegistry }}": 'false'
values:
crds:
enabled: true
imageRegistry: "{{ $var.privateRegistryUrl }}"
imageNamespace: jetstack
- name: MainApp
dependsOn:
- DockerHubImages
- IngressNginx
- CertManager
apiParameters:
- <<: *api-prereq-exists
parameterDependencyMap:
RegistryK8sImages: prerequisiteAlreadyInstalled
QuayJetstackImages: prerequisiteAlreadyInstalled
IngressNginx: prerequisiteAlreadyInstalled
CertManager: prerequisiteAlreadyInstalled
- <<: *api-skip-private-registry
parameterDependencyMap:
DockerHubImages: skipPrivateRegistry
RegistryK8sImages: skipPrivateRegistry
QuayJetstackImages: skipPrivateRegistry
IngressNginx: skipPrivateRegistry
CertManager: skipPrivateRegistry
- <<: *api-private-registry-url
parameterDependencyMap:
DockerHubImages: privateRegistryUrl
RegistryK8sImages: privateRegistryUrl
QuayJetstackImages: privateRegistryUrl
IngressNginx: privateRegistryUrl
CertManager: privateRegistryUrl
- <<: *api-pull-mode
parameterDependencyMap:
DockerHubImages: pullMode
RegistryK8sImages: pullMode
QuayJetstackImages: pullMode
IngressNginx: pullMode
CertManager: pullMode
helmChartConfiguration:
chartName: main-app
chartVersion: 1.0.0
chartRepoName: my-org
chartRepoURL: oci://registry-1.docker.io/my-org
releaseName: main-app
namespace: main-app
autoDiscoverImagesTag: "my-org.com/images"
layeredChartValues:
- scope:
"{{ $var.skipPrivateRegistry }}": 'true'
values:
global:
imageRegistry: docker.io
- scope:
"{{ $var.skipPrivateRegistry }}": 'false'
values:
global:
imageRegistry: "{{ $var.privateRegistryUrl }}"
Runtime skip for existing prerequisites¶
Use disable when a user-provided parameter decides whether a resource should run. Use runtime skip when the installer must inspect the target cluster during validation. This is common for prerequisite charts because a customer may already have an ingress controller, cert-manager, CSI driver, or other platform component installed.
When a validation hook calls skip_resource_deployment, the installer records the skip reason for the current resource and skips its remaining deployment steps during the same run. A retry also uses the persisted skip state.
#!/usr/bin/env bash
set -uo pipefail
skip_current_resource() {
local reason="$1"
echo "Skipping resource: ${reason}"
if declare -F skip_resource_deployment >/dev/null 2>&1; then
skip_resource_deployment "$reason"
else
echo "SKIP: ${reason}"
fi
exit 0
}
helm_release_match() {
local pattern="$1"
helm list -A --all -o json 2>/dev/null \
| jq -r --arg p "$pattern" '
[.[] | select((.chart // "") | test($p))]
| ((map(select(.status == "deployed")) | first) // first // empty)
| if type == "object" then "\(.name)|\(.namespace)|\(.chart)|\(.status)" else empty end
'
}
if [[ "${INTERNAL_HELM_PHASE:-validate}" == "validate" ]]; then
match="$(helm_release_match "cert-manager")"
if [[ -n "$match" ]]; then
skip_current_resource "cert-manager already exists (${match})"
fi
fi
If the hook detects an unhealthy existing installation, exit non-zero instead of skipping. Runtime skip should only be used when the existing resource is acceptable for the main application.
Building and releasing the installer¶
Use the Omnistrate CLI to build and release the installer:
omnistrate-ctl build \
--spec-type ServicePlanSpec \
--file installer-spec.yaml \
--product-name "My Application Air-Gapped Installer" \
--release \
--release-description "v1.0.0 initial release"
Full Example¶
The following example shows a complete minimal spec that packages Supabase as an air-gapped installer. It includes a deployment block, API parameters for the release name, namespace, JWT secret, database password, and DNS endpoint, along with lifecycle action hooks and a Helm chart configuration.
The video walkthrough below demonstrates the end-to-end experience of building, releasing, and installing using this exact spec — Watch on YouTube.
Complete Supabase specification
# yaml-language-server: $schema=https://api.omnistrate.cloud/2022-09-01-00/schema/service-spec-schema.json
# yaml-language-server: $schema=https://api.omnistrate.cloud/2022-09-01-00/schema/system-parameters-schema.json
name: Supabase
deployment:
requirements:
k8sVersion: ">=1.30.0" # optional
onPremDeployment:
# AWS account that hosts the installer artifacts
AwsAccountId: '<your-aws-account-id>'
AwsBootstrapRoleAccountArn: 'arn:aws:iam::<your-aws-account-id>:role/omnistrate-bootstrap-role'
onPremInstallerTools:
helperUserScript: |
#!/bin/bash
log_error() {
echo "Error: $1" > /tmp/error.log
}
services:
- name: Supabase
apiParameters:
- name: releaseName
key: releaseName
type: String
required: false
modifiable: false
export: true
description: "Helm release name"
defaultValue: "supabase"
- name: namespace
key: namespace
type: String
required: false
modifiable: false
export: true
defaultValue: "supabase"
description: "Kubernetes namespace to deploy Supabase into"
- name: jwtSecret
key: jwtSecret
type: Password
required: false
modifiable: false
export: true
description: "JWT Secret"
- name: dbPassword
key: dbPassword
description: "Database Password"
type: Password
modifiable: false
export: true
required: false
- name: dnsEndpoint
key: dnsEndpoint
type: String
required: false
modifiable: false
export: true
description: "DNS Endpoint for Supabase Ingress"
defaultValue: "example.com"
actionHooks:
- scope: CLUSTER
type: VALIDATE
commandTemplate: "echo 'Validate hook'"
- scope: CLUSTER
type: PRE_INSTALL
commandTemplate: "echo 'Pre-install hook'"
- scope: CLUSTER
type: POST_INSTALL
commandTemplate: "echo 'Post-install hook'"
- scope: CLUSTER
type: BACKUP
commandTemplate: "echo 'Backup hook'"
helmChartConfiguration:
chartName: supabase
chartVersion: 0.1.3
chartRepoName: supabase
chartRepoURL: https://example.github.io/supabase-charts/
releaseName: "{{ $var.releaseName }}"
namespace: "{{ $var.namespace }}"
chartValues:
secret:
jwt:
secret: "{{ $var.jwtSecret }}"
db:
password: "{{ $var.dbPassword }}"
kong:
ingress:
enabled: true
className: "nginx"
hosts:
- host: "{{ $var.dnsEndpoint }}"
paths:
- path: /
pathType: Prefix
Template syntax reference¶
| Syntax | Description |
|---|---|
{{ $var.<key> }} | References an API parameter by its key. |
{{ $file:<path> }} | Inlines the contents of a file at build time. |
{{ $secret.<name> }} | References an Omnistrate-managed secret. |
{{ $var.onprem_platform }} | Built-in variable: EKS, AKS, GKE, or GENERIC. |