Repository: replicatedhq/kots
Pull request #755 (2020-07-08 00:35:38)

airgapped kots install --kotsadm-registry


Patch:
diff --git a/.github/workflows/kotsadm.yaml b/.github/workflows/kotsadm.yaml
index 1b5eee8f1..41f3a856a 100644
--- a/.github/workflows/kotsadm.yaml
+++ b/.github/workflows/kotsadm.yaml
@@ -89,6 +89,8 @@ jobs:
       uses: actions/checkout@v2
 
     - uses: azure/docker-login@v1
+      env:
+        DOCKER_CONFIG: ./kotsadm/migrations/.docker
       with:
         username: ${{ secrets.DOCKERHUB_USER }}
         password: ${{ secrets.DOCKERHUB_PASSWORD }}
@@ -108,8 +110,17 @@ jobs:
       if: ${{ steps.get_tag.outputs.GIT_TAG != '' }}
       env:
         GIT_TAG: ${{ steps.get_tag.outputs.GIT_TAG }}
-      run: |
-        make -C kotsadm/migrations schema-release
+        DOCKER_CONFIG: ./.docker
+      uses: ./actions/containers
+      with:
+        command: "make -C kotsadm/migrations schema-release"
+
+    - name: Upload airgap image
+      if: ${{ steps.get_tag.outputs.GIT_TAG != '' }}
+      uses: actions/upload-artifact@v2
+      with:
+        name: migrations-image
+        path: ./kotsadm/migrations/bin/docker-archive
 
 
   test_web:
@@ -682,7 +693,14 @@ jobs:
       id: get_tag
       shell: bash
       run: echo ::set-output name=GIT_TAG::${GITHUB_REF/refs\/tags\//}
-  
+
+    - name: Download migrations
+      if: ${{ steps.get_tag.outputs.GIT_TAG != '' }}
+      uses: actions/download-artifact@v2
+      with:
+        name: migrations-image
+        path: ./docker-archive
+
     - name: Download kotsadm image
       if: ${{ steps.get_tag.outputs.GIT_TAG != '' }}
       uses: actions/download-artifact@v2
diff --git a/cmd/kots/cli/admin-console-upgrade.go b/cmd/kots/cli/admin-console-upgrade.go
index 88b687b95..13d2cd341 100644
--- a/cmd/kots/cli/admin-console-upgrade.go
+++ b/cmd/kots/cli/admin-console-upgrade.go
@@ -52,6 +52,14 @@ func AdminConsoleUpgradeCmd() *cobra.Command {
 				Namespace:             v.GetString("namespace"),
 				KubernetesConfigFlags: kubernetesConfigFlags,
 				ForceUpgradeKurl:      v.GetBool("force-upgrade-kurl"),
+
+				KotsadmOptions: kotsadmtypes.KotsadmOptions{
+					OverrideVersion:   v.GetString("kotsadm-tag"),
+					OverrideRegistry:  v.GetString("kotsadm-registry"),
+					OverrideNamespace: v.GetString("kotsadm-namespace"),
+					Username:          v.GetString("registry-username"),
+					Password:          v.GetString("registry-password"),
+				},
 			}
 
 			timeout, err := time.ParseDuration(v.GetString("wait-duration"))
@@ -61,10 +69,6 @@ func AdminConsoleUpgradeCmd() *cobra.Command {
 
 			upgradeOptions.Timeout = timeout
 
-			kotsadm.OverrideVersion = v.GetString("kotsadm-tag")
-			kotsadm.OverrideRegistry = v.GetString("kotsadm-registry")
-			kotsadm.OverrideNamespace = v.GetString("kotsadm-namespace")
-
 			log := logger.NewLogger()
 
 			if upgradeOptions.Namespace != "default" {
diff --git a/cmd/kots/cli/install.go b/cmd/kots/cli/install.go
index 28093bc90..6b25b9246 100644
--- a/cmd/kots/cli/install.go
+++ b/cmd/kots/cli/install.go
@@ -74,10 +74,6 @@ func InstallCmd() *cobra.Command {
 				namespace = enteredNamespace
 			}
 
-			kotsadm.OverrideVersion = v.GetString("kotsadm-tag")
-			kotsadm.OverrideRegistry = v.GetString("kotsadm-registry")
-			kotsadm.OverrideNamespace = v.GetString("kotsadm-namespace")
-
 			applicationMetadata, err := pull.PullApplicationMetadata(upstream)
 			if err != nil {
 				return errors.Wrap(err, "failed to pull app metadata")
@@ -128,6 +124,14 @@ func InstallCmd() *cobra.Command {
 				IncludeMinio:              v.GetBool("deploy-minio"),
 				IncludeDockerDistribution: v.GetBool("deploy-dockerdistribution"),
 				Timeout:                   time.Minute * 2,
+
+				KotsadmOptions: kotsadmtypes.KotsadmOptions{
+					OverrideVersion:   v.GetString("kotsadm-tag"),
+					OverrideRegistry:  v.GetString("kotsadm-registry"),
+					OverrideNamespace: v.GetString("kotsadm-namespace"),
+					Username:          v.GetString("registry-username"),
+					Password:          v.GetString("registry-password"),
+				},
 			}
 
 			timeout, err := time.ParseDuration(v.GetString("wait-duration"))
@@ -222,12 +226,14 @@ func InstallCmd() *cobra.Command {
 	cmd.Flags().String("repo", "", "repo uri to use when installing a helm chart")
 	cmd.Flags().StringSlice("set", []string{}, "values to pass to helm when running helm template")
 
+	cmd.Flags().String("kotsadm-registry", "", "set to override the registry of kotsadm images")
+	cmd.Flags().String("registry-username", "", "user name to use to authenticate with the registry")
+	cmd.Flags().String("registry-password", "", "password to use to authenticate with the registry")
+
 	// the following group of flags are useful for testing, but we don't want to pollute the help screen with them
 	cmd.Flags().String("kotsadm-tag", "", "set to override the tag of kotsadm. this may create an incompatible deployment because the version of kots and kotsadm are designed to work together")
-	cmd.Flags().String("kotsadm-registry", "", "set to override the registry of kotsadm image. this may create an incompatible deployment because the version of kots and kotsadm are designed to work together")
 	cmd.Flags().String("kotsadm-namespace", "", "set to override the namespace of kotsadm image. this may create an incompatible deployment because the version of kots and kotsadm are designed to work together")
 	cmd.Flags().MarkHidden("kotsadm-tag")
-	cmd.Flags().MarkHidden("kotsadm-registry")
 	cmd.Flags().MarkHidden("kotsadm-namespace")
 
 	// the following group of flags are experiemental and can be used to pull and push images during install time
diff --git a/kotsadm/migrations/Makefile b/kotsadm/migrations/Makefile
index ce9eaf22b..751af9f94 100644
--- a/kotsadm/migrations/Makefile
+++ b/kotsadm/migrations/Makefile
@@ -8,6 +8,10 @@ schema-alpha: build_schema
 .PHONY: schema-release
 schema-release: IMAGE = kotsadm/${PROJECT_NAME}:${GIT_TAG}
 schema-release: build_schema
+	mkdir -p bin/docker-archive/${PROJECT_NAME}
+	skopeo copy docker-daemon:kotsadm/${PROJECT_NAME}:${GIT_TAG} docker-archive:bin/docker-archive/${PROJECT_NAME}/${GIT_TAG}
+	mkdir -p bin/docker-archive/postgres
+	skopeo copy docker://postgres:10.7 docker-archive:bin/docker-archive/postgres/${GIT_TAG}
 
 .PHONY: build-ttl-ci.sh
 build-ttl-ci.sh: IMAGE = ttl.sh/${UUID}/${PROJECT_NAME}:2h
diff --git a/kotsadm/pkg/handlers/snapshots.go b/kotsadm/pkg/handlers/snapshots.go
index 465f9b9c8..48f233a30 100644
--- a/kotsadm/pkg/handlers/snapshots.go
+++ b/kotsadm/pkg/handlers/snapshots.go
@@ -1,10 +1,8 @@
 package handlers
 
 import (
-	"context"
 	"encoding/json"
 	"net/http"
-	"os"
 	"strings"
 
 	"github.com/pkg/errors"
@@ -13,9 +11,6 @@ import (
 	"github.com/replicatedhq/kots/kotsadm/pkg/session"
 	"github.com/replicatedhq/kots/kotsadm/pkg/snapshot"
 	snapshottypes "github.com/replicatedhq/kots/kotsadm/pkg/snapshot/types"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/client-go/kubernetes"
-	"sigs.k8s.io/controller-runtime/pkg/client/config"
 )
 
 type GlobalSnapshotSettingsResponse struct {
@@ -36,11 +31,10 @@ type UpdateGlobalSnapshotSettingsRequest struct {
 	Bucket   string `json:"bucket"`
 	Path     string `json:"path"`
 
-	AWS      *snapshottypes.StoreAWS    `json:"aws"`
-	Google   *snapshottypes.StoreGoogle `json:"gcp"`
-	Azure    *snapshottypes.StoreAzure  `json:"azure"`
-	Other    *snapshottypes.StoreOther  `json:"other"`
-	Internal bool                       `json:"internal"`
+	AWS    *snapshottypes.StoreAWS    `json:"aws"`
+	Google *snapshottypes.StoreGoogle `json:"gcp"`
+	Azure  *snapshottypes.StoreAzure  `json:"azure"`
+	Other  *snapshottypes.StoreOther  `json:"other"`
 }
 
 func UpdateGlobalSnapshotSettings(w http.ResponseWriter, r *http.Request) {
@@ -117,7 +111,6 @@ func UpdateGlobalSnapshotSettings(w http.ResponseWriter, r *http.Request) {
 		store.Azure = nil
 		store.Google = nil
 		store.Other = nil
-		store.Internal = nil
 
 		store.AWS.UseInstanceRole = updateGlobalSnapshotSettingsRequest.AWS.UseInstanceRole
 		if store.AWS.UseInstanceRole {
@@ -155,7 +148,6 @@ func UpdateGlobalSnapshotSettings(w http.ResponseWriter, r *http.Request) {
 		store.AWS = nil
 		store.Azure = nil
 		store.Other = nil
-		store.Internal = nil
 
 		store.Google.UseInstanceRole = updateGlobalSnapshotSettingsRequest.Google.UseInstanceRole
 		if store.Google.UseInstanceRole {
@@ -196,7 +188,6 @@ func UpdateGlobalSnapshotSettings(w http.ResponseWriter, r *http.Request) {
 		store.AWS = nil
 		store.Google = nil
 		store.Other = nil
-		store.Internal = nil
 
 		if updateGlobalSnapshotSettingsRequest.Azure.ResourceGroup != "" {
 			store.Azure.ResourceGroup = updateGlobalSnapshotSettingsRequest.Azure.ResourceGroup
@@ -233,7 +224,6 @@ func UpdateGlobalSnapshotSettings(w http.ResponseWriter, r *http.Request) {
 		store.AWS = nil
 		store.Google = nil
 		store.Azure = nil
-		store.Internal = nil
 
 		if updateGlobalSnapshotSettingsRequest.Other.AccessKeyID != "" {
 			store.Other.AccessKeyID = updateGlobalSnapshotSettingsRequest.Other.AccessKeyID
@@ -259,52 +249,6 @@ func UpdateGlobalSnapshotSettings(w http.ResponseWriter, r *http.Request) {
 			JSON(w, 400, globalSnapshotSettingsResponse)
 			return
 		}
-	} else if updateGlobalSnapshotSettingsRequest.Internal {
-		if !kurl.IsKurl() {
-			globalSnapshotSettingsResponse.Error = "cannot use internal storage on a non-kurl cluster"
-			JSON(w, 400, globalSnapshotSettingsResponse)
-			return
-		}
-
-		if store.Internal == nil {
-			store.Internal = &snapshottypes.StoreInternal{}
-		}
-		store.AWS = nil
-		store.Google = nil
-		store.Azure = nil
-		store.Other = nil
-
-		cfg, err := config.GetConfig()
-		if err != nil {
-			globalSnapshotSettingsResponse.Error = "failed to get cluster config"
-			JSON(w, 400, globalSnapshotSettingsResponse)
-			return
-		}
-
-		clientset, err := kubernetes.NewForConfig(cfg)
-		if err != nil {
-			globalSnapshotSettingsResponse.Error = "failed to create clientset"
-			JSON(w, 400, globalSnapshotSettingsResponse)
-			return
-		}
-
-		secret, err := clientset.CoreV1().Secrets(os.Getenv("POD_NAMESPACE")).Get(context.TODO(), "kotsadm-s3", metav1.GetOptions{})
-		if err != nil {
-			logger.Error(err)
-			globalSnapshotSettingsResponse.Error = err.Error()
-			JSON(w, 400, globalSnapshotSettingsResponse)
-			return
-		}
-
-		store.Provider = "aws"
-		store.Bucket = string(secret.Data["velero-local-bucket"])
-		store.Path = ""
-
-		store.Internal.AccessKeyID = string(secret.Data["access-key-id"])
-		store.Internal.SecretAccessKey = string(secret.Data["secret-access-key"])
-		store.Internal.Endpoint = string(secret.Data["endpoint"])
-		store.Internal.ObjectStoreClusterIP = string(secret.Data["object-store-cluster-ip"])
-		store.Internal.Region = "us-east-1"
 	}
 
 	if err := snapshot.ValidateStore(store); err != nil {
diff --git a/kotsadm/pkg/snapshot/store.go b/kotsadm/pkg/snapshot/store.go
index fd8e61174..4871bdc0e 100644
--- a/kotsadm/pkg/snapshot/store.go
+++ b/kotsadm/pkg/snapshot/store.go
@@ -4,7 +4,6 @@ import (
 	"bufio"
 	"bytes"
 	"context"
-	"fmt"
 	"strings"
 	"time"
 
@@ -154,18 +153,6 @@ func UpdateGlobalStore(store *types.Store) (*velerov1.BackupStorageLocation, err
 		kotsadmVeleroBackendStorageLocation.Spec.Config["s3Url"] = store.Other.Endpoint
 
 		// create or update the secret
-	} else if store.Internal != nil {
-		kotsadmVeleroBackendStorageLocation.Spec.Config["region"] = store.Internal.Region
-		kotsadmVeleroBackendStorageLocation.Spec.Config["s3Url"] = store.Internal.Endpoint
-		kotsadmVeleroBackendStorageLocation.Spec.Config["publicUrl"] = fmt.Sprintf("http://%s", store.Internal.ObjectStoreClusterIP)
-
-		// delete the secret
-		if currentSecretErr == nil {
-			err = clientset.CoreV1().Secrets(kotsadmVeleroBackendStorageLocation.Namespace).Delete(context.TODO(), "cloud-credentials", metav1.DeleteOptions{})
-			if err != nil {
-				return nil, errors.Wrap(err, "failed to delete internal secret")
-			}
-		}
 	} else if store.Google != nil {
 		if store.Google.UseInstanceRole {
 			kotsadmVeleroBackendStorageLocation.Spec.Config["serviceAccount"] = store.Google.ServiceAccount
@@ -198,7 +185,7 @@ func UpdateGlobalStore(store *types.Store) (*velerov1.BackupStorageLocation, err
 				}
 				_, err = clientset.CoreV1().Secrets(kotsadmVeleroBackendStorageLocation.Namespace).Create(context.TODO(), &toCreate, metav1.CreateOptions{})
 				if err != nil {
-					return nil, errors.Wrap(err, "failed to create google secret")
+					return nil, errors.Wrap(err, "failed to create aws secret")
 				}
 			} else {
 				// update
@@ -209,7 +196,7 @@ func UpdateGlobalStore(store *types.Store) (*velerov1.BackupStorageLocation, err
 				currentSecret.Data["cloud"] = []byte(store.Google.JSONFile)
 				_, err = clientset.CoreV1().Secrets(kotsadmVeleroBackendStorageLocation.Namespace).Update(context.TODO(), currentSecret, metav1.UpdateOptions{})
 				if err != nil {
-					return nil, errors.Wrap(err, "failed to update google secret")
+					return nil, errors.Wrap(err, "failed to update aws secret")
 				}
 			}
 		}
@@ -245,7 +232,7 @@ func UpdateGlobalStore(store *types.Store) (*velerov1.BackupStorageLocation, err
 			}
 			_, err = clientset.CoreV1().Secrets(kotsadmVeleroBackendStorageLocation.Namespace).Create(context.TODO(), &toCreate, metav1.CreateOptions{})
 			if err != nil {
-				return nil, errors.Wrap(err, "failed to create azure secret")
+				return nil, errors.Wrap(err, "failed to create aws secret")
 			}
 		} else {
 			// update
@@ -256,7 +243,7 @@ func UpdateGlobalStore(store *types.Store) (*velerov1.BackupStorageLocation, err
 			currentSecret.Data["cloud"] = providers.RenderAzureConfig(config)
 			_, err = clientset.CoreV1().Secrets(kotsadmVeleroBackendStorageLocation.Namespace).Update(context.TODO(), currentSecret, metav1.UpdateOptions{})
 			if err != nil {
-				return nil, errors.Wrap(err, "failed to update azure secret")
+				return nil, errors.Wrap(err, "failed to update aws secret")
 			}
 		}
 	}
@@ -469,13 +456,6 @@ func ValidateStore(store *types.Store) error {
 		return nil
 	}
 
-	if store.Internal != nil {
-		if err := validateInternal(store.Internal, store.Bucket); err != nil {
-			return errors.Wrap(err, "failed to validate S3-compatible configuration")
-		}
-		return nil
-	}
-
 	return errors.New("no valid configuration found")
 }
 
@@ -627,32 +607,6 @@ func validateOther(storeOther *types.StoreOther, bucket string) error {
 	return nil
 }
 
-func validateInternal(storeInternal *types.StoreInternal, bucket string) error {
-	s3Config := &aws.Config{
-		Region:           aws.String(storeInternal.Region),
-		Endpoint:         aws.String(storeInternal.Endpoint),
-		DisableSSL:       aws.Bool(true), // TODO: this needs to be configurable
-		S3ForcePathStyle: aws.Bool(true),
-	}
-
-	if storeInternal.AccessKeyID != "" && storeInternal.SecretAccessKey != "" {
-		s3Config.Credentials = credentials.NewStaticCredentials(storeInternal.AccessKeyID, storeInternal.SecretAccessKey, "")
-	}
-
-	newSession := session.New(s3Config)
-	s3Client := s3.New(newSession)
-
-	_, err := s3Client.HeadBucket(&s3.HeadBucketInput{
-		Bucket: aws.String(bucket),
-	})
-
-	if err != nil {
-		return errors.Wrap(err, "bucket does not exist")
-	}
-
-	return nil
-}
-
 func Redact(store *types.Store) error {
 	if store == nil {
 		return nil
diff --git a/kotsadm/pkg/snapshot/types/types.go b/kotsadm/pkg/snapshot/types/types.go
index 7659f3d83..7b0b2f9ec 100644
--- a/kotsadm/pkg/snapshot/types/types.go
+++ b/kotsadm/pkg/snapshot/types/types.go
@@ -32,23 +32,14 @@ type StoreOther struct {
 	Endpoint        string `json:"endpoint"`
 }
 
-type StoreInternal struct {
-	Region               string `json:"region"`
-	AccessKeyID          string `json:"accessKeyID"`
-	SecretAccessKey      string `json:"secretAccessKey"` // added for unmarshaling, redacted on marshaling
-	Endpoint             string `json:"endpoint"`
-	ObjectStoreClusterIP string `json:"objectStoreClusterIP"`
-}
-
 type Store struct {
-	Provider string         `json:"provider"`
-	Bucket   string         `json:"bucket"`
-	Path     string         `json:"path"`
-	AWS      *StoreAWS      `json:"aws,omitempty"`
-	Azure    *StoreAzure    `json:"azure,omitempty"`
-	Google   *StoreGoogle   `json:"gcp,omitempty"`
-	Other    *StoreOther    `json:"other,omitempty"`
-	Internal *StoreInternal `json:"internal,omitempty"`
+	Provider string       `json:"provider"`
+	Bucket   string       `json:"bucket"`
+	Path     string       `json:"path"`
+	AWS      *StoreAWS    `json:"aws,omitempty"`
+	Azure    *StoreAzure  `json:"azure,omitempty"`
+	Google   *StoreGoogle `json:"gcp,omitempty"`
+	Other    *StoreOther  `json:"other,omitempty"`
 }
 
 type Backup struct {
diff --git a/kotsadm/web/src/components/snapshots/SnapshotStorageDestination.jsx b/kotsadm/web/src/components/snapshots/SnapshotStorageDestination.jsx
index 043ab8809..051f56c57 100644
--- a/kotsadm/web/src/components/snapshots/SnapshotStorageDestination.jsx
+++ b/kotsadm/web/src/components/snapshots/SnapshotStorageDestination.jsx
@@ -297,9 +297,6 @@ class SnapshotStorageDestination extends Component {
       case "other":
         await this.snapshotProviderS3Compatible();
         break;
-      case "internal":
-        await this.snapshotProviderInternal();
-        break;
     }
   }
 
@@ -331,11 +328,6 @@ class SnapshotStorageDestination extends Component {
     this.props.updateSettings(payload);
   }
 
-  snapshotProviderInternal = async () => {
-    const payload = { internal: true };
-    this.props.updateSettings(payload);
-  }
-
   renderIcons = (destination) => {
     if (destination) {
       return <span className={`icon snapshotDestination--${destination.value}`} />;
@@ -347,7 +339,7 @@ class SnapshotStorageDestination extends Component {
   getDestinationLabel = (destination, label) => {
     return (
       <div style={{ alignItems: "center", display: "flex" }}>
-        <span style={{ fontSize: 18, marginRight: "10px", minWidth: 16, textAlign: "center" }}>{this.renderIcons(destination)}</span>
+        <span style={{ fontSize: 18, marginRight: "10px" }}>{this.renderIcons(destination)}</span>
         <span style={{ fontSize: 14, lineHeight: "16px" }}>{label}</span>
       </div>
     );
@@ -578,11 +570,6 @@ class SnapshotStorageDestination extends Component {
           </div>
         )
 
-      case "internal":
-        return (
-          null
-        )
-
       default:
         return (
           <div>No snapshot destination is selected</div>
@@ -615,7 +602,7 @@ class SnapshotStorageDestination extends Component {
             if (snapshotSettings.isKurl) {
               availableDestinations.push({
                 value: "internal",
-                label: "Internal Storage (Default)",
+                label: "Internal Storage",
               });
             }
             break;
diff --git a/kotsadm/web/src/scss/utilities/icons.scss b/kotsadm/web/src/scss/utilities/icons.scss
index 189cce155..e76cb91bd 100644
--- a/kotsadm/web/src/scss/utilities/icons.scss
+++ b/kotsadm/web/src/scss/utilities/icons.scss
@@ -595,11 +595,6 @@
   width: 16px;
   height: 16px;
 }
-.icon.snapshotDestination--internal {
-  background-position: -182px -378px;
-  width: 16px;
-  height: 16px;
-}
 .icon.awsIcon {
   background-position: -185px -725px;
   width: 27px;
diff --git a/pkg/kotsadm/api_objects.go b/pkg/kotsadm/api_objects.go
index 6c45633f0..c450da243 100644
--- a/pkg/kotsadm/api_objects.go
+++ b/pkg/kotsadm/api_objects.go
@@ -183,8 +183,7 @@ func updateApiDeployment(deployment *appsv1.Deployment, deployOptions types.Depl
 		return errors.New("failed to find kotsadm-api container in deployment")
 	}
 
-	// image
-	deployment.Spec.Template.Spec.Containers[containerIdx].Image = fmt.Sprintf("%s/kotsadm-api:%s", kotsadmRegistry(), kotsadmTag())
+	deployment.Spec.Template.Spec.Containers[containerIdx].Image = fmt.Sprintf("%s/kotsadm-api:%s", kotsadmRegistry(deployOptions.KotsadmOptions), kotsadmTag(deployOptions.KotsadmOptions))
 
 	// copy the env vars from the desired to existing. this could undo a change that the user had.
 	// we don't know which env vars we set and which are user edited. this method avoids deleting
@@ -218,6 +217,15 @@ func apiDeployment(deployOptions types.DeployOptions) *appsv1.Deployment {
 		}
 	}
 
+	var pullSecrets []corev1.LocalObjectReference
+	if s := kotsadmPullSecret(deployOptions.Namespace, deployOptions.KotsadmOptions); s != nil {
+		pullSecrets = []corev1.LocalObjectReference{
+			{
+				Name: s.ObjectMeta.Name,
+			},
+		}
+	}
+
 	deployment := &appsv1.Deployment{
 		TypeMeta: metav1.TypeMeta{
 			APIVersion: "apps/v1",
@@ -244,9 +252,10 @@ func apiDeployment(deployOptions types.DeployOptions) *appsv1.Deployment {
 					SecurityContext:    &securityContext,
 					ServiceAccountName: "kotsadm-api",
 					RestartPolicy:      corev1.RestartPolicyAlways,
+					ImagePullSecrets:   pullSecrets,
 					Containers: []corev1.Container{
 						{
-							Image:           fmt.Sprintf("%s/kotsadm-api:%s", kotsadmRegistry(), kotsadmTag()),
+							Image:           fmt.Sprintf("%s/kotsadm-api:%s", kotsadmRegistry(deployOptions.KotsadmOptions), kotsadmTag(deployOptions.KotsadmOptions)),
 							ImagePullPolicy: corev1.PullAlways,
 							Name:            "kotsadm-api",
 							Ports: []corev1.ContainerPort{
diff --git a/pkg/kotsadm/kotsadm_objects.go b/pkg/kotsadm/kotsadm_objects.go
index 949ea228f..66ea6465a 100644
--- a/pkg/kotsadm/kotsadm_objects.go
+++ b/pkg/kotsadm/kotsadm_objects.go
@@ -170,7 +170,7 @@ func updateKotsadmDeployment(deployment *appsv1.Deployment, deployOptions types.
 	}
 
 	// image
-	deployment.Spec.Template.Spec.Containers[containerIdx].Image = fmt.Sprintf("%s/kotsadm:%s", kotsadmRegistry(), kotsadmTag())
+	deployment.Spec.Template.Spec.Containers[containerIdx].Image = fmt.Sprintf("%s/kotsadm:%s", kotsadmRegistry(deployOptions.KotsadmOptions), kotsadmTag(deployOptions.KotsadmOptions))
 
 	// copy the env vars from the desired to existing. this could undo a change that the user had.
 	// we don't know which env vars we set and which are user edited. this method avoids deleting
@@ -205,6 +205,14 @@ func kotsadmDeployment(deployOptions types.DeployOptions) *appsv1.Deployment {
 	}
 
 	backupSize := resource.MustParse("1Gi")
+	var pullSecrets []corev1.LocalObjectReference
+	if s := kotsadmPullSecret(deployOptions.Namespace, deployOptions.KotsadmOptions); s != nil {
+		pullSecrets = []corev1.LocalObjectReference{
+			{
+				Name: s.ObjectMeta.Name,
+			},
+		}
+	}
 
 	deployment := &appsv1.Deployment{
 		TypeMeta: metav1.TypeMeta{
@@ -249,9 +257,10 @@ func kotsadmDeployment(deployOptions types.DeployOptions) *appsv1.Deployment {
 					},
 					ServiceAccountName: "kotsadm",
 					RestartPolicy:      corev1.RestartPolicyAlways,
+					ImagePullSecrets:   pullSecrets,
 					InitContainers: []corev1.Container{
 						{
-							Image:           fmt.Sprintf("%s/kotsadm:%s", kotsadmRegistry(), kotsadmTag()),
+							Image:           fmt.Sprintf("%s/kotsadm:%s", kotsadmRegistry(deployOptions.KotsadmOptions), kotsadmTag(deployOptions.KotsadmOptions)),
 							ImagePullPolicy: corev1.PullAlways,
 							Name:            "restore-db",
 							Command: []string{
@@ -278,7 +287,7 @@ func kotsadmDeployment(deployOptions types.DeployOptions) *appsv1.Deployment {
 							},
 						},
 						{
-							Image:           fmt.Sprintf("%s/kotsadm:%s", kotsadmRegistry(), kotsadmTag()),
+							Image:           fmt.Sprintf("%s/kotsadm:%s", kotsadmRegistry(deployOptions.KotsadmOptions), kotsadmTag(deployOptions.KotsadmOptions)),
 							ImagePullPolicy: corev1.PullAlways,
 							Name:            "restore-s3",
 							Command: []string{
@@ -330,7 +339,7 @@ func kotsadmDeployment(deployOptions types.DeployOptions) *appsv1.Deployment {
 					},
 					Containers: []corev1.Container{
 						{
-							Image:           fmt.Sprintf("%s/kotsadm:%s", kotsadmRegistry(), kotsadmTag()),
+							Image:           fmt.Sprintf("%s/kotsadm:%s", kotsadmRegistry(deployOptions.KotsadmOptions), kotsadmTag(deployOptions.KotsadmOptions)),
 							ImagePullPolicy: corev1.PullAlways,
 							Name:            "kotsadm",
 							Ports: []corev1.ContainerPort{
diff --git a/pkg/kotsadm/kotsadm_version.go b/pkg/kotsadm/kotsadm_version.go
index 6dc36f0d7..27717c5a4 100644
--- a/pkg/kotsadm/kotsadm_version.go
+++ b/pkg/kotsadm/kotsadm_version.go
@@ -5,25 +5,20 @@ import (
 	"strings"
 
 	"github.com/Masterminds/semver"
+	"github.com/replicatedhq/kots/pkg/docker/registry"
+	"github.com/replicatedhq/kots/pkg/kotsadm/types"
 	"github.com/replicatedhq/kots/pkg/version"
-)
-
-var (
-	OverrideVersion   = ""
-	OverrideRegistry  = ""
-	OverrideNamespace = ""
+	corev1 "k8s.io/api/core/v1"
 )
 
 // return "alpha" for all invalid versions of kots,
 // kotsadm tag that matches this version for others
-func kotsadmTag() string {
-	if OverrideVersion != "" {
-		return OverrideVersion
+func kotsadmTag(options types.KotsadmOptions) string {
+	if options.OverrideVersion != "" {
+		return options.OverrideVersion
 	}
 
-	kotsVersion := version.Version()
-
-	return kotsadmTagForVersionString(kotsVersion)
+	return kotsadmTagForVersionString(version.Version())
 }
 
 func kotsadmTagForVersionString(kotsVersion string) string {
@@ -43,18 +38,44 @@ func kotsadmTagForVersionString(kotsVersion string) string {
 	return kotsVersion
 }
 
-func kotsadmRegistry() string {
-	if OverrideRegistry == "" {
-		if OverrideNamespace == "" {
+func kotsadmRegistry(options types.KotsadmOptions) string {
+	if options.OverrideRegistry == "" {
+		if options.OverrideNamespace == "" {
 			return "kotsadm"
 		} else {
-			return OverrideNamespace
+			return options.OverrideNamespace
 		}
 	}
 
-	if OverrideNamespace == "" {
-		return fmt.Sprintf("%s/kotsadm", OverrideRegistry)
+	registry := options.OverrideRegistry
+	namespace := options.OverrideNamespace
+
+	hostParts := strings.Split(options.OverrideRegistry, "/")
+	if len(hostParts) == 2 {
+		registry = hostParts[0]
+		namespace = hostParts[1]
 	}
 
-	return fmt.Sprintf("%s/%s", OverrideRegistry, OverrideNamespace)
+	if namespace == "" {
+		// note that this makes it impossible to have a registry without a namespace
+		// keeping for backwards compatibility
+		return fmt.Sprintf("%s/kotsadm", registry)
+	}
+
+	return fmt.Sprintf("%s/%s", registry, namespace)
+}
+
+func kotsadmPullSecret(namespace string, options types.KotsadmOptions) *corev1.Secret {
+	if options.OverrideRegistry == "" || options.Username == "" || options.Password == "" {
+		return nil
+	}
+
+	secret, _ := registry.PullSecretForRegistries([]string{options.OverrideRegistry}, options.Username, options.Password, namespace)
+	if secret == nil {
+		return nil
+	}
+
+	secret.ObjectMeta.Name = types.PrivateKotsadmRegistrySecret
+
+	return secret
 }
diff --git a/pkg/kotsadm/main.go b/pkg/kotsadm/main.go
index bb3e393bf..279af7b6b 100644
--- a/pkg/kotsadm/main.go
+++ b/pkg/kotsadm/main.go
@@ -343,6 +343,10 @@ func ensureKotsadm(deployOptions types.DeployOptions, clientset *kubernetes.Clie
 		}
 	}
 
+	if err := ensureKotsadmPullSecrets(&deployOptions, clientset); err != nil {
+		return errors.Wrap(err, "failed to ensure postgres")
+	}
+
 	if err := ensurePostgres(deployOptions, clientset); err != nil {
 		return errors.Wrap(err, "failed to ensure postgres")
 	}
diff --git a/pkg/kotsadm/minio_objects.go b/pkg/kotsadm/minio_objects.go
index be0eed1f4..d6790ab59 100644
--- a/pkg/kotsadm/minio_objects.go
+++ b/pkg/kotsadm/minio_objects.go
@@ -51,6 +51,15 @@ func minioStatefulset(deployOptions types.DeployOptions) *appsv1.StatefulSet {
 		}
 	}
 
+	var pullSecrets []corev1.LocalObjectReference
+	if s := kotsadmPullSecret(deployOptions.Namespace, deployOptions.KotsadmOptions); s != nil {
+		pullSecrets = []corev1.LocalObjectReference{
+			{
+				Name: s.ObjectMeta.Name,
+			},
+		}
+	}
+
 	statefulset := &appsv1.StatefulSet{
 		TypeMeta: metav1.TypeMeta{
 			APIVersion: "apps/v1",
@@ -99,7 +108,8 @@ func minioStatefulset(deployOptions types.DeployOptions) *appsv1.StatefulSet {
 					},
 				},
 				Spec: corev1.PodSpec{
-					SecurityContext: &securityContext,
+					SecurityContext:  &securityContext,
+					ImagePullSecrets: pullSecrets,
 					Volumes: []corev1.Volume{
 						{
 							Name: "kotsadm-minio",
@@ -118,7 +128,7 @@ func minioStatefulset(deployOptions types.DeployOptions) *appsv1.StatefulSet {
 					},
 					Containers: []corev1.Container{
 						{
-							Image:           fmt.Sprintf("%s/minio:%s", kotsadmRegistry(), kotsadmTag()),
+							Image:           fmt.Sprintf("%s/minio:%s", kotsadmRegistry(deployOptions.KotsadmOptions), kotsadmTag(deployOptions.KotsadmOptions)),
 							ImagePullPolicy: corev1.PullIfNotPresent,
 							Name:            "kotsadm-minio",
 							Command: []string{
diff --git a/pkg/kotsadm/operator_objects.go b/pkg/kotsadm/operator_objects.go
index 8647c6aaf..d57a59cc1 100644
--- a/pkg/kotsadm/operator_objects.go
+++ b/pkg/kotsadm/operator_objects.go
@@ -164,7 +164,7 @@ func updateOperatorDeployment(deployment *appsv1.Deployment, deployOptions types
 	}
 
 	// image
-	deployment.Spec.Template.Spec.Containers[containerIdx].Image = fmt.Sprintf("%s/kotsadm-operator:%s", kotsadmRegistry(), kotsadmTag())
+	deployment.Spec.Template.Spec.Containers[containerIdx].Image = fmt.Sprintf("%s/kotsadm-operator:%s", kotsadmRegistry(deployOptions.KotsadmOptions), kotsadmTag(deployOptions.KotsadmOptions))
 
 	// copy the env vars from the desired to existing. this could undo a change that the user had.
 	// we don't know which env vars we set and which are user edited. this method avoids deleting
@@ -198,6 +198,15 @@ func operatorDeployment(deployOptions types.DeployOptions) *appsv1.Deployment {
 		}
 	}
 
+	var pullSecrets []corev1.LocalObjectReference
+	if s := kotsadmPullSecret(deployOptions.Namespace, deployOptions.KotsadmOptions); s != nil {
+		pullSecrets = []corev1.LocalObjectReference{
+			{
+				Name: s.ObjectMeta.Name,
+			},
+		}
+	}
+
 	deployment := &appsv1.Deployment{
 		TypeMeta: metav1.TypeMeta{
 			APIVersion: "apps/v1",
@@ -224,9 +233,10 @@ func operatorDeployment(deployOptions types.DeployOptions) *appsv1.Deployment {
 					SecurityContext:    &securityContext,
 					ServiceAccountName: "kotsadm-operator",
 					RestartPolicy:      corev1.RestartPolicyAlways,
+					ImagePullSecrets:   pullSecrets,
 					Containers: []corev1.Container{
 						{
-							Image:           fmt.Sprintf("%s/kotsadm-operator:%s", kotsadmRegistry(), kotsadmTag()),
+							Image:           fmt.Sprintf("%s/kotsadm-operator:%s", kotsadmRegistry(deployOptions.KotsadmOptions), kotsadmTag(deployOptions.KotsadmOptions)),
 							ImagePullPolicy: corev1.PullAlways,
 							Name:            "kotsadm-operator",
 							Env: []corev1.EnvVar{
diff --git a/pkg/kotsadm/postgres_objects.go b/pkg/kotsadm/postgres_objects.go
index d528a2159..627048a28 100644
--- a/pkg/kotsadm/postgres_objects.go
+++ b/pkg/kotsadm/postgres_objects.go
@@ -1,6 +1,7 @@
 package kotsadm
 
 import (
+	"fmt"
 	"os"
 
 	"github.com/replicatedhq/kots/pkg/kotsadm/types"
@@ -15,6 +16,17 @@ import (
 func postgresStatefulset(deployOptions types.DeployOptions) *appsv1.StatefulSet {
 	size := resource.MustParse("1Gi")
 
+	image := "postgres:10.7"
+	var pullSecrets []corev1.LocalObjectReference
+	if s := kotsadmPullSecret(deployOptions.Namespace, deployOptions.KotsadmOptions); s != nil {
+		image = fmt.Sprintf("%s/postgres:%s", kotsadmRegistry(deployOptions.KotsadmOptions), kotsadmTag(deployOptions.KotsadmOptions))
+		pullSecrets = []corev1.LocalObjectReference{
+			{
+				Name: s.ObjectMeta.Name,
+			},
+		}
+	}
+
 	if deployOptions.LimitRange != nil {
 		var allowedMax *resource.Quantity
 		var allowedMin *resource.Quantity
@@ -90,7 +102,8 @@ func postgresStatefulset(deployOptions types.DeployOptions) *appsv1.StatefulSet
 					}),
 				},
 				Spec: corev1.PodSpec{
-					SecurityContext: &securityContext,
+					SecurityContext:  &securityContext,
+					ImagePullSecrets: pullSecrets,
 					Volumes: []corev1.Volume{
 						{
 							Name: "kotsadm-postgres",
@@ -103,7 +116,7 @@ func postgresStatefulset(deployOptions types.DeployOptions) *appsv1.StatefulSet
 					},
 					Containers: []corev1.Container{
 						{
-							Image:           "postgres:10.7",
+							Image:           image,
 							ImagePullPolicy: corev1.PullIfNotPresent,
 							Name:            "kotsadm-postgres",
 							Ports: []corev1.ContainerPort{
diff --git a/pkg/kotsadm/restore_job.go b/pkg/kotsadm/restore_job.go
index 2a0a6da4b..a93e07163 100644
--- a/pkg/kotsadm/restore_job.go
+++ b/pkg/kotsadm/restore_job.go
@@ -11,6 +11,7 @@ import (
 )
 
 func restoreJob(backupName string, namespace string, isOpenShift bool) *batchv1.Job {
+	deployOptions := types.DeployOptions{} // TODO: make this real
 	var securityContext corev1.PodSecurityContext
 	if !isOpenShift {
 		securityContext = corev1.PodSecurityContext{
@@ -19,6 +20,15 @@ func restoreJob(backupName string, namespace string, isOpenShift bool) *batchv1.
 		}
 	}
 
+	var pullSecrets []corev1.LocalObjectReference
+	if s := kotsadmPullSecret(deployOptions.Namespace, deployOptions.KotsadmOptions); s != nil {
+		pullSecrets = []corev1.LocalObjectReference{
+			{
+				Name: s.ObjectMeta.Name,
+			},
+		}
+	}
+
 	job := &batchv1.Job{
 		TypeMeta: metav1.TypeMeta{
 			APIVersion: "batch/v1",
@@ -40,9 +50,10 @@ func restoreJob(backupName string, namespace string, isOpenShift bool) *batchv1.
 					SecurityContext:    &securityContext,
 					RestartPolicy:      corev1.RestartPolicyNever,
 					ServiceAccountName: "kotsadm",
+					ImagePullSecrets:   pullSecrets,
 					Containers: []corev1.Container{
 						{
-							Image:           fmt.Sprintf("%s/kotsadm:%s", kotsadmRegistry(), kotsadmTag()),
+							Image:           fmt.Sprintf("%s/kotsadm:%s", kotsadmRegistry(deployOptions.KotsadmOptions), kotsadmTag(deployOptions.KotsadmOptions)),
 							ImagePullPolicy: corev1.PullAlways,
 							Name:            "kotsadm-restore",
 							Command: []string{
diff --git a/pkg/kotsadm/schemahero_objects.go b/pkg/kotsadm/schemahero_objects.go
index 3d7ddf909..7591edcdb 100644
--- a/pkg/kotsadm/schemahero_objects.go
+++ b/pkg/kotsadm/schemahero_objects.go
@@ -21,6 +21,15 @@ func migrationsPod(deployOptions types.DeployOptions) *corev1.Pod {
 		}
 	}
 
+	var pullSecrets []corev1.LocalObjectReference
+	if s := kotsadmPullSecret(deployOptions.Namespace, deployOptions.KotsadmOptions); s != nil {
+		pullSecrets = []corev1.LocalObjectReference{
+			{
+				Name: s.ObjectMeta.Name,
+			},
+		}
+	}
+
 	pod := &corev1.Pod{
 		TypeMeta: metav1.TypeMeta{
 			APIVersion: "v1",
@@ -32,11 +41,12 @@ func migrationsPod(deployOptions types.DeployOptions) *corev1.Pod {
 			Labels:    types.GetKotsadmLabels(),
 		},
 		Spec: corev1.PodSpec{
-			SecurityContext: &securityContext,
-			RestartPolicy:   corev1.RestartPolicyOnFailure,
+			SecurityContext:  &securityContext,
+			RestartPolicy:    corev1.RestartPolicyOnFailure,
+			ImagePullSecrets: pullSecrets,
 			Containers: []corev1.Container{
 				{
-					Image:           fmt.Sprintf("%s/kotsadm-migrations:%s", kotsadmRegistry(), kotsadmTag()),
+					Image:           fmt.Sprintf("%s/kotsadm-migrations:%s", kotsadmRegistry(deployOptions.KotsadmOptions), kotsadmTag(deployOptions.KotsadmOptions)),
 					ImagePullPolicy: corev1.PullAlways,
 					Name:            name,
 					Env: []corev1.EnvVar{
diff --git a/pkg/kotsadm/secrets.go b/pkg/kotsadm/secrets.go
index b994039bd..864c3ced4 100644
--- a/pkg/kotsadm/secrets.go
+++ b/pkg/kotsadm/secrets.go
@@ -73,15 +73,32 @@ func getSecretsYAML(deployOptions *types.DeployOptions) (map[string][]byte, erro
 	}
 	docs["secret-s3.yaml"] = s3.Bytes()
 
-	var secret bytes.Buffer
-	if err := s.Encode(apiClusterTokenSecret(*deployOptions), &secret); err != nil {
+	var tokenSecret bytes.Buffer
+	if err := s.Encode(apiClusterTokenSecret(*deployOptions), &tokenSecret); err != nil {
 		return nil, errors.Wrap(err, "failed to marshal api cluster token secret")
 	}
-	docs["secret-api-cluster-token.yaml"] = secret.Bytes()
+	docs["secret-api-cluster-token.yaml"] = tokenSecret.Bytes()
+
+	// this secret is optional
+	if secret := privateKotsadmRegistrySecret(*deployOptions); secret != nil {
+		var registrySecret bytes.Buffer
+		if err := s.Encode(secret, &registrySecret); err != nil {
+			return nil, errors.Wrap(err, "failed to marshal private kotsadm registry secret")
+		}
+		docs["private-kotsadm-registry-secret.yaml"] = registrySecret.Bytes()
+	}
 
 	return docs, nil
 }
 
+func ensureKotsadmPullSecrets(deployOptions *types.DeployOptions, clientset *kubernetes.Clientset) error {
+	if err := ensurePrivateKotsadmRegistrySecret(*deployOptions, clientset); err != nil {
+		return errors.Wrap(err, "failed to ensure private kotsadm registry secret")
+	}
+
+	return nil
+}
+
 func ensureSecrets(deployOptions *types.DeployOptions, clientset *kubernetes.Clientset) error {
 	if err := ensureJWTSessionSecret(deployOptions.Namespace, clientset); err != nil {
 		return errors.Wrap(err, "failed to ensure jwt session secret")
@@ -328,7 +345,7 @@ func ensureAPIClusterTokenSecret(deployOptions types.DeployOptions, clientset *k
 
 		_, err := clientset.CoreV1().Secrets(deployOptions.Namespace).Create(context.TODO(), apiClusterTokenSecret(deployOptions), metav1.CreateOptions{})
 		if err != nil {
-			return errors.Wrap(err, "Failed to create cluster token secret")
+			return errors.Wrap(err, "failed to create cluster token secret")
 		}
 	}
 
@@ -354,3 +371,24 @@ func getAPIClusterToken(namespace string, clientset *kubernetes.Clientset) (stri
 
 	return string(tokenBytes), nil
 }
+
+func ensurePrivateKotsadmRegistrySecret(deployOptions types.DeployOptions, clientset *kubernetes.Clientset) error {
+	_, err := clientset.CoreV1().Secrets(deployOptions.Namespace).Get(context.TODO(), types.PrivateKotsadmRegistrySecret, metav1.GetOptions{})
+	if err != nil {
+		if !kuberneteserrors.IsNotFound(err) {
+			return errors.Wrap(err, "failed to get existing private kotsadm registry secret")
+		}
+
+		secret := privateKotsadmRegistrySecret(deployOptions)
+		if secret == nil {
+			return nil
+		}
+
+		_, err := clientset.CoreV1().Secrets(deployOptions.Namespace).Create(context.TODO(), secret, metav1.CreateOptions{})
+		if err != nil {
+			return errors.Wrap(err, "failed to create private kotsadm registry secret")
+		}
+	}
+
+	return nil
+}
diff --git a/pkg/kotsadm/secrets_objects.go b/pkg/kotsadm/secrets_objects.go
index 007d59679..21e2ddbbd 100644
--- a/pkg/kotsadm/secrets_objects.go
+++ b/pkg/kotsadm/secrets_objects.go
@@ -130,3 +130,7 @@ func apiClusterTokenSecret(deployOptions types.DeployOptions) *corev1.Secret {
 		},
 	}
 }
+
+func privateKotsadmRegistrySecret(deployOptions types.DeployOptions) *corev1.Secret {
+	return kotsadmPullSecret(deployOptions.Namespace, deployOptions.KotsadmOptions)
+}
diff --git a/pkg/kotsadm/types/constants.go b/pkg/kotsadm/types/constants.go
index 9a95e71bc..222ec0d37 100644
--- a/pkg/kotsadm/types/constants.go
+++ b/pkg/kotsadm/types/constants.go
@@ -4,6 +4,7 @@ const KotsadmKey = "kots.io/kotsadm"
 const KotsadmLabelValue = "true"
 
 const ClusterTokenSecret = "kotsadm-cluster-token"
+const PrivateKotsadmRegistrySecret = "kotsadm-private-registry"
 
 const VeleroKey = "kots.io/backup-type"
 const VeleroLabelConsoleValue = "admin-console"
diff --git a/pkg/kotsadm/types/deployoptions.go b/pkg/kotsadm/types/deployoptions.go
index aa86d6635..44e30b8e4 100644
--- a/pkg/kotsadm/types/deployoptions.go
+++ b/pkg/kotsadm/types/deployoptions.go
@@ -32,4 +32,6 @@ type DeployOptions struct {
 	IncludeMinio              bool
 	IncludeDockerDistribution bool
 	Timeout                   time.Duration
+
+	KotsadmOptions KotsadmOptions
 }
diff --git a/pkg/kotsadm/types/registryoptions.go b/pkg/kotsadm/types/registryoptions.go
new file mode 100644
index 000000000..91653afac
--- /dev/null
+++ b/pkg/kotsadm/types/registryoptions.go
@@ -0,0 +1,9 @@
+package types
+
+type KotsadmOptions struct {
+	OverrideVersion   string
+	OverrideRegistry  string
+	OverrideNamespace string
+	Username          string
+	Password          string
+}
diff --git a/pkg/kotsadm/types/upgradeoptions.go b/pkg/kotsadm/types/upgradeoptions.go
index fcd59b5d9..b3c3993b4 100644
--- a/pkg/kotsadm/types/upgradeoptions.go
+++ b/pkg/kotsadm/types/upgradeoptions.go
@@ -11,4 +11,6 @@ type UpgradeOptions struct {
 	KubernetesConfigFlags *genericclioptions.ConfigFlags
 	ForceUpgradeKurl      bool
 	Timeout               time.Duration
+
+	KotsadmOptions KotsadmOptions
 }


Test patch:
diff --git a/pkg/kotsadm/kotsad_version_test.go b/pkg/kotsadm/kotsad_version_test.go
index c1e6bba14..148912b6f 100644
--- a/pkg/kotsadm/kotsad_version_test.go
+++ b/pkg/kotsadm/kotsad_version_test.go
@@ -3,6 +3,7 @@ package kotsadm
 import (
 	"testing"
 
+	"github.com/replicatedhq/kots/pkg/kotsadm/types"
 	"github.com/stretchr/testify/assert"
 	"go.undefinedlabs.com/scopeagent"
 )
@@ -36,11 +37,14 @@ func Test_kotsadmRegistry(t *testing.T) {
 		t.Run(test.name, func(t *testing.T) {
 			scopetest := scopeagent.StartTest(t)
 			defer scopetest.End()
-			OverrideVersion = test.overrideVersion
-			OverrideRegistry = test.overrideRegistry
-			OverrideNamespace = test.overrideNamespace
 
-			actual := kotsadmRegistry()
+			options := types.KotsadmOptions{
+				OverrideVersion:   test.overrideVersion,
+				OverrideRegistry:  test.overrideRegistry,
+				OverrideNamespace: test.overrideNamespace,
+			}
+
+			actual := kotsadmRegistry(options)
 			assert.Equal(t, test.expected, actual)
 		})
 	}