Repository: replicatedhq/kots
Pull request #780 (2020-07-10 20:13:09)

404 when app download not found


Patch:
diff --git a/.github/workflows/kotsadm.yaml b/.github/workflows/kotsadm.yaml
index 0556e717c..1b5eee8f1 100644
--- a/.github/workflows/kotsadm.yaml
+++ b/.github/workflows/kotsadm.yaml
@@ -12,7 +12,7 @@ jobs:
   generate-schema:
     runs-on: ubuntu-18.04
     container:
-      image: schemahero/schemahero:0.9.0
+      image: schemahero/schemahero:0.7.2
     steps:
       - name: Checkout
         uses: actions/checkout@v2
@@ -89,8 +89,6 @@ 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 }}
@@ -103,8 +101,6 @@ jobs:
 
     - name: Build
       if: ${{ github.ref == 'refs/heads/master' }}
-      env:
-        DOCKER_CONFIG: ./.docker
       run: |
         make -C kotsadm/migrations schema-alpha
 
@@ -112,17 +108,8 @@ jobs:
       if: ${{ steps.get_tag.outputs.GIT_TAG != '' }}
       env:
         GIT_TAG: ${{ steps.get_tag.outputs.GIT_TAG }}
-        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
+      run: |
+        make -C kotsadm/migrations schema-release
 
 
   test_web:
@@ -695,14 +682,7 @@ 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-push-images.go b/cmd/kots/cli/admin-console-push-images.go
deleted file mode 100644
index 4ff1f84a6..000000000
--- a/cmd/kots/cli/admin-console-push-images.go
+++ /dev/null
@@ -1,52 +0,0 @@
-package cli
-
-import (
-	"os"
-
-	"github.com/pkg/errors"
-	"github.com/replicatedhq/kots/pkg/kotsadm"
-	kotsadmtypes "github.com/replicatedhq/kots/pkg/kotsadm/types"
-	"github.com/spf13/cobra"
-	"github.com/spf13/viper"
-)
-
-func AdminPushImagesCmd() *cobra.Command {
-	cmd := &cobra.Command{
-		Use:           "push-images [airgap filename] [registry host]",
-		Short:         "Push admin console images",
-		Long:          "Push admin console images from airgap bundle to a private registry",
-		SilenceUsage:  true,
-		SilenceErrors: false,
-		PreRun: func(cmd *cobra.Command, args []string) {
-			viper.BindPFlags(cmd.Flags())
-		},
-		RunE: func(cmd *cobra.Command, args []string) error {
-			v := viper.GetViper()
-
-			if len(args) != 2 {
-				cmd.Help()
-				os.Exit(1)
-			}
-
-			options := kotsadmtypes.PushKotsadmImagesOptions{
-				AirgapArchive:  args[0],
-				Registry:       args[1],
-				Username:       v.GetString("registry-username"),
-				Password:       v.GetString("registry-password"),
-				ProgressWriter: os.Stdout,
-			}
-
-			err := kotsadm.PushImages(options)
-			if err != nil {
-				return errors.Wrap(err, "failed to push images")
-			}
-
-			return nil
-		},
-	}
-
-	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")
-
-	return cmd
-}
diff --git a/cmd/kots/cli/admin-console-upgrade.go b/cmd/kots/cli/admin-console-upgrade.go
index 13d2cd341..88b687b95 100644
--- a/cmd/kots/cli/admin-console-upgrade.go
+++ b/cmd/kots/cli/admin-console-upgrade.go
@@ -52,14 +52,6 @@ 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"))
@@ -69,6 +61,10 @@ 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/admin-console.go b/cmd/kots/cli/admin-console.go
index 7c8642c27..6edb596af 100644
--- a/cmd/kots/cli/admin-console.go
+++ b/cmd/kots/cli/admin-console.go
@@ -81,7 +81,6 @@ func AdminConsoleCmd() *cobra.Command {
 	}
 
 	cmd.AddCommand(AdminConsoleUpgradeCmd())
-	cmd.AddCommand(AdminPushImagesCmd())
 
 	return cmd
 }
diff --git a/cmd/kots/cli/install.go b/cmd/kots/cli/install.go
index 6b25b9246..28093bc90 100644
--- a/cmd/kots/cli/install.go
+++ b/cmd/kots/cli/install.go
@@ -74,6 +74,10 @@ 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")
@@ -124,14 +128,6 @@ 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"))
@@ -226,14 +222,12 @@ 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/design/_template.md b/design/_template.md
index d0be03347..b83893325 100644
--- a/design/_template.md
+++ b/design/_template.md
@@ -37,10 +37,6 @@ The same applies to changes in CRDs, YAML examples, and so on.
 
 Ideally the changes should be made in sequence so that the work required to implement this design can be done incrementally, possibly in parallel.
 
-## Testing
-
-Write a summary of how this enhancement will be tested to ensure there are no regressions in the future.
-
 ## Alternatives Considered
 
 If there are alternative high level or detailed designs that were not pursued they should be called out here with a brief explanation of why they were not pursued.
diff --git a/design/airgap-existing-cluster.md b/design/airgap-existing-cluster.md
deleted file mode 100644
index 9dcdfe6dd..000000000
--- a/design/airgap-existing-cluster.md
+++ /dev/null
@@ -1,109 +0,0 @@
-# Support for installing KOTS Admin Console in Airgapped Clusters
-
-## Goals
-
-- Install kotsadm to an existing cluster where the cluster does not have internet access
-- Install kotsadm to an existing cluster from a workstation where the workstation does not have internet access at the time of installation
-- Support different credentials for push / pull (workstation / cluster) to the local registry
-- Support temporary/dynamic and persistent/static credentials for pushing kotsadm images to the registry
-- Support only persistent/static credentials for pulling kotsadm images from the airgapped registry
-- Support different credentials for pushing vs pulling images to the local registry
-
-## Non Goals
-
-- Create a single installer for kotsadm + application
-- Create a single download for kotsadm, kots plugin, application and license
-- Unattended, automatic installs of kotsadm to existing clusters in airgap mode
-- Support for installing versions of KOTS different from the KOTS CLI version installed
-
-## Background
-
-It's not currently possible to install KOTS Admin Console into an existing cluster that doesn't have outbound internet access.
-
-## High-Level Design
-
-Airgap Workstation: the computer with kubectl access to the airgap cluster
-Public Workstation: the computer with access to the internet
-
-From the public workstation:
-1. The user downloads the kots plugin suitable for Airgap Workstation.
-1. The user downloads the kotsadm archive containing kots container images
-1. The user downloads the airgap bundle of the application
-1. The user copies the kots plugin, the kotsadm archive and the application airgap bundle onto a medium (eg a USB stick) that can be transfered into the airgapped network.
-
-From the airgap workstation:
-1. The user will install the kots plugin 
-1. The user will run a `kubectl kots` command to copy the container images in the kotsadm archive to the local registry, providing registry details for pushing
-1. The user will install kotsadm to the cluster using the kots plugin via `kots install` passing local registry information which will be used to generate the kotsadm yaml and image pull secrets (if neccessary)
-1. The user will install the application and license using the existing airgap installation process 
-
-## Detailed Design
-
-The CI process, on tag, will create the kotsadm archive and include it in the GitHub Release.
-The filename will be `kots_airgap_<version>.tar.gz` and this file contains .tar file exports of all kotsadm images.
-The files SHOULD be stored in the archive in the same format as application airgap files are stored.
-
-Add a `kots admin-console push-images` to the kots cli to retag and push all kotsadm images to an authenticated registry that is accessible from the cluster, using the desired push credentials.
-This new command is used to copy all of the KOTS Admin Console images from one location to another.
-
-Example:
-
-```
-kubectl kots admin-console push-images ./kotsadm-1.16.1.tar.gz registry.somebigbank.com/my-app \
-  --registry-username push-user \
-  --registry-password abcdef
-```
-
-Stretch: `--registry-username` and `--registry-password` are both optional.
-If not present, attempt to authenticate using the already authenticated local docker credentials.
-Stretch: if still not authenticated, prompt for username/password, use these to push but do not save locally.
-Stretch: if username is specified and password is not, prompt for password to use, do not save locally.
-
-KOTS will then load the images from the file in the first parameter, retag them to the local registry and push. 
-All images will retain the same image name and tag, just the registry, namespace will change.
-
-At this point, the images are in the registry, but not running in the cluster yet.
-A workflow could scan these images before continuing.
-
-Next, add a flag to `kots install` to collect / rewrite local registry endpoint/namespace into kots manifests, using the desired pull credentials.
-This will assume that the images are already in the destination.
-
-For example:
-
-```
-kubectl kots install --kotsadm-registry registry.somebigbank.com/my-app \
-  --registry-username pull-user \
-  --registry-password fedcba \
-  app-name
-```
-
-If a `registry-username` and `registry-password` are provided, an `imagePullSecret` will be created and deployed with the registry credentials and added to all kotsadm pods.
-
-The only difference between the command above and an online installation is that the `image` tag will be rewritten in all KOTS Admin Console manifests to point to `registry.somebigbank.com/my-app` and an `imagePullSecret` MAY be added.
-All image names and tags will be unchanged.
-For example, the kotsadm-operator will be `registry.somebigbank.com/my-app/kotsadm-operator:v1.16.1`.
-
-Stretch: Additionally, a new `--yaml` flag will be added to have `kots` only generate the yaml (not apply) to support gitops workflows.
-
-## Limitations
-
-- No support for custom branding in airgap installations to existing clusters
-- No support for `requireMinimalRBACPrivileges` set to `true` (all installs will be cluster admin, which matches all use cases of KOTS install today)
-
-## Assumptions
-
-- The airgap workstation will have kubectl access to the cluster
-- The airgap workstation must have push access to an image registry that the cluster has pull access 
-- The cluster credentials to pull from the local registry is either unauthenticated or static and persistent credentials (i.e. no Vault integration or other dynamically generated credentials will be supported)
-
-## Testing
-
-TBD
-
-## Alternatives Considered
-
-
-
-## Security Considerations
-
-- No open security considerations
diff --git a/kotsadm/.buildkite/pipeline.yml b/kotsadm/.buildkite/pipeline.yml
index ca384f0a4..e1c4f0277 100644
--- a/kotsadm/.buildkite/pipeline.yml
+++ b/kotsadm/.buildkite/pipeline.yml
@@ -6,7 +6,7 @@ steps:
       - "migrations/fixtures/schema/fixtures.sql"
     plugins:
       - docker#v3.5.0:
-          image: schemahero/schemahero:0.9.0
+          image: schemahero/schemahero:0.7.2
           always-pull: true
           workdir: /repo
           debug: true
diff --git a/kotsadm/CONTRIBUTING.md b/kotsadm/CONTRIBUTING.md
index eb53886ba..870161b1a 100644
--- a/kotsadm/CONTRIBUTING.md
+++ b/kotsadm/CONTRIBUTING.md
@@ -7,7 +7,7 @@ Contributions are definitely welcome! We've published documentation on how to se
 The recommended configuration to build and run kotsadm on a microk8s cluster, locally.
 
 Required Software:
-- [Skaffold](https://skaffold.dev) 1.10.1 or later
+- [Skaffold](https://skaffold.dev) 0.29.0 or later
 - [Kustomize](https://kustomize.io) 2.0 or later
 - Kubernetes (Recommended to run microk8s)
 
@@ -19,6 +19,12 @@ Build Kotsadm go binary:
 make kotsadm
 ```
 
+Apply Schemahero CRDs
+
+```
+kubectl apply -f migrations/kustomize/base/schemahero.yaml
+```
+
 Next, you can build and run all server components in the Kubernetes cluster with:
 
 ```
diff --git a/kotsadm/Makefile b/kotsadm/Makefile
index c1e34770e..05ad5c60c 100644
--- a/kotsadm/Makefile
+++ b/kotsadm/Makefile
@@ -40,8 +40,8 @@ build-alpha:
 build-release:
 	docker build -f deploy/Dockerfile --build-arg version=${GIT_TAG} -t kotsadm/kotsadm:${GIT_TAG} .
 	docker push kotsadm/kotsadm:${GIT_TAG}
-	mkdir -p bin/docker-archive/kotsadm
-	skopeo copy docker-daemon:kotsadm/kotsadm:${GIT_TAG} docker-archive:bin/docker-archive/kotsadm/${GIT_TAG}
+	mkdir -p bin/docker-archive
+	skopeo copy docker-daemon:kotsadm/kotsadm:${GIT_TAG} docker-archive:bin/docker-archive/kotsadm.tar
 
 .PHONY: project-pact-tests
 project-pact-tests:
diff --git a/kotsadm/api/Makefile b/kotsadm/api/Makefile
index a42e362c8..9e3130457 100644
--- a/kotsadm/api/Makefile
+++ b/kotsadm/api/Makefile
@@ -83,8 +83,8 @@ build-alpha: build_and_push
 .PHONY: build-release
 build-release: IMAGE = kotsadm/${PROJECT_NAME}:${GIT_TAG}
 build-release: build_and_push
-	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
+	skopeo copy docker-daemon:kotsadm/${PROJECT_NAME}:${GIT_TAG} docker-archive:bin/docker-archive/${PROJECT_NAME}.tar
 
 .PHONY: build_and_push
 build_and_push:
diff --git a/kotsadm/api/src/kots_app/resolvers/kots_app_queries.ts b/kotsadm/api/src/kots_app/resolvers/kots_app_queries.ts
index 6780b062b..9e021e825 100644
--- a/kotsadm/api/src/kots_app/resolvers/kots_app_queries.ts
+++ b/kotsadm/api/src/kots_app/resolvers/kots_app_queries.ts
@@ -79,6 +79,17 @@ export function KotsQueries(stores: Stores, params: Params) {
       return versions;
     },
 
+    async getAppRegistryDetails(root: any, args: any, context: Context): Promise<KotsAppRegistryDetails | {}> {
+      const appId = await stores.kotsAppStore.getIdFromSlug(args.slug);
+      const app = await context.getApp(appId);
+      const details = await stores.kotsAppStore.getAppRegistryDetails(app.id, true);
+      if (!details.registryHostname) {
+        return {}
+      }
+      return details;
+    },
+
+
     async getAppConfigGroups(root: any, args: any, context: Context): Promise<KotsConfigGroup[]> {
       const appId = await stores.kotsAppStore.getIdFromSlug(args.slug);
       const app = await context.getApp(appId);
diff --git a/kotsadm/api/src/schema/query.ts b/kotsadm/api/src/schema/query.ts
index e87eff9aa..3516ffe48 100644
--- a/kotsadm/api/src/schema/query.ts
+++ b/kotsadm/api/src/schema/query.ts
@@ -22,6 +22,7 @@ export const Query = `
     listPendingKotsVersions(slug: String!): [KotsVersion]
     listPastKotsVersions(slug: String!): [KotsVersion]
     getCurrentKotsVersion(slug: String!): KotsVersion
+    getAppRegistryDetails(slug: String!): KotsAppRegistryDetails
 
     listHelmCharts: [HelmChart]
     getHelmChart(id: String!): HelmChart
@@ -34,6 +35,8 @@ export const Query = `
     getKotsDownstreamOutput(appSlug: String!, clusterSlug: String!, sequence: Int!): KotsDownstreamOutput
     templateConfigGroups(slug: String!, sequence: Int!, configGroups: [KotsConfigGroupInput]!): [KotsConfigGroup]
 
+    validateRegistryInfo(slug: String, endpoint: String, username: String, password: String, org: String): String!
+
     listSupportBundles(watchSlug: String!): [SupportBundle]
     getSupportBundle(watchSlug: String!): SupportBundle
     getSupportBundleCommand(watchSlug: String): String
diff --git a/kotsadm/api/src/schema/resolvers.ts b/kotsadm/api/src/schema/resolvers.ts
index 839210fca..febb8c7bc 100644
--- a/kotsadm/api/src/schema/resolvers.ts
+++ b/kotsadm/api/src/schema/resolvers.ts
@@ -1,4 +1,4 @@
-import { UserMutations } from "../user";
+import { UserQueries, UserMutations } from "../user";
 import { ClusterMutations, ClusterQueries } from "../cluster";
 import { FeatureMutations, FeatureQueries } from "../feature";
 import { HealthzQueries } from "../healthz";
@@ -21,6 +21,7 @@ export const Resolvers = (stores: Stores, params: Params) => {
     ...PrefightQueries(stores),
     ...AppsQueries(stores),
     ...ClusterQueries(stores),
+    ...UserQueries(stores),
     ...MonitoringQueries(stores),
     ...KotsQueries(stores, params),
     ...SnapshotQueries(stores, params),
diff --git a/kotsadm/api/src/snapshots/graphql/snapshot_types.ts b/kotsadm/api/src/snapshots/graphql/snapshot_types.ts
index 508dc265b..dbe76d743 100644
--- a/kotsadm/api/src/snapshots/graphql/snapshot_types.ts
+++ b/kotsadm/api/src/snapshots/graphql/snapshot_types.ts
@@ -135,8 +135,8 @@ const SnapshotHook = `
     stderr: String
     started: String
     finished: String
-    error: [SnapshotError]
-    warning: [SnapshotError]
+    errors: [SnapshotError]
+    warnings: [SnapshotError]
   }
 `;
 
diff --git a/kotsadm/api/src/snapshots/resolvers/parseBackupLogs.ts b/kotsadm/api/src/snapshots/resolvers/parseBackupLogs.ts
index 92c09be8f..09686e0eb 100644
--- a/kotsadm/api/src/snapshots/resolvers/parseBackupLogs.ts
+++ b/kotsadm/api/src/snapshots/resolvers/parseBackupLogs.ts
@@ -26,7 +26,7 @@ export interface BackupLogLine {
 };
 
 function execKey(line: any): string {
-  return `${line.namespace}/${line.name}/${line.hookPhase}/${line.hookSource}/${line.hookType}`;
+  return `${line.namespace}/${line.name}/${line.hookContainer}/${line.hookName}/${line.hookPhase}/${line.hookCommand}`;
 }
 
 function isExecBegin(line: any): boolean {
@@ -50,7 +50,7 @@ function isWarning(line: any): boolean {
 }
 
 function isExec(line: any): boolean {
-  return line.hookName ? false : true;
+  return !!line.hookName;
 }
 
 /*
@@ -64,7 +64,7 @@ export function parseBackupLogs(buffer: Buffer): ParsedBackupLogs {
     // https://github.com/csquared/node-logfmt/blob/e279d43cde019acfcca0abe99ea6b7ce8327e1f7/lib/logfmt_parser.js#L46
     return parse(s.replace("\\n", "\n")) as BackupLogLine;
   });
-  const errors: Array<SnapshotError> = [];
+  const errors: Array<SnapshotError>  = [];
   const warnings: Array<SnapshotError> = [];
   const execs: Array<SnapshotHook> = [];
   const openExecs: any = {};
@@ -79,7 +79,6 @@ export function parseBackupLogs(buffer: Buffer): ParsedBackupLogs {
         delete openExecs[key];
       }
 
-
       const exec = {
         namespace: line.namespace,
         podName: line.name,
@@ -87,9 +86,10 @@ export function parseBackupLogs(buffer: Buffer): ParsedBackupLogs {
         command: line.hookCommand,
         hookName: line.hookName,
         phase: line.hookPhase,
-        started: line.time
+        started: line.time,
       };
       openExecs[key] = exec;
+
       return;
     }
 
@@ -121,7 +121,8 @@ export function parseBackupLogs(buffer: Buffer): ParsedBackupLogs {
         console.log("Dropping exec error from backup logs");
         return;
       }
-      open.error = [{ title: line.msg, message: line.error! }];
+      open.errors.push({ title: line.msg, message: line.error });
+      return;
     }
 
     if (isWarning(line) && isExec(line)) {
@@ -130,11 +131,13 @@ export function parseBackupLogs(buffer: Buffer): ParsedBackupLogs {
         console.log("Dropping exec warning from backup logs");
         return;
       }
-      open.warning = [{ title: line.msg, message: line.warning! }];
+      open.warnings.push({ title: line.msg, message: line.error });
+      return;
     }
 
     if (isError(line)) {
       errors.push({ title: line.msg, message: line.error! });
+      return;
     }
 
     if (isWarning(line)) {
diff --git a/kotsadm/api/src/snapshots/snapshot.ts b/kotsadm/api/src/snapshots/snapshot.ts
index 9b2214fc1..6185ebd47 100644
--- a/kotsadm/api/src/snapshots/snapshot.ts
+++ b/kotsadm/api/src/snapshots/snapshot.ts
@@ -72,8 +72,8 @@ export interface SnapshotHook {
   stderr: string;
   started: string;
   finished: string;
-  error: SnapshotError[],
-  warning: SnapshotError[],
+  errors: SnapshotError[],
+  warnings: SnapshotError[],
 }
 
 export interface RestoreDetail {
diff --git a/kotsadm/api/src/troubleshoot/troubleshoot_store.ts b/kotsadm/api/src/troubleshoot/troubleshoot_store.ts
index 1cb71890c..842fb3872 100644
--- a/kotsadm/api/src/troubleshoot/troubleshoot_store.ts
+++ b/kotsadm/api/src/troubleshoot/troubleshoot_store.ts
@@ -29,9 +29,12 @@ export class TroubleshootStore {
         supportbundle_analysis.max_severity AS analysis_max_severity,
         supportbundle_analysis.insights AS analysis_insights,
         supportbundle_analysis.created_at AS analysis_created_at,
+        watch.slug as watch_slug,
+        watch.title as watch_title,
         app.name as kots_app_title,
         app_version.kots_license as kots_license
       from supportbundle
+        left join watch on supportbundle.watch_id = watch.id
         left join app_downstream on supportbundle.watch_id = app_downstream.app_id
         left join app on supportbundle.watch_id = app.id
         left join app_version on supportbundle.watch_id = app_version.app_id
@@ -49,6 +52,7 @@ export class TroubleshootStore {
 
     supportBundle.id = row.id;
     supportBundle.slug = row.slug;
+    supportBundle.watchId = row.watch_id;
     supportBundle.name = row.name;
     supportBundle.size = row.size;
     supportBundle.status = row.status;
@@ -97,6 +101,9 @@ export class TroubleshootStore {
       supportBundle.analysis = analysis;
     }
 
+    supportBundle.watchSlug = row.watch_slug;
+    supportBundle.watchName = row.kots_app_title;
+
     return supportBundle;
   }
 
diff --git a/kotsadm/api/src/user/index.ts b/kotsadm/api/src/user/index.ts
index 789a46db4..02d588b05 100644
--- a/kotsadm/api/src/user/index.ts
+++ b/kotsadm/api/src/user/index.ts
@@ -2,5 +2,6 @@ export * from "./user";
 export * from "./scm_lead";
 export * from "./github_nonce";
 export * from "./user_store";
+export * from "./resolvers/user_queries";
 export * from "./resolvers/user_mutations";
 export * from "./graphql/user_types";
\ No newline at end of file
diff --git a/kotsadm/api/src/user/resolvers/user_queries.ts b/kotsadm/api/src/user/resolvers/user_queries.ts
new file mode 100644
index 000000000..437e63e61
--- /dev/null
+++ b/kotsadm/api/src/user/resolvers/user_queries.ts
@@ -0,0 +1,24 @@
+import { Stores } from "../../schema/stores";
+import { Context } from "../../context";
+import { ReplicatedError } from "../../server/errors";
+import { kotsTestRegistryCredentials } from "../../kots_app/kots_ffi";
+
+export function UserQueries(stores: Stores) {
+  return {
+    async validateRegistryInfo(root: any, {slug, endpoint, username, password, org}: any, context: Context): Promise<String> {
+      if (password === stores.kotsAppStore.getPasswordMask()) {
+        const appId = await stores.kotsAppStore.getIdFromSlug(slug);
+        const details = await stores.kotsAppStore.getAppRegistryDetails(appId);
+        password = details.registryPassword;
+      }
+      try {
+        const errorText = await kotsTestRegistryCredentials(endpoint, username, password, org);
+        return errorText;
+      } catch (err) {
+        throw new ReplicatedError(`Error: ${err}`);
+      }
+    },
+  }
+}
+
+
diff --git a/kotsadm/build-ttl-ci.sh b/kotsadm/build-ttl-ci.sh
index ddc992068..8d0c77531 100755
--- a/kotsadm/build-ttl-ci.sh
+++ b/kotsadm/build-ttl-ci.sh
@@ -18,7 +18,7 @@ mkdir -p -m 755 $PWD/migrations/fixtures/schema
 docker run \
   -v $PWD/migrations/fixtures:/out \
   -v $PWD/migrations/tables:/in \
-  schemahero/schemahero:0.9.0 \
+  schemahero/schemahero:0.7.2 \
   fixtures --input-dir /in --output-dir /out/schema --dbname ship-cloud --driver postgres
 
 make -C migrations/fixtures deps build run build-ttl-ci.sh
diff --git a/kotsadm/migrations/Dockerfile.skaffold b/kotsadm/migrations/Dockerfile.skaffold
index d1b992922..3ff28cb3a 100644
--- a/kotsadm/migrations/Dockerfile.skaffold
+++ b/kotsadm/migrations/Dockerfile.skaffold
@@ -1,3 +1,15 @@
-FROM schemahero/schemahero:0.9.0
+FROM node:10
+ADD . /src
+WORKDIR /src/fixtures
+RUN rm -rf node_modules
+RUN make deps build run
 
-ADD --chown=schemahero:schemahero ./tables ./tables
+FROM postgres:10.7
+
+COPY --from=0 /src/fixtures/fixtures.sql /docker-entrypoint-initdb.d/fixtures.sql
+
+RUN useradd -c 'kotsadm-migrations user' -m -d /home/kotsadm-migrations -s /bin/bash -u 1001 kotsadm-migrations
+RUN chown -R kotsadm-migrations.kotsadm-migrations /src/fixtures/fixtures.sql
+RUN chown -R kotsadm-migrations.kotsadm-migrations /docker-entrypoint-initdb.d/fixtures.sql
+USER kotsadm-migrations
+ENV HOME /home/kotsadm-migrations
diff --git a/kotsadm/migrations/Makefile b/kotsadm/migrations/Makefile
index 3299a6fe3..ce9eaf22b 100644
--- a/kotsadm/migrations/Makefile
+++ b/kotsadm/migrations/Makefile
@@ -8,16 +8,12 @@ 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
 build-ttl-ci.sh: build_schema
 
 build_schema:
-	docker pull schemahero/schemahero:0.9.0
+	docker pull schemahero/schemahero:0.7.2
 	docker build -f deploy/Dockerfile -t ${IMAGE} .
 	docker push ${IMAGE}
diff --git a/kotsadm/migrations/deploy/Dockerfile b/kotsadm/migrations/deploy/Dockerfile
index 916cb140a..5df9c801d 100644
--- a/kotsadm/migrations/deploy/Dockerfile
+++ b/kotsadm/migrations/deploy/Dockerfile
@@ -1,4 +1,4 @@
-FROM schemahero/schemahero:0.9.0
+FROM schemahero/schemahero:0.7.2
 
 ADD --chown=schemahero:schemahero ./tables ./tables
 
diff --git a/kotsadm/migrations/fixtures/Makefile b/kotsadm/migrations/fixtures/Makefile
index 2daea14b6..9f53e6913 100644
--- a/kotsadm/migrations/fixtures/Makefile
+++ b/kotsadm/migrations/fixtures/Makefile
@@ -29,8 +29,8 @@ build-ttl-ci.sh:
 
 .PHONY: schema-fixtures
 schema-fixtures:
-	docker pull schemahero/schemahero:0.9.0
-	docker run --user root -e uid=1 -v `pwd`:/out -v `pwd`/../tables:/in schemahero/schemahero:0.9.0 fixtures --input-dir /in --output-dir /out/schema --dbname -ship-cloud --driver postgres
+	docker pull schemahero/schemahero:0.7.2
+	docker run --user root -e uid=1 -v `pwd`:/out -v `pwd`/../tables:/in schemahero/schemahero:0.7.2 fixtures --input-dir /in --output-dir /out/schema --dbname -ship-cloud --driver postgres
 
 .PHONY: publish
 publish: IMAGE = kotsadm/${PROJECT_NAME}:latest
diff --git a/kotsadm/migrations/kustomize/base/kustomization.yaml b/kotsadm/migrations/kustomize/base/kustomization.yaml
index 1b4321416..985130fc1 100644
--- a/kotsadm/migrations/kustomize/base/kustomization.yaml
+++ b/kotsadm/migrations/kustomize/base/kustomization.yaml
@@ -1,1 +1,3 @@
-bases: []
\ No newline at end of file
+bases:
+  - ../../tables
+
diff --git a/kotsadm/migrations/kustomize/base/schemahero.yaml b/kotsadm/migrations/kustomize/base/schemahero.yaml
new file mode 100644
index 000000000..df16b9303
--- /dev/null
+++ b/kotsadm/migrations/kustomize/base/schemahero.yaml
@@ -0,0 +1,802 @@
+---
+apiVersion: apiextensions.k8s.io/v1beta1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.2.4
+  creationTimestamp: null
+  name: databases.databases.schemahero.io
+spec:
+  group: databases.schemahero.io
+  names:
+    kind: Database
+    listKind: DatabaseList
+    plural: databases
+    singular: database
+  scope: Namespaced
+  subresources:
+    status: {}
+  validation:
+    openAPIV3Schema:
+      description: Database is the Schema for the databases API
+      properties:
+        apiVersion:
+          description:
+            "APIVersion defines the versioned schema of this representation
+            of an object. Servers should convert recognized schemas to the latest
+            internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources"
+          type: string
+        connection:
+          description:
+            DatabaseConnection defines connection parameters for the database
+            driver
+          properties:
+            mysql:
+              properties:
+                uri:
+                  properties:
+                    value:
+                      type: string
+                    valueFrom:
+                      properties:
+                        secretKeyRef:
+                          properties:
+                            key:
+                              type: string
+                            name:
+                              type: string
+                          required:
+                            - key
+                            - name
+                          type: object
+                      type: object
+                  type: object
+              type: object
+            postgres:
+              properties:
+                uri:
+                  properties:
+                    value:
+                      type: string
+                    valueFrom:
+                      properties:
+                        secretKeyRef:
+                          properties:
+                            key:
+                              type: string
+                            name:
+                              type: string
+                          required:
+                            - key
+                            - name
+                          type: object
+                      type: object
+                  type: object
+              type: object
+          type: object
+        gitops:
+          properties:
+            branch:
+              type: string
+            isPlanEnabled:
+              type: boolean
+            paths:
+              items:
+                type: string
+              type: array
+            pollInterval:
+              type: string
+            url:
+              type: string
+          required:
+            - isPlanEnabled
+            - url
+          type: object
+        kind:
+          description:
+            "Kind is a string value representing the REST resource this
+            object represents. Servers may infer this from the endpoint the client
+            submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds"
+          type: string
+        metadata:
+          type: object
+        schemahero:
+          properties:
+            image:
+              type: string
+            nodeSelector:
+              additionalProperties:
+                type: string
+              type: object
+          type: object
+        status:
+          description: DatabaseStatus defines the observed state of Database
+          properties:
+            gitRepoStatus:
+              type: string
+            gitopsPlanPulls:
+              additionalProperties:
+                type: string
+              type: object
+            gitopsPlanStatus:
+              type: string
+            isConnected:
+              type: boolean
+            lastPing:
+              type: string
+          required:
+            - isConnected
+            - lastPing
+          type: object
+      type: object
+  version: v1alpha2
+  versions:
+    - name: v1alpha2
+      served: true
+      storage: true
+status:
+  acceptedNames:
+    kind: ""
+    plural: ""
+  conditions: []
+  storedVersions: []
+---
+
+---
+apiVersion: apiextensions.k8s.io/v1beta1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.2.4
+  creationTimestamp: null
+  name: migrations.schemas.schemahero.io
+spec:
+  group: schemas.schemahero.io
+  names:
+    kind: Migration
+    listKind: MigrationList
+    plural: migrations
+    singular: migration
+  scope: Namespaced
+  validation:
+    openAPIV3Schema:
+      description: Migration is the Schema for the migrations API
+      properties:
+        apiVersion:
+          description:
+            "APIVersion defines the versioned schema of this representation
+            of an object. Servers should convert recognized schemas to the latest
+            internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources"
+          type: string
+        kind:
+          description:
+            "Kind is a string value representing the REST resource this
+            object represents. Servers may infer this from the endpoint the client
+            submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds"
+          type: string
+        metadata:
+          type: object
+        spec:
+          description: MigrationSpec defines the desired state of Migration
+          type: object
+        status:
+          description: MigrationStatus defines the observed state of Migration
+          type: object
+      type: object
+  version: v1alpha2
+  versions:
+    - name: v1alpha2
+      served: true
+      storage: true
+status:
+  acceptedNames:
+    kind: ""
+    plural: ""
+  conditions: []
+  storedVersions: []
+---
+apiVersion: apiextensions.k8s.io/v1beta1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.2.4
+  creationTimestamp: null
+  name: tables.schemas.schemahero.io
+spec:
+  group: schemas.schemahero.io
+  names:
+    kind: Table
+    listKind: TableList
+    plural: tables
+    singular: table
+  scope: Namespaced
+  validation:
+    openAPIV3Schema:
+      description: Table is the Schema for the tables API
+      properties:
+        apiVersion:
+          description:
+            "APIVersion defines the versioned schema of this representation
+            of an object. Servers should convert recognized schemas to the latest
+            internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources"
+          type: string
+        kind:
+          description:
+            "Kind is a string value representing the REST resource this
+            object represents. Servers may infer this from the endpoint the client
+            submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds"
+          type: string
+        metadata:
+          type: object
+        spec:
+          description: TableSpec defines the desired state of Table
+          properties:
+            database:
+              type: string
+            isPlan:
+              type: boolean
+            name:
+              type: string
+            requires:
+              items:
+                type: string
+              type: array
+            schema:
+              properties:
+                mysql:
+                  properties:
+                    columns:
+                      items:
+                        properties:
+                          constraints:
+                            properties:
+                              notNull:
+                                type: boolean
+                            type: object
+                          default:
+                            type: string
+                          name:
+                            type: string
+                          type:
+                            type: string
+                        required:
+                          - name
+                          - type
+                        type: object
+                      type: array
+                    foreignKeys:
+                      items:
+                        properties:
+                          columns:
+                            items:
+                              type: string
+                            type: array
+                          name:
+                            type: string
+                          onDelete:
+                            type: string
+                          references:
+                            properties:
+                              columns:
+                                items:
+                                  type: string
+                                type: array
+                              table:
+                                type: string
+                            required:
+                              - columns
+                              - table
+                            type: object
+                        required:
+                          - columns
+                          - onDelete
+                          - references
+                        type: object
+                      type: array
+                    indexes:
+                      items:
+                        properties:
+                          columns:
+                            items:
+                              type: string
+                            type: array
+                          isUnique:
+                            type: boolean
+                          name:
+                            type: string
+                          type:
+                            type: string
+                        required:
+                          - columns
+                        type: object
+                      type: array
+                    isDeleted:
+                      type: boolean
+                    primaryKey:
+                      items:
+                        type: string
+                      type: array
+                  required:
+                    - primaryKey
+                  type: object
+                postgres:
+                  properties:
+                    columns:
+                      items:
+                        properties:
+                          constraints:
+                            properties:
+                              notNull:
+                                type: boolean
+                            type: object
+                          default:
+                            type: string
+                          name:
+                            type: string
+                          type:
+                            type: string
+                        required:
+                          - name
+                          - type
+                        type: object
+                      type: array
+                    foreignKeys:
+                      items:
+                        properties:
+                          columns:
+                            items:
+                              type: string
+                            type: array
+                          name:
+                            type: string
+                          onDelete:
+                            type: string
+                          references:
+                            properties:
+                              columns:
+                                items:
+                                  type: string
+                                type: array
+                              table:
+                                type: string
+                            required:
+                              - columns
+                              - table
+                            type: object
+                        required:
+                          - columns
+                          - onDelete
+                          - references
+                        type: object
+                      type: array
+                    indexes:
+                      items:
+                        properties:
+                          columns:
+                            items:
+                              type: string
+                            type: array
+                          isUnique:
+                            type: boolean
+                          name:
+                            type: string
+                          type:
+                            type: string
+                        required:
+                          - columns
+                        type: object
+                      type: array
+                    isDeleted:
+                      type: boolean
+                    primaryKey:
+                      items:
+                        type: string
+                      type: array
+                  required:
+                    - primaryKey
+                  type: object
+              type: object
+          required:
+            - database
+            - name
+            - schema
+          type: object
+        status:
+          description: TableStatus defines the observed state of Table
+          properties:
+            plan:
+              type: string
+          type: object
+      type: object
+  version: v1alpha2
+  versions:
+    - name: v1alpha2
+      served: true
+      storage: true
+status:
+  acceptedNames:
+    kind: ""
+    plural: ""
+  conditions: []
+  storedVersions: []
+---
+apiVersion: v1
+kind: Namespace
+metadata:
+  labels:
+    control-plane: controller-manager
+    controller-tools.k8s.io: "1.0"
+  name: schemahero-system
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+  creationTimestamp: null
+  name: schemahero-manager-role
+rules:
+  - apiGroups:
+      - apps
+    resources:
+      - deployments
+    verbs:
+      - get
+      - list
+      - watch
+      - create
+      - update
+      - patch
+      - delete
+  - apiGroups:
+      - apps
+    resources:
+      - deployments/status
+    verbs:
+      - get
+      - update
+      - patch
+  - apiGroups:
+      - databases.schemahero.io
+    resources:
+      - databases
+    verbs:
+      - get
+      - list
+      - watch
+      - create
+      - update
+      - patch
+      - delete
+  - apiGroups:
+      - databases.schemahero.io
+    resources:
+      - databases/status
+    verbs:
+      - get
+      - update
+      - patch
+  - apiGroups:
+      - apps
+    resources:
+      - deployments
+    verbs:
+      - get
+      - list
+      - watch
+      - create
+      - update
+      - patch
+      - delete
+  - apiGroups:
+      - apps
+    resources:
+      - deployments/status
+    verbs:
+      - get
+      - update
+      - patch
+  - apiGroups:
+      - schemas.schemahero.io
+    resources:
+      - migrations
+    verbs:
+      - get
+      - list
+      - watch
+      - create
+      - update
+      - patch
+      - delete
+  - apiGroups:
+      - schemas.schemahero.io
+    resources:
+      - migrations/status
+    verbs:
+      - get
+      - update
+      - patch
+  - apiGroups:
+      - apps
+    resources:
+      - deployments
+    verbs:
+      - get
+      - list
+      - watch
+      - create
+      - update
+      - patch
+      - delete
+  - apiGroups:
+      - apps
+    resources:
+      - deployments/status
+    verbs:
+      - get
+      - update
+      - patch
+  - apiGroups:
+      - schemas.schemahero.io
+    resources:
+      - tables
+    verbs:
+      - get
+      - list
+      - watch
+      - create
+      - update
+      - patch
+      - delete
+  - apiGroups:
+      - schemas.schemahero.io
+    resources:
+      - tables/status
+    verbs:
+      - get
+      - update
+      - patch
+  - apiGroups:
+      - admissionregistration.k8s.io
+    resources:
+      - mutatingwebhookconfigurations
+      - validatingwebhookconfigurations
+    verbs:
+      - get
+      - list
+      - watch
+      - create
+      - update
+      - patch
+      - delete
+  - apiGroups:
+      - ""
+    resources:
+      - secrets
+    verbs:
+      - get
+      - list
+      - watch
+      - create
+      - update
+      - patch
+      - delete
+  - apiGroups:
+      - ""
+    resources:
+      - services
+    verbs:
+      - get
+      - list
+      - watch
+      - create
+      - update
+      - patch
+      - delete
+  - apiGroups:
+      - ""
+    resources:
+      - pods
+    verbs:
+      - get
+      - list
+      - watch
+      - create
+      - update
+      - patch
+      - delete
+  - apiGroups:
+      - ""
+    resources:
+      - configmaps
+    verbs:
+      - get
+      - list
+      - watch
+      - create
+      - update
+      - patch
+      - delete
+  - apiGroups:
+      - "rbac.authorization.k8s.io"
+    resources:
+      - roles
+    verbs:
+      - get
+      - list
+      - watch
+      - create
+      - update
+      - patch
+      - delete
+  - apiGroups:
+      - "rbac.authorization.k8s.io"
+    resources:
+      - rolebindings
+    verbs:
+      - get
+      - list
+      - watch
+      - create
+      - update
+      - patch
+      - delete
+  - apiGroups:
+      - ""
+    resources:
+      - serviceaccounts
+    verbs:
+      - get
+      - list
+      - watch
+      - create
+      - update
+      - patch
+      - delete
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+  name: schemahero-proxy-role
+rules:
+  - apiGroups:
+      - authentication.k8s.io
+    resources:
+      - tokenreviews
+    verbs:
+      - create
+  - apiGroups:
+      - authorization.k8s.io
+    resources:
+      - subjectaccessreviews
+    verbs:
+      - create
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+  creationTimestamp: null
+  name: schemahero-manager-rolebinding
+roleRef:
+  apiGroup: rbac.authorization.k8s.io
+  kind: ClusterRole
+  name: schemahero-manager-role
+subjects:
+  - kind: ServiceAccount
+    name: default
+    namespace: schemahero-system
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+  name: schemahero-proxy-rolebinding
+roleRef:
+  apiGroup: rbac.authorization.k8s.io
+  kind: ClusterRole
+  name: schemahero-proxy-role
+subjects:
+  - kind: ServiceAccount
+    name: default
+    namespace: schemahero-system
+---
+apiVersion: v1
+kind: Secret
+metadata:
+  name: schemahero-webhook-server-secret
+  namespace: schemahero-system
+---
+apiVersion: v1
+kind: Service
+metadata:
+  annotations:
+    prometheus.io/port: "8443"
+    prometheus.io/scheme: https
+    prometheus.io/scrape: "true"
+  labels:
+    control-plane: controller-manager
+    controller-tools.k8s.io: "1.0"
+  name: schemahero-controller-manager-metrics-service
+  namespace: schemahero-system
+spec:
+  ports:
+    - name: https
+      port: 8443
+      targetPort: https
+  selector:
+    control-plane: controller-manager
+    controller-tools.k8s.io: "1.0"
+---
+apiVersion: v1
+kind: Service
+metadata:
+  labels:
+    control-plane: controller-manager
+    controller-tools.k8s.io: "1.0"
+  name: schemahero-controller-manager-service
+  namespace: schemahero-system
+spec:
+  ports:
+    - port: 443
+  selector:
+    control-plane: controller-manager
+    controller-tools.k8s.io: "1.0"
+---
+apiVersion: apps/v1
+kind: StatefulSet
+metadata:
+  labels:
+    control-plane: controller-manager
+    controller-tools.k8s.io: "1.0"
+  name: schemahero-controller-manager
+  namespace: schemahero-system
+spec:
+  selector:
+    matchLabels:
+      control-plane: controller-manager
+      controller-tools.k8s.io: "1.0"
+  serviceName: schemahero-controller-manager-service
+  volumeClaimTemplates: []
+  template:
+    metadata:
+      labels:
+        control-plane: controller-manager
+        controller-tools.k8s.io: "1.0"
+    spec:
+      securityContext:
+        runAsUser: 1001
+        fsGroup: 1001
+      containers:
+        - args:
+            - --secure-listen-address=0.0.0.0:8443
+            - --upstream=http://127.0.0.1:8088/
+            - --logtostderr=true
+            - --v=10
+          image: gcr.io/kubebuilder/kube-rbac-proxy:v0.4.0
+          name: kube-rbac-proxy
+          ports:
+            - containerPort: 8443
+              name: https
+        - args:
+            - --metrics-addr=127.0.0.1:8088
+          command:
+            - /manager
+          env:
+            - name: POD_NAMESPACE
+              valueFrom:
+                fieldRef:
+                  fieldPath: metadata.namespace
+            - name: SECRET_NAME
+              value: schemahero-webhook-server-secret
+          image: schemahero/schemahero-manager:0.7.2
+          imagePullPolicy: Always
+          name: manager
+          ports:
+            - containerPort: 9876
+              name: webhook-server
+              protocol: TCP
+          resources:
+            limits:
+              cpu: 100m
+              memory: 200Mi
+            requests:
+              cpu: 100m
+              memory: 100Mi
+      terminationGracePeriodSeconds: 10
diff --git a/kotsadm/migrations/kustomize/overlays/dev/job.yaml b/kotsadm/migrations/kustomize/overlays/dev/job.yaml
deleted file mode 100644
index 0f4df1d72..000000000
--- a/kotsadm/migrations/kustomize/overlays/dev/job.yaml
+++ /dev/null
@@ -1,49 +0,0 @@
-apiVersion: batch/v1
-kind: Job
-metadata:
-  name: kotsadm-migrations
-spec:
-  template:
-    spec:
-      volumes:
-        - name: migrations
-          emptyDir:
-            medium: Memory      
-      initContainers:
-      - env:
-        - name: SCHEMAHERO_DRIVER
-          value: postgres
-        - name: SCHEMAHERO_SPEC_FILE
-          value: /tables
-        - name: SCHEMAHERO_URI
-          valueFrom:
-            secretKeyRef:
-              key: uri
-              name: ship-postgres
-        - name: SCHEMAHERO_OUT
-          value: /migrations/plan.yaml              
-        image: kotsadm-migrations
-        name: kotsadm-migrations
-        args: ["plan"]
-        name: migrations-plan
-        volumeMounts:
-          - name: migrations
-            mountPath: /migrations        
-      containers:
-      - env:
-        - name: SCHEMAHERO_DRIVER
-          value: postgres
-        - name: SCHEMAHERO_DDL
-          value: /migrations/plan.yaml
-        - name: SCHEMAHERO_URI
-          valueFrom:
-            secretKeyRef:
-              key: uri
-              name: ship-postgres
-        image: kotsadm-migrations
-        name: kotsadm-migrations
-        args: ["apply"]
-        volumeMounts:
-          - name: migrations
-            mountPath: /migrations
-      restartPolicy: OnFailure
diff --git a/kotsadm/migrations/kustomize/overlays/dev/kotsadm-postgres.yaml b/kotsadm/migrations/kustomize/overlays/dev/kotsadm-postgres.yaml
new file mode 100644
index 000000000..09ebd9f57
--- /dev/null
+++ b/kotsadm/migrations/kustomize/overlays/dev/kotsadm-postgres.yaml
@@ -0,0 +1,10 @@
+apiVersion: databases.schemahero.io/v1alpha2
+kind: Database
+metadata:
+  name: kotsadm-postgres
+schemahero:
+  image: schemahero/schemahero:0.7.2
+connection:
+  postgres:
+    uri:
+      value: postgres://shipcloud:password@postgres.default.svc.cluster.local:5432/shipcloud?sslmode=disable
diff --git a/kotsadm/migrations/kustomize/overlays/dev/kustomization.yaml b/kotsadm/migrations/kustomize/overlays/dev/kustomization.yaml
index 2138fe1ce..5c14654ef 100644
--- a/kotsadm/migrations/kustomize/overlays/dev/kustomization.yaml
+++ b/kotsadm/migrations/kustomize/overlays/dev/kustomization.yaml
@@ -3,4 +3,4 @@ bases:
 
 resources:
   - ./postgres.yaml
-  - ./job.yaml
\ No newline at end of file
+  - ./kotsadm-postgres.yaml
diff --git a/kotsadm/migrations/kustomize/overlays/production/database.yaml b/kotsadm/migrations/kustomize/overlays/production/database.yaml
new file mode 100644
index 000000000..196ec9fdc
--- /dev/null
+++ b/kotsadm/migrations/kustomize/overlays/production/database.yaml
@@ -0,0 +1,17 @@
+apiVersion: databases.schemahero.io/v1alpha2
+kind: Database
+metadata:
+  labels:
+    controller-tools.k8s.io: "1.0"
+  name: kotsadm-postgres
+schemahero:
+  image: schemahero/schemahero:0.7.2
+  nodeSelector:
+    replicated/node-pool: privileged
+connection:
+  postgres:
+    uri:
+      valueFrom:
+        secretKeyRef:
+          name: kotsadm-postgres
+          key: uri
diff --git a/kotsadm/migrations/kustomize/overlays/production/kotsadm-postgres.yaml b/kotsadm/migrations/kustomize/overlays/production/kotsadm-postgres.yaml
new file mode 100644
index 000000000..074a94089
--- /dev/null
+++ b/kotsadm/migrations/kustomize/overlays/production/kotsadm-postgres.yaml
@@ -0,0 +1,10 @@
+apiVersion: bitnami.com/v1alpha1
+kind: SealedSecret
+metadata:
+  creationTimestamp: null
+  name: kotsadm-postgres
+  namespace: default
+spec:
+  encryptedData:
+    uri: AgA499iTkCkE1n5/AffvSVxXwXZ25kr1D0TbGhnxrk+nVpYMbrSFJNq7G1XKImBDTphdYH9Ge5xXvQ9dK5nv4F5UDEMeO60yN28bqBuxPVW9UENc+KS67h/d8NFcWAgtVjc2AQ/2+1ZaJiuqOt8YpdIezGhiqzKoH7PDsNBITSIVqgg6IttXVrgyj2lwOYC+HWILTa/wyokL0TvVWoIGj2QDXyP2uqqUbMg/Xgpd/TwcsvnXg7f2n79eV5X33taMs7GBd9kUrM+2JCLbUa67uKkfhvsUFcBZ1P3xBVQbzbx70poO4wK0fzMpkkyJMu/hQzT33zHINqyMVqz0cka05h6VsoFMq/PJAXk0oNmpWvMuISd5BU21DCTt5h/kcn1aV7Y25lEGq60Q/72+gz61CFM3ZU8EbSd374bSgvk7kMAsAR2yv5/qWNWBy0WaAuYMwz9vRV7MxWsYK0hK2on7e+n4/1kk6rYchAZbYa519Iquhb46Yp3m9QlDTY+2uVQevEleSv3+H7i4kl5ri3Llul2ExsVMJTeTKzdEBsr2+BUJ1GIYn6yITMxmuI6BN4LARF/xro78pWI26AptG8FvKpu+r9zDCLMbE5chk/aXkMeorFJDOSQTr5bZIpssqyXc8P16Sji7ruQQngJ+HNFGkISaPnmubdUOPdaKRgi2M8wid92yobGz0DZTjhULf2vHpB/ckIJySzBdp8avHX4JZChqdi/xbi2qX40zx+zqT/salEh2wOaa1Hq/z8kaWlacjFfwHql0bWXg4u5fJ9rcMHtwnH0V/xER3ny5QnKf2GavcDUxZ3JslZoWzx7bILIWkyDmLIRtcHxDijDq9q6kpCYpoEw6kEWBSvxM0VhPpiECYJ40Nwd/NUwMKiqBbOqjZ1wztwKgZrVPSnPublwrt/ohYwt4
+
diff --git a/kotsadm/migrations/kustomize/overlays/production/kustomization.yaml b/kotsadm/migrations/kustomize/overlays/production/kustomization.yaml
new file mode 100644
index 000000000..4b4a3b3d2
--- /dev/null
+++ b/kotsadm/migrations/kustomize/overlays/production/kustomization.yaml
@@ -0,0 +1,6 @@
+bases:
+  - ../../base
+
+resources:
+  - ./database.yaml
+  - ./kotsadm-postgres.yaml
diff --git a/kotsadm/migrations/kustomize/overlays/staging/database.yaml b/kotsadm/migrations/kustomize/overlays/staging/database.yaml
new file mode 100644
index 000000000..0ad1db60c
--- /dev/null
+++ b/kotsadm/migrations/kustomize/overlays/staging/database.yaml
@@ -0,0 +1,15 @@
+apiVersion: databases.schemahero.io/v1alpha2
+kind: Database
+metadata:
+  name: kotsadm-postgres
+schemahero:
+  image: schemahero/schemahero:0.7.2
+  nodeSelector:
+    replicated/node-pool: privileged
+connection:
+  postgres:
+    uri:
+      valueFrom:
+        secretKeyRef:
+          name: kotsadm-postgres
+          key: uri
diff --git a/kotsadm/migrations/kustomize/overlays/staging/kotsadm-postgres.yaml b/kotsadm/migrations/kustomize/overlays/staging/kotsadm-postgres.yaml
new file mode 100644
index 000000000..e8edf7ce1
--- /dev/null
+++ b/kotsadm/migrations/kustomize/overlays/staging/kotsadm-postgres.yaml
@@ -0,0 +1,10 @@
+apiVersion: bitnami.com/v1alpha1
+kind: SealedSecret
+metadata:
+  creationTimestamp: null
+  name: kotsadm-postgres
+  namespace: default
+spec:
+  encryptedData:
+    uri: AgB0h/LnQ69zx7t/c75RsEz+20dbE0ptGlYMl2jcDqpuSzz8gBYyJG+ZLi/Knef/r79w+m7bIDyZq3LAa5my8MyEA5WunZJDK24oWYvBASwik/1f/15h12zlDdgyCuoPe5pDlaVZvXyLsR97SdjhO6KFmJ58i3xVTJQcXw8TEAA5CeQ3pAfO+VOx+LrebwyPKdAZSYIfV20jX2wPnugjImEWr2BXRPYOtlnFwp9/pbCDEbQe61jHD9JMcNEEC9kyZz2faR7vq1oOfqvrGAnXEtUUe1Fg+eQ1d3yHVM/N2rtkg0nmh0y0spQO6nVuZ4sgQOMg7qyzO6qSStiNvnIY3cnk6QQvBinrlLP9Q6QGClOO54u1+Y7MRQtBVkztFkhBq1Xkq18RGbKPZdlRm1SIICMiOYMNHX5KgKJQjsz2IGRcD3/09XEY77esRjoKk9wMbVTUoflyYOrzaMBThOkLq2dHp74csaW4mJz4av5WzW//2V4t45PIeyVvIZcfDiucjkt8ONbHRwpqtvuJvDJtuMGSWFiXrJj8XCbk1SLgvfeg/Qxg8Duv4jmsjKY7mfLHs+vDI4BJOnE1vk9Zrz7h8zIM07YNAK3Q0mkWe4xHIn+i4JftvrQJO8psBuPRlM00fUTECBbB13I9OrVexE0nftsqRKU7hb3/PL5rTjO8wpS5pCUxgkV+0yMSfs89Dtpr9yYnOiPAeriB9+qmMG1RnsQN8Y9JdXKDbLl1IuVzflFLaPiYwBx/5aS6ZWQrCduYcf5WS+1xqCBiMScm1qXIGOAckbh9MDdaKC78gYvZMiIbtiIreExqL3BTMVtLzUdJDC+gK4B8Im9b6SrE9jxlSyjZpxQglLoAPXSN4xvJMPfVcsKxx4IRTpdS5Q/4hvzf2rcASWkf3VfFF/cp9LOQUwHnHKlD
+
diff --git a/kotsadm/migrations/kustomize/overlays/staging/kustomization.yaml b/kotsadm/migrations/kustomize/overlays/staging/kustomization.yaml
new file mode 100644
index 000000000..4b4a3b3d2
--- /dev/null
+++ b/kotsadm/migrations/kustomize/overlays/staging/kustomization.yaml
@@ -0,0 +1,6 @@
+bases:
+  - ../../base
+
+resources:
+  - ./database.yaml
+  - ./kotsadm-postgres.yaml
diff --git a/kotsadm/migrations/tables/api_task_status.yaml b/kotsadm/migrations/tables/api_task_status.yaml
index b48c4fd8e..7df223083 100644
--- a/kotsadm/migrations/tables/api_task_status.yaml
+++ b/kotsadm/migrations/tables/api_task_status.yaml
@@ -1,4 +1,4 @@
-apiVersion: schemas.schemahero.io/v1alpha4
+apiVersion: schemas.schemahero.io/v1alpha2
 kind: Table
 metadata:
   labels:
diff --git a/kotsadm/migrations/tables/app.yaml b/kotsadm/migrations/tables/app.yaml
index be45416e4..7e6601e03 100644
--- a/kotsadm/migrations/tables/app.yaml
+++ b/kotsadm/migrations/tables/app.yaml
@@ -1,4 +1,4 @@
-apiVersion: schemas.schemahero.io/v1alpha4
+apiVersion: schemas.schemahero.io/v1alpha2
 kind: Table
 metadata:
   name: app
diff --git a/kotsadm/migrations/tables/app_downstream.yaml b/kotsadm/migrations/tables/app_downstream.yaml
index c07da8435..4a0406a80 100644
--- a/kotsadm/migrations/tables/app_downstream.yaml
+++ b/kotsadm/migrations/tables/app_downstream.yaml
@@ -1,4 +1,4 @@
-apiVersion: schemas.schemahero.io/v1alpha4
+apiVersion: schemas.schemahero.io/v1alpha2
 kind: Table
 metadata:
   name: app-downstream
diff --git a/kotsadm/migrations/tables/app_downstream_output.yaml b/kotsadm/migrations/tables/app_downstream_output.yaml
index dfd8e0fd2..6e5ff1d6f 100644
--- a/kotsadm/migrations/tables/app_downstream_output.yaml
+++ b/kotsadm/migrations/tables/app_downstream_output.yaml
@@ -1,4 +1,4 @@
-apiVersion: schemas.schemahero.io/v1alpha4
+apiVersion: schemas.schemahero.io/v1alpha2
 kind: Table
 metadata:
   name: app-downstream-output
@@ -34,4 +34,4 @@ spec:
       - name: apply_stderr
         type: text
       - name: is_error
-        type: boolean
\ No newline at end of file
+        type: boolean
diff --git a/kotsadm/migrations/tables/app_downstream_version.yaml b/kotsadm/migrations/tables/app_downstream_version.yaml
index 8c46a0917..49b1801cc 100644
--- a/kotsadm/migrations/tables/app_downstream_version.yaml
+++ b/kotsadm/migrations/tables/app_downstream_version.yaml
@@ -1,4 +1,4 @@
-apiVersion: schemas.schemahero.io/v1alpha4
+apiVersion: schemas.schemahero.io/v1alpha2
 kind: Table
 metadata:
   name: app-downstream-version
diff --git a/kotsadm/migrations/tables/app_status.yaml b/kotsadm/migrations/tables/app_status.yaml
index 7e0356979..fecdc2813 100644
--- a/kotsadm/migrations/tables/app_status.yaml
+++ b/kotsadm/migrations/tables/app_status.yaml
@@ -1,4 +1,4 @@
-apiVersion: schemas.schemahero.io/v1alpha4
+apiVersion: schemas.schemahero.io/v1alpha2
 kind: Table
 metadata:
   name: app-status
diff --git a/kotsadm/migrations/tables/app_version.yaml b/kotsadm/migrations/tables/app_version.yaml
index e0e151f41..d6e7bfea8 100644
--- a/kotsadm/migrations/tables/app_version.yaml
+++ b/kotsadm/migrations/tables/app_version.yaml
@@ -1,4 +1,4 @@
-apiVersion: schemas.schemahero.io/v1alpha4
+apiVersion: schemas.schemahero.io/v1alpha2
 kind: Table
 metadata:
   name: app-version
diff --git a/kotsadm/migrations/tables/cluster.yaml b/kotsadm/migrations/tables/cluster.yaml
index 6ec1912f6..f24bef216 100644
--- a/kotsadm/migrations/tables/cluster.yaml
+++ b/kotsadm/migrations/tables/cluster.yaml
@@ -1,4 +1,4 @@
-apiVersion: schemas.schemahero.io/v1alpha4
+apiVersion: schemas.schemahero.io/v1alpha2
 kind: Table
 metadata:
   name: cluster
diff --git a/kotsadm/migrations/tables/cluster_github.yaml b/kotsadm/migrations/tables/cluster_github.yaml
new file mode 100644
index 000000000..7babfef53
--- /dev/null
+++ b/kotsadm/migrations/tables/cluster_github.yaml
@@ -0,0 +1,39 @@
+apiVersion: schemas.schemahero.io/v1alpha2
+kind: Table
+metadata:
+  labels:
+    controller-tools.k8s.io: "1.0"
+  name: cluster-github
+spec:
+  database: kotsadm-postgres
+  name: cluster_github
+  requires: []
+  schema:
+    postgres:
+      primaryKey:
+      - cluster_id
+      columns:
+      - name: cluster_id
+        type: text
+        constraints:
+          notNull: true
+      - name: owner
+        type: text
+        constraints:
+          notNull: true
+      - name: repo
+        type: text
+        constraints:
+          notNull: true
+      - name: branch
+        type: text
+      - name: installation_id
+        type: integer
+        constraints:
+          notNull: true
+      - name: is_deleted
+        type: boolean
+        default: "false"
+      - name: is_404
+        type: boolean
+        default: "false"
diff --git a/kotsadm/migrations/tables/email_notification.yaml b/kotsadm/migrations/tables/email_notification.yaml
new file mode 100644
index 000000000..9bc028c86
--- /dev/null
+++ b/kotsadm/migrations/tables/email_notification.yaml
@@ -0,0 +1,26 @@
+apiVersion: schemas.schemahero.io/v1alpha2
+kind: Table
+metadata:
+  labels:
+    controller-tools.k8s.io: "1.0"
+  name: email-notification
+spec:
+  database: kotsadm-postgres
+  name: email_notification
+  requires: []
+  schema:
+    postgres:
+      primaryKey: []
+      columns:
+      - name: notification_id
+        type: text
+        constraints:
+          notNull: true
+      - name: recipient
+        type: text
+        constraints:
+          notNull: true
+      - name: created_at
+        type: timestamp without time zone
+        constraints:
+          notNull: true
diff --git a/kotsadm/migrations/tables/feature.yaml b/kotsadm/migrations/tables/feature.yaml
new file mode 100644
index 000000000..5a0ece9f8
--- /dev/null
+++ b/kotsadm/migrations/tables/feature.yaml
@@ -0,0 +1,21 @@
+apiVersion: schemas.schemahero.io/v1alpha2
+kind: Table
+metadata:
+  labels:
+    controller-tools.k8s.io: "1.0"
+  name: feature
+spec:
+  database: kotsadm-postgres
+  name: feature
+  requires: []
+  schema:
+    postgres:
+      primaryKey:
+      - id
+      columns:
+      - name: id
+        type: text
+        constraints:
+          notNull: true
+      - name: created_at
+        type: timestamp without time zone
diff --git a/kotsadm/migrations/tables/github_install.yaml b/kotsadm/migrations/tables/github_install.yaml
new file mode 100644
index 000000000..3a1dc35cf
--- /dev/null
+++ b/kotsadm/migrations/tables/github_install.yaml
@@ -0,0 +1,36 @@
+apiVersion: schemas.schemahero.io/v1alpha2
+kind: Table
+metadata:
+  labels:
+    controller-tools.k8s.io: "1.0"
+  name: github-install
+spec:
+  database: kotsadm-postgres
+  name: github_install
+  requires: []
+  schema:
+    postgres:
+      primaryKey:
+      - id
+      columns:
+      - name: id
+        type: text
+        constraints:
+          notNull: true
+      - name: installation_id
+        type: integer
+      - name: account_login
+        type: text
+      - name: account_type
+        type: text
+      - name: account_html_url
+        type: text
+      - name: sender_login
+        type: text
+      - name: organization_members_total
+        type: integer
+      - name: created_at
+        type: timestamp without time zone
+      - name: is_deleted
+        type: boolean
+        default: "false"
diff --git a/kotsadm/migrations/tables/github_nonce.yaml b/kotsadm/migrations/tables/github_nonce.yaml
new file mode 100644
index 000000000..663938b0d
--- /dev/null
+++ b/kotsadm/migrations/tables/github_nonce.yaml
@@ -0,0 +1,22 @@
+apiVersion: schemas.schemahero.io/v1alpha2
+kind: Table
+metadata:
+  labels:
+    controller-tools.k8s.io: "1.0"
+  name: github-nonce
+spec:
+  database: kotsadm-postgres
+  name: github_nonce
+  requires: []
+  schema:
+    postgres:
+      primaryKey: []
+      columns:
+      - name: nonce
+        type: text
+        constraints:
+          notNull: true
+      - name: expire_at
+        type: timestamp without time zone
+        constraints:
+          notNull: true
diff --git a/kotsadm/migrations/tables/github_user.yaml b/kotsadm/migrations/tables/github_user.yaml
new file mode 100644
index 000000000..f740093fc
--- /dev/null
+++ b/kotsadm/migrations/tables/github_user.yaml
@@ -0,0 +1,33 @@
+apiVersion: schemas.schemahero.io/v1alpha2
+kind: Table
+metadata:
+  labels:
+    controller-tools.k8s.io: "1.0"
+  name: github-user
+spec:
+  database: kotsadm-postgres
+  name: github_user
+  requires: []
+  schema:
+    postgres:
+      primaryKey:
+      - username
+      columns:
+      - name: username
+        type: text
+        constraints:
+          notNull: true
+      - name: github_id
+        type: integer
+        constraints:
+          notNull: true
+      - name: avatar_url
+        type: character varying (255)
+        constraints:
+          notNull: false
+      - name: email
+        type: text
+      - name: user_id
+        type: text
+        # constraints:
+        #   notNull: true
diff --git a/kotsadm/migrations/tables/helm_chart.yaml b/kotsadm/migrations/tables/helm_chart.yaml
new file mode 100644
index 000000000..0d5fed7c4
--- /dev/null
+++ b/kotsadm/migrations/tables/helm_chart.yaml
@@ -0,0 +1,49 @@
+apiVersion: schemas.schemahero.io/v1alpha2
+kind: Table
+metadata:
+  name: helm-chart
+spec:
+  database: kotsadm-postgres
+  name: helm_chart
+  requires: []
+  schema:
+    postgres:
+      primaryKey:
+      - id
+      columns:
+      - name: id
+        type: text
+        constraints:
+          notNull: true
+      - name: cluster_id
+        type: text
+        constraints:
+          notNull: true
+      - name: helm_name
+        type: text
+        constraints:
+          notNull: true
+      - name: namespace
+        type: text
+        constraints:
+          notNull: true
+      - name: version
+        type: int
+      - name: first_deployed_at
+        type: timestamp without time zone
+        constraints:
+          notNull: true
+      - name: last_deployed_at
+        type: timestamp without time zone
+        constraints:
+          notNull: true
+      - name: is_deleted
+        type: boolean
+      - name: chart_version
+        type: text
+        constraints:
+          notNull: true
+      - name: app_version
+        type: text
+        constraints:
+          notNull: true
diff --git a/kotsadm/migrations/tables/helm_chart_source.yaml b/kotsadm/migrations/tables/helm_chart_source.yaml
new file mode 100644
index 000000000..bfdf02f6a
--- /dev/null
+++ b/kotsadm/migrations/tables/helm_chart_source.yaml
@@ -0,0 +1,23 @@
+apiVersion: schemas.schemahero.io/v1alpha2
+kind: Table
+metadata:
+  name: helm-chart-source
+spec:
+  database: kotsadm-postgres
+  name: helm_chart_source
+  requires: []
+  schema:
+    postgres:
+      primaryKey:
+      - helm_chart_id
+      - source
+      columns:
+      - name: helm_chart_id
+        type: text
+        constraints:
+          notNull: true
+      - name: source
+        type: text
+        constraints:
+          notNull: true
+
diff --git a/kotsadm/migrations/tables/image_watch.yaml b/kotsadm/migrations/tables/image_watch.yaml
new file mode 100644
index 000000000..9344ee527
--- /dev/null
+++ b/kotsadm/migrations/tables/image_watch.yaml
@@ -0,0 +1,49 @@
+apiVersion: schemas.schemahero.io/v1alpha2
+kind: Table
+metadata:
+  labels:
+    controller-tools.k8s.io: "1.0"
+  name: image-watch
+spec:
+  database: kotsadm-postgres
+  name: image_watch
+  requires: []
+  schema:
+    postgres:
+      primaryKey:
+      - id
+      columns:
+      - name: id
+        type: text
+        constraints:
+          notNull: true
+      - name: batch_id
+        type: text
+        constraints:
+          notNull: true
+      - name: image_name
+        type: text
+        constraints:
+          notNull: true
+      - name: checked_at
+        type: timestamp without time zone
+      - name: is_private
+        type: boolean
+        default: "false"
+      - name: versions_behind
+        type: integer
+        default: "0"
+      - name: detected_version
+        type: text
+      - name: latest_version
+        type: text
+      - name: compatible_version
+        type: text
+      - name: check_error
+        type: text
+      - name: docker_pullable
+        type: text
+      - name: path
+        type: text
+      - name: started_processing_at
+        type: timestamp without time zone
diff --git a/kotsadm/migrations/tables/image_watch_batch.yaml b/kotsadm/migrations/tables/image_watch_batch.yaml
new file mode 100644
index 000000000..6e658c5c8
--- /dev/null
+++ b/kotsadm/migrations/tables/image_watch_batch.yaml
@@ -0,0 +1,29 @@
+apiVersion: schemas.schemahero.io/v1alpha2
+kind: Table
+metadata:
+  labels:
+    controller-tools.k8s.io: "1.0"
+  name: image-watch-batch
+spec:
+  database: kotsadm-postgres
+  name: image_watch_batch
+  requires: []
+  schema:
+    postgres:
+      primaryKey:
+      - id
+      columns:
+      - name: id
+        type: text
+        constraints:
+          notNull: true
+      - name: user_id
+        type: text
+      - name: images_input
+        type: text
+        constraints:
+          notNull: true
+      - name: created_at
+        type: timestamp without time zone
+        constraints:
+          notNull: true
diff --git a/kotsadm/migrations/tables/kotsadm_params.yaml b/kotsadm/migrations/tables/kotsadm_params.yaml
index c38046d40..8531a4fa4 100644
--- a/kotsadm/migrations/tables/kotsadm_params.yaml
+++ b/kotsadm/migrations/tables/kotsadm_params.yaml
@@ -1,4 +1,4 @@
-apiVersion: schemas.schemahero.io/v1alpha4
+apiVersion: schemas.schemahero.io/v1alpha2
 kind: Table
 metadata:
   labels:
diff --git a/kotsadm/migrations/tables/kustomization.yaml b/kotsadm/migrations/tables/kustomization.yaml
new file mode 100644
index 000000000..4732fa1a7
--- /dev/null
+++ b/kotsadm/migrations/tables/kustomization.yaml
@@ -0,0 +1,55 @@
+resources:
+- ./api_task_status.yaml
+- ./session.yaml
+- ./ship_user.yaml
+- ./user_watch.yaml
+- ./webhook_notification.yaml
+- ./email_notification.yaml
+- ./watch.yaml
+- ./github_user.yaml
+- ./github_nonce.yaml
+- ./ship_notification.yaml
+- ./ship_init.yaml
+- ./ship_output_files.yaml
+- ./ship_update.yaml
+- ./pullrequest_notification.yaml
+- ./image_watch_batch.yaml
+- ./image_watch.yaml
+- ./pending_pullrequest_notification.yaml
+- ./pullrequest_history.yaml
+- ./user_feature.yaml
+- ./feature.yaml
+- ./watch_feature.yaml
+- ./ship_unfork.yaml
+- ./track_scm_leads.yaml
+- ./user_cluster.yaml
+- ./watch_downstream_token.yaml
+- ./cluster_github.yaml
+- ./watch_cluster.yaml
+- ./watch_version.yaml
+- ./object_store.yaml
+- ./ship_user_local.yaml
+- ./cluster.yaml
+- ./ship_edit.yaml
+- ./ship_init_pending.yaml
+- ./ship_init_pending_user.yaml
+- ./helm_chart_source.yaml
+- ./helm_chart.yaml
+- ./watch_troubleshoot_collector.yaml
+- ./watch_troubleshoot_analyzer.yaml
+- ./supportbundle.yaml
+- ./supportbundle_analysis.yaml
+- ./watch_license.yaml
+- ./github_install.yaml
+- ./preflight_result.yaml
+- ./preflight_spec.yaml
+- ./kotsadm_params.yaml
+- ./app.yaml
+- ./app_version.yaml
+- ./user_app.yaml
+- ./app_downstream.yaml
+- ./app_downstream_version.yaml
+- ./app_downstream_output.yaml
+- ./pending_support_bundle.yaml
+- ./app_status.yaml
+- ./scheduled_snapshots.yaml
diff --git a/kotsadm/migrations/tables/object_store.yaml b/kotsadm/migrations/tables/object_store.yaml
index f71f07ee8..f079eab6f 100644
--- a/kotsadm/migrations/tables/object_store.yaml
+++ b/kotsadm/migrations/tables/object_store.yaml
@@ -1,4 +1,4 @@
-apiVersion: schemas.schemahero.io/v1alpha4
+apiVersion: schemas.schemahero.io/v1alpha2
 kind: Table
 metadata:
   labels:
diff --git a/kotsadm/migrations/tables/pending_pullrequest_notification.yaml b/kotsadm/migrations/tables/pending_pullrequest_notification.yaml
new file mode 100644
index 000000000..4bbde8866
--- /dev/null
+++ b/kotsadm/migrations/tables/pending_pullrequest_notification.yaml
@@ -0,0 +1,45 @@
+apiVersion: schemas.schemahero.io/v1alpha2
+kind: Table
+metadata:
+  labels:
+    controller-tools.k8s.io: "1.0"
+  name: pending-pullrequest-notification
+spec:
+  database: kotsadm-postgres
+  name: pending_pullrequest_notification
+  requires: []
+  schema:
+    postgres:
+      primaryKey: []
+      columns:
+      - name: pullrequest_history_id
+        type: text
+        constraints:
+          notNull: true
+      - name: org
+        type: text
+        constraints:
+          notNull: true
+      - name: repo
+        type: text
+        constraints:
+          notNull: true
+      - name: branch
+        type: text
+        constraints:
+          notNull: true
+      - name: root_path
+        type: text
+      - name: created_at
+        type: timestamp without time zone
+        constraints:
+          notNull: true
+      - name: github_installation_id
+        type: integer
+        constraints:
+          notNull: true
+      - name: pullrequest_number
+        type: integer
+        default: '-1'
+      - name: watch_id
+        type: text
diff --git a/kotsadm/migrations/tables/pending_support_bundle.yaml b/kotsadm/migrations/tables/pending_support_bundle.yaml
index 837061e4c..7e2fe42f7 100644
--- a/kotsadm/migrations/tables/pending_support_bundle.yaml
+++ b/kotsadm/migrations/tables/pending_support_bundle.yaml
@@ -1,4 +1,4 @@
-apiVersion: schemas.schemahero.io/v1alpha4
+apiVersion: schemas.schemahero.io/v1alpha2
 kind: Table
 metadata:
   name: pending-supportbundle
diff --git a/kotsadm/migrations/tables/preflight_result.yaml b/kotsadm/migrations/tables/preflight_result.yaml
index 7c4062080..848152d3d 100644
--- a/kotsadm/migrations/tables/preflight_result.yaml
+++ b/kotsadm/migrations/tables/preflight_result.yaml
@@ -1,4 +1,4 @@
-apiVersion: schemas.schemahero.io/v1alpha4
+apiVersion: schemas.schemahero.io/v1alpha2
 kind: Table
 metadata:
   labels:
diff --git a/kotsadm/migrations/tables/preflight_spec.yaml b/kotsadm/migrations/tables/preflight_spec.yaml
index 93846d237..c97339418 100644
--- a/kotsadm/migrations/tables/preflight_spec.yaml
+++ b/kotsadm/migrations/tables/preflight_spec.yaml
@@ -1,4 +1,4 @@
-apiVersion: schemas.schemahero.io/v1alpha4
+apiVersion: schemas.schemahero.io/v1alpha2
 kind: Table
 metadata:
   name: preflight-spec
diff --git a/kotsadm/migrations/tables/pullrequest_history.yaml b/kotsadm/migrations/tables/pullrequest_history.yaml
new file mode 100644
index 000000000..492a33ee0
--- /dev/null
+++ b/kotsadm/migrations/tables/pullrequest_history.yaml
@@ -0,0 +1,53 @@
+apiVersion: schemas.schemahero.io/v1alpha2
+kind: Table
+metadata:
+  labels:
+    controller-tools.k8s.io: "1.0"
+  name: pullrequest-history
+spec:
+  database: kotsadm-postgres
+  name: pullrequest_history
+  requires: []
+  schema:
+    postgres:
+      primaryKey: []
+      columns:
+      - name: notification_id
+        type: text
+        constraints:
+          notNull: true
+      - name: pullrequest_number
+        type: integer
+        constraints:
+          notNull: true
+      - name: version_label
+        type: text
+        constraints:
+          notNull: true
+      - name: org
+        type: text
+        constraints:
+          notNull: true
+      - name: repo
+        type: text
+        constraints:
+          notNull: true
+      - name: branch
+        type: text
+      - name: root_path
+        type: text
+      - name: created_at
+        type: timestamp without time zone
+        constraints:
+          notNull: true
+      - name: github_status
+        type: text
+        constraints:
+          notNull: true
+        default: 'unknown'
+      - name: sequence
+        type: integer
+        default: "0"
+      - name: source_branch
+        type: text
+        default: ''
diff --git a/kotsadm/migrations/tables/pullrequest_notification.yaml b/kotsadm/migrations/tables/pullrequest_notification.yaml
new file mode 100644
index 000000000..cbcf86a5f
--- /dev/null
+++ b/kotsadm/migrations/tables/pullrequest_notification.yaml
@@ -0,0 +1,38 @@
+apiVersion: schemas.schemahero.io/v1alpha2
+kind: Table
+metadata:
+  labels:
+    controller-tools.k8s.io: "1.0"
+  name: pullrequest-notification
+spec:
+  database: kotsadm-postgres
+  name: pullrequest_notification
+  requires: []
+  schema:
+    postgres:
+      primaryKey: []
+      columns:
+      - name: notification_id
+        type: text
+        constraints:
+          notNull: true
+      - name: org
+        type: text
+        constraints:
+          notNull: true
+      - name: repo
+        type: text
+        constraints:
+          notNull: true
+      - name: branch
+        type: text
+      - name: root_path
+        type: text
+      - name: created_at
+        type: timestamp without time zone
+        constraints:
+          notNull: true
+      - name: github_installation_id
+        type: integer
+        constraints:
+          notNull: true
diff --git a/kotsadm/migrations/tables/scheduled_snapshots.yaml b/kotsadm/migrations/tables/scheduled_snapshots.yaml
index 3fa114d1c..5dcca1570 100644
--- a/kotsadm/migrations/tables/scheduled_snapshots.yaml
+++ b/kotsadm/migrations/tables/scheduled_snapshots.yaml
@@ -1,4 +1,4 @@
-apiVersion: schemas.schemahero.io/v1alpha4
+apiVersion: schemas.schemahero.io/v1alpha2
 kind: Table
 metadata:
   name: scheduled-snapshots
diff --git a/kotsadm/migrations/tables/session.yaml b/kotsadm/migrations/tables/session.yaml
index 6b1888364..89285d17c 100644
--- a/kotsadm/migrations/tables/session.yaml
+++ b/kotsadm/migrations/tables/session.yaml
@@ -1,4 +1,4 @@
-apiVersion: schemas.schemahero.io/v1alpha4
+apiVersion: schemas.schemahero.io/v1alpha2
 kind: Table
 metadata:
   labels:
diff --git a/kotsadm/migrations/tables/ship_edit.yaml b/kotsadm/migrations/tables/ship_edit.yaml
new file mode 100644
index 000000000..c12aa3f0c
--- /dev/null
+++ b/kotsadm/migrations/tables/ship_edit.yaml
@@ -0,0 +1,37 @@
+apiVersion: schemas.schemahero.io/v1alpha2
+kind: Table
+metadata:
+  labels:
+    controller-tools.k8s.io: "1.0"
+  name: ship-edit
+spec:
+  database: kotsadm-postgres
+  name: ship_edit
+  requires: []
+  schema:
+    postgres:
+      primaryKey:
+      - id
+      columns:
+      - name: id
+        type: text
+        constraints:
+          notNull: true
+      - name: watch_id
+        type: text
+        constraints:
+          notNull: true
+      - name: user_id
+        type: text
+        constraints:
+          notNull: true
+      - name: result
+        type: text
+      - name: created_at
+        type: timestamp without time zone
+        constraints:
+          notNull: true
+      - name: finished_at
+        type: timestamp without time zone
+      - name: is_headless
+        type: boolean
diff --git a/kotsadm/migrations/tables/ship_init.yaml b/kotsadm/migrations/tables/ship_init.yaml
new file mode 100644
index 000000000..29af4bd79
--- /dev/null
+++ b/kotsadm/migrations/tables/ship_init.yaml
@@ -0,0 +1,46 @@
+apiVersion: schemas.schemahero.io/v1alpha2
+kind: Table
+metadata:
+  labels:
+    controller-tools.k8s.io: "1.0"
+  name: ship-init
+spec:
+  database: kotsadm-postgres
+  name: ship_init
+  requires: []
+  schema:
+    postgres:
+      primaryKey:
+      - id
+      columns:
+      - name: id
+        type: text
+        constraints:
+          notNull: true
+      - name: upstream_uri
+        type: text
+        constraints:
+          notNull: true
+      - name: created_at
+        type: timestamp without time zone
+        constraints:
+          notNull: true
+      - name: finished_at
+        type: timestamp without time zone
+      - name: result
+        type: text
+      - name: user_id
+        type: text
+        constraints:
+          notNull: true
+      - name: cluster_id
+        type: text
+      - name: github_path
+        type: text
+      - name: requested_upstream_uri
+        type: text
+      - name: parent_watch_id
+        type: text
+      - name: parent_sequence
+        type: integer
+
diff --git a/kotsadm/migrations/tables/ship_init_pending.yaml b/kotsadm/migrations/tables/ship_init_pending.yaml
new file mode 100644
index 000000000..f8c975240
--- /dev/null
+++ b/kotsadm/migrations/tables/ship_init_pending.yaml
@@ -0,0 +1,39 @@
+apiVersion: schemas.schemahero.io/v1alpha2
+kind: Table
+metadata:
+  labels:
+    controller-tools.k8s.io: "1.0"
+  name: ship-init-pending
+spec:
+  database: kotsadm-postgres
+  name: ship_init_pending
+  requires: []
+  schema:
+    postgres:
+      primaryKey:
+      - id
+      columns:
+      - name: id
+        type: text
+        constraints:
+          notNull: true
+      - name: upstream_uri
+        type: text
+        constraints:
+          notNull: true
+      - name: requested_upstream_uri
+        type: text
+        constraints:
+          notNull: true
+      - name: title
+        type: text
+        constraints:
+          notNull: true
+      - name: created_at
+        type: timestamp without time zone
+        constraints:
+          notNull: true
+      - name: finished_at
+        type: timestamp without time zone
+      - name: result
+        type: text
diff --git a/kotsadm/migrations/tables/ship_init_pending_user.yaml b/kotsadm/migrations/tables/ship_init_pending_user.yaml
new file mode 100644
index 000000000..75dfc183d
--- /dev/null
+++ b/kotsadm/migrations/tables/ship_init_pending_user.yaml
@@ -0,0 +1,22 @@
+apiVersion: schemas.schemahero.io/v1alpha2
+kind: Table
+metadata:
+  labels:
+    controller-tools.k8s.io: "1.0"
+  name: ship-init-pending-user
+spec:
+  database: kotsadm-postgres
+  name: ship_init_pending_user
+  requires: []
+  schema:
+    postgres:
+      primaryKey: []
+      columns:
+      - name: user_id
+        type: text
+        constraints:
+          notNull: true
+      - name: ship_init_pending_id
+        type: text
+        constraints:
+          notNull: true
diff --git a/kotsadm/migrations/tables/ship_notification.yaml b/kotsadm/migrations/tables/ship_notification.yaml
new file mode 100644
index 000000000..65b418fb0
--- /dev/null
+++ b/kotsadm/migrations/tables/ship_notification.yaml
@@ -0,0 +1,33 @@
+apiVersion: schemas.schemahero.io/v1alpha2
+kind: Table
+metadata:
+  labels:
+    controller-tools.k8s.io: "1.0"
+  name: ship-notification
+spec:
+  database: kotsadm-postgres
+  name: ship_notification
+  requires: []
+  schema:
+    postgres:
+      primaryKey:
+      - id
+      columns:
+      - name: id
+        type: text
+        constraints:
+          notNull: true
+      - name: watch_id
+        type: text
+        constraints:
+          notNull: true
+      - name: created_at
+        type: timestamp without time zone
+        constraints:
+          notNull: true
+      - name: updated_at
+        type: timestamp without time zone
+      - name: triggered_at
+        type: timestamp without time zone
+      - name: enabled
+        type: smallint
diff --git a/kotsadm/migrations/tables/ship_output_files.yaml b/kotsadm/migrations/tables/ship_output_files.yaml
new file mode 100644
index 000000000..c47cc0387
--- /dev/null
+++ b/kotsadm/migrations/tables/ship_output_files.yaml
@@ -0,0 +1,33 @@
+apiVersion: schemas.schemahero.io/v1alpha2
+kind: Table
+metadata:
+  labels:
+    controller-tools.k8s.io: "1.0"
+  name: ship-output-files
+spec:
+  database: kotsadm-postgres
+  name: ship_output_files
+  requires: []
+  schema:
+    postgres:
+      primaryKey:
+      - watch_id
+      - sequence
+      columns:
+      - name: watch_id
+        type: text
+        constraints:
+          notNull: true
+      - name: created_at
+        type: timestamp without time zone
+        constraints:
+          notNull: true
+      - name: sequence
+        type: integer
+        constraints:
+          notNull: true
+        default: "0"
+      - name: filepath
+        type: text
+        constraints:
+          notNull: true
diff --git a/kotsadm/migrations/tables/ship_unfork.yaml b/kotsadm/migrations/tables/ship_unfork.yaml
new file mode 100644
index 000000000..db89cead9
--- /dev/null
+++ b/kotsadm/migrations/tables/ship_unfork.yaml
@@ -0,0 +1,39 @@
+apiVersion: schemas.schemahero.io/v1alpha2
+kind: Table
+metadata:
+  labels:
+    controller-tools.k8s.io: "1.0"
+  name: ship-unfork
+spec:
+  database: kotsadm-postgres
+  name: ship_unfork
+  requires: []
+  schema:
+    postgres:
+      primaryKey:
+      - id
+      columns:
+      - name: id
+        type: text
+        constraints:
+          notNull: true
+      - name: upstream_uri
+        type: text
+        constraints:
+          notNull: true
+      - name: fork_uri
+        type: text
+        constraints:
+          notNull: true
+      - name: created_at
+        type: timestamp without time zone
+        constraints:
+          notNull: true
+      - name: finished_at
+        type: timestamp without time zone
+      - name: result
+        type: text
+      - name: user_id
+        type: text
+        constraints:
+          notNull: true
diff --git a/kotsadm/migrations/tables/ship_update.yaml b/kotsadm/migrations/tables/ship_update.yaml
new file mode 100644
index 000000000..56965fc4d
--- /dev/null
+++ b/kotsadm/migrations/tables/ship_update.yaml
@@ -0,0 +1,39 @@
+apiVersion: schemas.schemahero.io/v1alpha2
+kind: Table
+metadata:
+  labels:
+    controller-tools.k8s.io: "1.0"
+  name: ship-update
+spec:
+  database: kotsadm-postgres
+  name: ship_update
+  requires: []
+  schema:
+    postgres:
+      primaryKey:
+      - id
+      columns:
+      - name: id
+        type: text
+        constraints:
+          notNull: true
+      - name: watch_id
+        type: text
+        constraints:
+          notNull: true
+      - name: user_id
+        type: text
+      - name: result
+        type: text
+      - name: created_at
+        type: timestamp without time zone
+        constraints:
+          notNull: true
+      - name: started_at
+        type: timestamp without time zone
+      - name: finished_at
+        type: timestamp without time zone
+      - name: parent_sequence
+        type: integer
+      - name: ship_logs
+        type: text
diff --git a/kotsadm/migrations/tables/ship_user.yaml b/kotsadm/migrations/tables/ship_user.yaml
index c63764384..46f31b524 100644
--- a/kotsadm/migrations/tables/ship_user.yaml
+++ b/kotsadm/migrations/tables/ship_user.yaml
@@ -1,4 +1,4 @@
-apiVersion: schemas.schemahero.io/v1alpha4
+apiVersion: schemas.schemahero.io/v1alpha2
 kind: Table
 metadata:
   labels:
@@ -23,4 +23,4 @@ spec:
         type: integer
       - name: last_login
         type: timestamp without time zone
-      
\ No newline at end of file
+      
diff --git a/kotsadm/migrations/tables/ship_user_local.yaml b/kotsadm/migrations/tables/ship_user_local.yaml
index 4469235ea..44d7dff76 100644
--- a/kotsadm/migrations/tables/ship_user_local.yaml
+++ b/kotsadm/migrations/tables/ship_user_local.yaml
@@ -1,4 +1,4 @@
-apiVersion: schemas.schemahero.io/v1alpha4
+apiVersion: schemas.schemahero.io/v1alpha2
 kind: Table
 metadata:
   labels:
@@ -31,4 +31,4 @@ spec:
       - name: email
         type: text
         constraints:
-          notNull: true
\ No newline at end of file
+          notNull: true
diff --git a/kotsadm/migrations/tables/supportbundle.yaml b/kotsadm/migrations/tables/supportbundle.yaml
index ffc9f3602..b5b8ce935 100644
--- a/kotsadm/migrations/tables/supportbundle.yaml
+++ b/kotsadm/migrations/tables/supportbundle.yaml
@@ -1,4 +1,4 @@
-apiVersion: schemas.schemahero.io/v1alpha4
+apiVersion: schemas.schemahero.io/v1alpha2
 kind: Table
 metadata:
   labels:
diff --git a/kotsadm/migrations/tables/supportbundle_analysis.yaml b/kotsadm/migrations/tables/supportbundle_analysis.yaml
index 98066c6e8..13e637890 100644
--- a/kotsadm/migrations/tables/supportbundle_analysis.yaml
+++ b/kotsadm/migrations/tables/supportbundle_analysis.yaml
@@ -1,4 +1,4 @@
-apiVersion: schemas.schemahero.io/v1alpha4
+apiVersion: schemas.schemahero.io/v1alpha2
 kind: Table
 metadata:
   labels:
diff --git a/kotsadm/migrations/tables/track_scm_leads.yaml b/kotsadm/migrations/tables/track_scm_leads.yaml
new file mode 100644
index 000000000..74918d1dc
--- /dev/null
+++ b/kotsadm/migrations/tables/track_scm_leads.yaml
@@ -0,0 +1,40 @@
+apiVersion: schemas.schemahero.io/v1alpha2
+kind: Table
+metadata:
+  labels:
+    controller-tools.k8s.io: "1.0"
+  name: track-scm-leads
+spec:
+  database: kotsadm-postgres
+  name: track_scm_leads
+  requires: []
+  schema:
+    postgres:
+      primaryKey:
+      - id
+      columns:
+      - name: id
+        type: text
+        constraints:
+          notNull: true
+      - name: deployment_type
+        type: text
+        constraints:
+          notNull: true
+      - name: email_address
+        type: text
+        constraints:
+          notNull: true
+      - name: scm_provider
+        type: text
+        constraints:
+          notNull: true
+      - name: created_at
+        type: timestamp without time zone
+        constraints:
+          notNull: true
+      - name: followed_up
+        type: boolean
+        constraints:
+          notNull: true
+        default: "false"
diff --git a/kotsadm/migrations/tables/user_app.yaml b/kotsadm/migrations/tables/user_app.yaml
index b23dda9c2..9585ffaee 100644
--- a/kotsadm/migrations/tables/user_app.yaml
+++ b/kotsadm/migrations/tables/user_app.yaml
@@ -1,5 +1,4 @@
- 
-apiVersion: schemas.schemahero.io/v1alpha4
+apiVersion: schemas.schemahero.io/v1alpha2
 kind: Table
 metadata:
   labels:
@@ -18,4 +17,4 @@ spec:
       - name: user_id
         type: text
       - name: app_id
-        type: text
\ No newline at end of file
+        type: text
diff --git a/kotsadm/migrations/tables/user_cluster.yaml b/kotsadm/migrations/tables/user_cluster.yaml
new file mode 100644
index 000000000..aa675c5d7
--- /dev/null
+++ b/kotsadm/migrations/tables/user_cluster.yaml
@@ -0,0 +1,22 @@
+apiVersion: schemas.schemahero.io/v1alpha2
+kind: Table
+metadata:
+  labels:
+    controller-tools.k8s.io: "1.0"
+  name: user-cluster
+spec:
+  database: kotsadm-postgres
+  name: user_cluster
+  requires: []
+  schema:
+    postgres:
+      primaryKey: []
+      columns:
+      - name: user_id
+        type: text
+        constraints:
+          notNull: true
+      - name: cluster_id
+        type: text
+        constraints:
+          notNull: true
diff --git a/kotsadm/migrations/tables/user_feature.yaml b/kotsadm/migrations/tables/user_feature.yaml
new file mode 100644
index 000000000..fbc3d39c2
--- /dev/null
+++ b/kotsadm/migrations/tables/user_feature.yaml
@@ -0,0 +1,24 @@
+apiVersion: schemas.schemahero.io/v1alpha2
+kind: Table
+metadata:
+  labels:
+    controller-tools.k8s.io: "1.0"
+  name: user-feature
+spec:
+  database: kotsadm-postgres
+  name: user_feature
+  requires: []
+  schema:
+    postgres:
+      primaryKey:
+      - user_id
+      - feature_id
+      columns:
+      - name: user_id
+        type: text
+        constraints:
+          notNull: true
+      - name: feature_id
+        type: text
+        constraints:
+          notNull: true
diff --git a/kotsadm/migrations/tables/user_watch.yaml b/kotsadm/migrations/tables/user_watch.yaml
new file mode 100644
index 000000000..e0e8fcc5c
--- /dev/null
+++ b/kotsadm/migrations/tables/user_watch.yaml
@@ -0,0 +1,24 @@
+apiVersion: schemas.schemahero.io/v1alpha2
+kind: Table
+metadata:
+  labels:
+    controller-tools.k8s.io: "1.0"
+  name: user-watch
+spec:
+  database: kotsadm-postgres
+  name: user_watch
+  requires: []
+  schema:
+    postgres:
+      primaryKey:
+      - user_id
+      - watch_id
+      columns:
+      - name: user_id
+        type: text
+        constraints:
+          notNull: true
+      - name: watch_id
+        type: text
+        constraints:
+          notNull: true
diff --git a/kotsadm/migrations/tables/watch.yaml b/kotsadm/migrations/tables/watch.yaml
new file mode 100644
index 000000000..fc770c828
--- /dev/null
+++ b/kotsadm/migrations/tables/watch.yaml
@@ -0,0 +1,48 @@
+apiVersion: schemas.schemahero.io/v1alpha2
+kind: Table
+metadata:
+  labels:
+    controller-tools.k8s.io: "1.0"
+  name: watch
+spec:
+  database: kotsadm-postgres
+  name: watch
+  requires: []
+  schema:
+    postgres:
+      primaryKey:
+      - id
+      columns:
+      - name: id
+        type: text
+        constraints:
+          notNull: true
+      - name: current_state
+        type: text
+      - name: title
+        type: text
+        constraints:
+          notNull: true
+      - name: icon_uri
+        type: text
+      - name: created_at
+        type: timestamp without time zone
+        constraints:
+          notNull: true
+      - name: updated_at
+        type: timestamp without time zone
+      - name: slug
+        type: text
+        constraints:
+          notNull: true
+      - name: parent_watch_id
+        type: text
+      - name: current_sequence
+        type: integer
+      - name: metadata
+        type: text
+      - name: last_watch_check_at
+        type: timestamp without time zone
+      - name: is_update_required
+        type: boolean
+        default: "false"
diff --git a/kotsadm/migrations/tables/watch_cluster.yaml b/kotsadm/migrations/tables/watch_cluster.yaml
new file mode 100644
index 000000000..7c6cdc48e
--- /dev/null
+++ b/kotsadm/migrations/tables/watch_cluster.yaml
@@ -0,0 +1,24 @@
+apiVersion: schemas.schemahero.io/v1alpha2
+kind: Table
+metadata:
+  labels:
+    controller-tools.k8s.io: "1.0"
+  name: watch-cluster
+spec:
+  database: kotsadm-postgres
+  name: watch_cluster
+  requires: []
+  schema:
+    postgres:
+      primaryKey: []
+      columns:
+      - name: watch_id
+        type: text
+        constraints:
+          notNull: true
+      - name: cluster_id
+        type: text
+        constraints:
+          notNull: true
+      - name: github_path
+        type: text
diff --git a/kotsadm/migrations/tables/watch_downstream_token.yaml b/kotsadm/migrations/tables/watch_downstream_token.yaml
new file mode 100644
index 000000000..0fd323cff
--- /dev/null
+++ b/kotsadm/migrations/tables/watch_downstream_token.yaml
@@ -0,0 +1,23 @@
+apiVersion: schemas.schemahero.io/v1alpha2
+kind: Table
+metadata:
+  labels:
+    controller-tools.k8s.io: "1.0"
+  name: watch-downstream-token
+spec:
+  database: kotsadm-postgres
+  name: watch_downstream_token
+  requires: []
+  schema:
+    postgres:
+      primaryKey:
+      - token
+      columns:
+      - name: token
+        type: text
+        constraints:
+          notNull: true
+      - name: watch_id
+        type: text
+        constraints:
+          notNull: true
diff --git a/kotsadm/migrations/tables/watch_feature.yaml b/kotsadm/migrations/tables/watch_feature.yaml
new file mode 100644
index 000000000..ca5be145b
--- /dev/null
+++ b/kotsadm/migrations/tables/watch_feature.yaml
@@ -0,0 +1,24 @@
+apiVersion: schemas.schemahero.io/v1alpha2
+kind: Table
+metadata:
+  labels:
+    controller-tools.k8s.io: "1.0"
+  name: watch-feature
+spec:
+  database: kotsadm-postgres
+  name: watch_feature
+  requires: []
+  schema:
+    postgres:
+      primaryKey:
+      - watch_id
+      - feature_id
+      columns:
+      - name: watch_id
+        type: text
+        constraints:
+          notNull: true
+      - name: feature_id
+        type: text
+        constraints:
+          notNull: true
diff --git a/kotsadm/migrations/tables/watch_license.yaml b/kotsadm/migrations/tables/watch_license.yaml
new file mode 100644
index 000000000..8d8e3866a
--- /dev/null
+++ b/kotsadm/migrations/tables/watch_license.yaml
@@ -0,0 +1,24 @@
+apiVersion: schemas.schemahero.io/v1alpha2
+kind: Table
+metadata:
+  labels:
+    controller-tools.k8s.io: "1.0"
+  name: watch-license
+spec:
+  database: kotsadm-postgres
+  name: watch_license
+  requires: []
+  schema:
+    postgres:
+      primaryKey:
+      - watch_id
+      columns:
+      - name: watch_id
+        type: text
+        constraints:
+          notNull: true
+      - name: license
+        type: text
+      - name: license_updated_at
+        type: timestamp without time zone
+
diff --git a/kotsadm/migrations/tables/watch_troubleshoot_analyzer.yaml b/kotsadm/migrations/tables/watch_troubleshoot_analyzer.yaml
new file mode 100644
index 000000000..9caab6752
--- /dev/null
+++ b/kotsadm/migrations/tables/watch_troubleshoot_analyzer.yaml
@@ -0,0 +1,29 @@
+apiVersion: schemas.schemahero.io/v1alpha2
+kind: Table
+metadata:
+  labels:
+    controller-tools.k8s.io: "1.0"
+  name: watch-toubleshoot-analyzer
+spec:
+  database: kotsadm-postgres
+  name: watch_troubleshoot_analyzer
+  requires: []
+  schema:
+    postgres:
+      primaryKey:
+      - watch_id
+      columns:
+      - name: watch_id
+        type: text
+        constraints:
+          notNull: true
+      - name: release_analyzer
+        type: text
+      - name: release_analyzer_updated_at
+        type: timestamp without time zone
+      - name: updated_analyzer
+        type: text
+      - name: updated_analyzer_updated_at
+        type: timestamp without time zone
+      - name: use_updated_analyzer
+        type: boolean
diff --git a/kotsadm/migrations/tables/watch_troubleshoot_collector.yaml b/kotsadm/migrations/tables/watch_troubleshoot_collector.yaml
new file mode 100644
index 000000000..e0ac570b4
--- /dev/null
+++ b/kotsadm/migrations/tables/watch_troubleshoot_collector.yaml
@@ -0,0 +1,29 @@
+apiVersion: schemas.schemahero.io/v1alpha2
+kind: Table
+metadata:
+  labels:
+    controller-tools.k8s.io: "1.0"
+  name: watch-toubleshoot-collector
+spec:
+  database: kotsadm-postgres
+  name: watch_troubleshoot_collector
+  requires: []
+  schema:
+    postgres:
+      primaryKey:
+      - watch_id
+      columns:
+      - name: watch_id
+        type: text
+        constraints:
+          notNull: true
+      - name: release_collector
+        type: text
+      - name: release_collector_updated_at
+        type: timestamp without time zone
+      - name: updated_collector
+        type: text
+      - name: updated_collector_updated_at
+        type: timestamp without time zone
+      - name: use_updated_collector
+        type: boolean
diff --git a/kotsadm/migrations/tables/watch_version.yaml b/kotsadm/migrations/tables/watch_version.yaml
new file mode 100644
index 000000000..9dbe26000
--- /dev/null
+++ b/kotsadm/migrations/tables/watch_version.yaml
@@ -0,0 +1,47 @@
+apiVersion: schemas.schemahero.io/v1alpha2
+kind: Table
+metadata:
+  name: watch-version
+spec:
+  database: kotsadm-postgres
+  name: watch_version
+  requires: []
+  schema:
+    postgres:
+      primaryKey:
+        - watch_id
+        - sequence
+      columns:
+      - name: watch_id
+        type: text
+        constraints:
+          notNull: true
+      - name: created_at
+        type: timestamp without time zone
+      - name: version_label
+        type: text
+        constraints:
+          notNull: true
+      - name: status
+        type: text
+        constraints:
+          notNull: true
+        default: 'unknown'
+      - name: source_branch
+        type: text
+      - name: sequence
+        type: integer
+        default: "0"
+      - name: pullrequest_number
+        type: integer
+      - name: parent_sequence
+        type: integer
+      - name: deployed_at
+        type: timestamp without time zone
+      - name: commit_sha
+        type: text
+      - name: last_synced_at
+        type: timestamp without time zone
+      - name: is_404
+        type: boolean
+        default: "false"
diff --git a/kotsadm/migrations/tables/webhook_notification.yaml b/kotsadm/migrations/tables/webhook_notification.yaml
new file mode 100644
index 000000000..32ee1e390
--- /dev/null
+++ b/kotsadm/migrations/tables/webhook_notification.yaml
@@ -0,0 +1,26 @@
+apiVersion: schemas.schemahero.io/v1alpha2
+kind: Table
+metadata:
+  labels:
+    controller-tools.k8s.io: "1.0"
+  name: webhook-notification
+spec:
+  database: kotsadm-postgres
+  name: webhook_notification
+  requires: []
+  schema:
+    postgres:
+      primaryKey: []
+      columns:
+      - name: notification_id
+        type: text
+        constraints:
+          notNull: true
+      - name: destination_uri
+        type: text
+        constraints:
+          notNull: true
+      - name: created_at
+        type: timestamp without time zone
+        constraints:
+          notNull: true
diff --git a/kotsadm/minio/Makefile b/kotsadm/minio/Makefile
index 4e47b5648..b15b80ba9 100644
--- a/kotsadm/minio/Makefile
+++ b/kotsadm/minio/Makefile
@@ -19,5 +19,5 @@ publish-latest: publish
 .PHONY: publish-release
 publish-release: IMAGE_TAG = ${GIT_TAG}
 publish-release: publish
-	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
+	skopeo copy docker-daemon:kotsadm/${PROJECT_NAME}:${GIT_TAG} docker-archive:bin/docker-archive/${PROJECT_NAME}.tar
diff --git a/kotsadm/operator/Makefile b/kotsadm/operator/Makefile
index 6dac06cfc..328bff57b 100644
--- a/kotsadm/operator/Makefile
+++ b/kotsadm/operator/Makefile
@@ -51,5 +51,5 @@ build-alpha:
 build-release:
 	docker build -f deploy/Dockerfile -t kotsadm/kotsadm-operator:${GIT_TAG} .
 	docker push kotsadm/kotsadm-operator:${GIT_TAG}
-	mkdir -p bin/docker-archive/kotsadm-operator
-	skopeo copy docker-daemon:kotsadm/kotsadm-operator:${GIT_TAG} docker-archive:bin/docker-archive/kotsadm-operator/${GIT_TAG}
+	mkdir -p bin/docker-archive
+	skopeo copy docker-daemon:kotsadm/kotsadm-operator:${GIT_TAG} docker-archive:bin/docker-archive/kotsadm-operator.tar
diff --git a/kotsadm/pkg/airgap/airgap.go b/kotsadm/pkg/airgap/airgap.go
index 7a11a4eb7..561899960 100644
--- a/kotsadm/pkg/airgap/airgap.go
+++ b/kotsadm/pkg/airgap/airgap.go
@@ -80,17 +80,17 @@ func CreateAppFromAirgap(pendingApp *PendingApp, airgapBundle multipart.File, re
 	defer func() {
 		if finalError == nil {
 			if err := task.ClearTaskStatus("airgap-install"); err != nil {
-				logger.Error(errors.Wrap(err, "failed to clear install task status"))
+				logger.Error(errors.Wrap(err, "faild to clear install task status"))
 			}
 			if err := setAppInstallState(pendingApp.ID, "installed"); err != nil {
-				logger.Error(errors.Wrap(err, "failed to set app status to installed"))
+				logger.Error(errors.Wrap(err, "faild to set app status to installed"))
 			}
 		} else {
 			if err := task.SetTaskStatus("airgap-install", finalError.Error(), "failed"); err != nil {
-				logger.Error(errors.Wrap(err, "failed to set error on install task status"))
+				logger.Error(errors.Wrap(err, "faild to set error on install task status"))
 			}
 			if err := setAppInstallState(pendingApp.ID, "airgap_upload_error"); err != nil {
-				logger.Error(errors.Wrap(err, "failed to set app status to error"))
+				logger.Error(errors.Wrap(err, "faild to set app status to error"))
 			}
 		}
 	}()
@@ -115,10 +115,6 @@ func CreateAppFromAirgap(pendingApp *PendingApp, airgapBundle multipart.File, re
 	defer os.RemoveAll(tmpFile.Name())
 
 	// Extract it
-	if err := task.SetTaskStatus("airgap-install", "Extracting files...", "running"); err != nil {
-		return errors.Wrap(err, "failed to set task status")
-	}
-
 	// we seem to need a lot of temp dirs here... maybe too many?
 	archiveDir, err := version.ExtractArchiveToTempDirectory(tmpFile.Name())
 	if err != nil {
@@ -126,6 +122,10 @@ func CreateAppFromAirgap(pendingApp *PendingApp, airgapBundle multipart.File, re
 	}
 	defer os.RemoveAll(archiveDir)
 
+	if err := task.SetTaskStatus("airgap-install", "Processing app package...", "running"); err != nil {
+		return errors.Wrap(err, "failed to set task status")
+	}
+
 	// extract the release
 	workspace, err := ioutil.TempDir("", "kots-airgap")
 	if err != nil {
@@ -144,10 +144,6 @@ func CreateAppFromAirgap(pendingApp *PendingApp, airgapBundle multipart.File, re
 	}
 	defer os.RemoveAll(tmpRoot)
 
-	if err := task.SetTaskStatus("airgap-install", "Reading license data...", "running"); err != nil {
-		return errors.Wrap(err, "failed to set task status")
-	}
-
 	decode := scheme.Codecs.UniversalDeserializer().Decode
 	obj, _, err := decode([]byte(pendingApp.LicenseData), nil, nil)
 	if err != nil {
@@ -241,17 +237,6 @@ func CreateAppFromAirgap(pendingApp *PendingApp, airgapBundle multipart.File, re
 		return errors.Wrap(err, "failed to get app from pending app")
 	}
 
-	if password == registry.PasswordMask {
-		// On initial install, registry info can be copied from kotsadm config,
-		// and password in this case will not be included in the request.
-		kotsadmSettings, err := registry.GetKotsadmRegistry()
-		if err != nil {
-			logger.Error(errors.Wrap(err, "failed to load kotsadm config"))
-		} else if kotsadmSettings.Hostname == registryHost {
-			password = kotsadmSettings.Password
-		}
-	}
-
 	if err := registry.UpdateRegistry(pendingApp.ID, registryHost, username, password, namespace); err != nil {
 		return errors.Wrap(err, "failed to update registry")
 	}
diff --git a/kotsadm/pkg/airgap/update.go b/kotsadm/pkg/airgap/update.go
index c63ca6515..daa16326a 100644
--- a/kotsadm/pkg/airgap/update.go
+++ b/kotsadm/pkg/airgap/update.go
@@ -47,11 +47,11 @@ func UpdateAppFromAirgap(a *app.App, airgapBundle multipart.File) (finalError er
 	defer func() {
 		if finalError == nil {
 			if err := task.ClearTaskStatus("update-download"); err != nil {
-				logger.Error(errors.Wrap(err, "failed to clear update-download task status"))
+				logger.Error(errors.Wrap(err, "faild to clear update-download task status"))
 			}
 		} else {
 			if err := task.SetTaskStatus("update-download", finalError.Error(), "failed"); err != nil {
-				logger.Error(errors.Wrap(err, "failed to set error on update-download task status"))
+				logger.Error(errors.Wrap(err, "faild to set error on update-download task status"))
 			}
 		}
 	}()
@@ -95,21 +95,12 @@ func UpdateAppFromAirgap(a *app.App, airgapBundle multipart.File) (finalError er
 	if err != nil {
 		return errors.Wrap(err, "failed to create temp file")
 	}
-
-	if err := task.SetTaskStatus("update-download", "Copying package...", "running"); err != nil {
-		return errors.Wrap(err, "failed to set task status")
-	}
-
 	_, err = io.Copy(tmpFile, airgapBundle)
 	if err != nil {
 		return errors.Wrap(err, "failed to copy temp airgap")
 	}
 	defer os.RemoveAll(tmpFile.Name())
 
-	if err := task.SetTaskStatus("update-download", "Extracting files...", "running"); err != nil {
-		return errors.Wrap(err, "failed to set task status")
-	}
-
 	airgapRoot, err := version.ExtractArchiveToTempDirectory(tmpFile.Name())
 	if err != nil {
 		return errors.Wrap(err, "failed to extract archive")
@@ -125,10 +116,6 @@ func UpdateAppFromAirgap(a *app.App, airgapBundle multipart.File) (finalError er
 		appNamespace = os.Getenv("KOTSADM_TARGET_NAMESPACE")
 	}
 
-	if err := task.SetTaskStatus("update-download", "Creating app version...", "running"); err != nil {
-		return errors.Wrap(err, "failed to set task status")
-	}
-
 	appSequence, err := version.GetNextAppSequence(a.ID, &a.CurrentSequence)
 	if err != nil {
 		return errors.Wrap(err, "failed to get new app sequence")
diff --git a/kotsadm/pkg/apiserver/server.go b/kotsadm/pkg/apiserver/server.go
index 6f5bad41f..b5d9f4c83 100644
--- a/kotsadm/pkg/apiserver/server.go
+++ b/kotsadm/pkg/apiserver/server.go
@@ -9,6 +9,7 @@ import (
 	"net/url"
 	"os"
 	"path/filepath"
+	"runtime"
 	"time"
 
 	"github.com/gorilla/mux"
@@ -19,7 +20,7 @@ import (
 )
 
 func Start() {
-	log.Printf("kotsadm version %s\n", os.Getenv("VERSION"))
+	log.Printf("kotsadm version %s built with %s\n", os.Getenv("VERSION"), runtime.Version())
 
 	ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
 	if err := waitForDependencies(ctx); err != nil {
@@ -106,12 +107,8 @@ func Start() {
 	r.Path("/api/v1/license").Methods("OPTIONS", "POST").HandlerFunc(handlers.UploadNewLicense)
 	r.Path("/api/v1/license/resume").Methods("OPTIONS", "PUT").HandlerFunc(handlers.ResumeInstallOnline)
 
-	r.Path("/api/v1/registry").Methods("OPTIONS", "GET").HandlerFunc(handlers.GetKotsadmRegistry)
-
 	r.Path("/api/v1/metadata").Methods("OPTIONS", "GET").HandlerFunc(handlers.Metadata)
 	r.Path("/api/v1/app/{appSlug}/registry").Methods("OPTIONS", "PUT").HandlerFunc(handlers.UpdateAppRegistry)
-	r.Path("/api/v1/app/{appSlug}/registry").Methods("OPTIONS", "GET").HandlerFunc(handlers.GetAppRegistry)
-	r.Path("/api/v1/app/{appSlug}/registry/validate").Methods("OPTIONS", "POST").HandlerFunc(handlers.ValidateAppRegistry)
 	r.Path("/api/v1/app/{appSlug}/config").Methods("OPTIONS", "PUT").HandlerFunc(handlers.UpdateAppConfig)
 	r.Path("/api/v1/app/{appSlug}/license").Methods("OPTIONS", "PUT").HandlerFunc(handlers.SyncLicense)
 	r.Path("/api/v1/app/{appSlug}/updatecheck").Methods("OPTIONS", "POST").HandlerFunc(handlers.AppUpdateCheck)
diff --git a/kotsadm/pkg/app/app.go b/kotsadm/pkg/app/app.go
index 58df0a43a..d1537c049 100644
--- a/kotsadm/pkg/app/app.go
+++ b/kotsadm/pkg/app/app.go
@@ -29,14 +29,6 @@ type App struct {
 	IsGitOps              bool
 }
 
-type RegistryInfo struct {
-	Hostname    string
-	Username    string
-	Password    string
-	PasswordEnc string
-	Namespace   string
-}
-
 func Get(id string) (*App, error) {
 	logger.Debug("getting app from id",
 		zap.String("id", id))
diff --git a/kotsadm/pkg/automation/automation.go b/kotsadm/pkg/automation/automation.go
index 286c90606..6d81a8aed 100644
--- a/kotsadm/pkg/automation/automation.go
+++ b/kotsadm/pkg/automation/automation.go
@@ -101,7 +101,7 @@ func AutomateInstall() error {
 			Name:        a.Name,
 			LicenseData: string(license),
 		}
-		_, err = online.CreateAppFromOnline(&pendingApp, upstreamURI, true)
+		_, err = online.CreateAppFromOnline(&pendingApp, upstreamURI)
 		if err != nil {
 			logger.Error(err)
 			continue
diff --git a/kotsadm/pkg/handlers/download.go b/kotsadm/pkg/handlers/download.go
index 4099734fb..4194dd84c 100644
--- a/kotsadm/pkg/handlers/download.go
+++ b/kotsadm/pkg/handlers/download.go
@@ -1,6 +1,7 @@
 package handlers
 
 import (
+	"database/sql"
 	"io"
 	"io/ioutil"
 	"net/http"
@@ -9,6 +10,7 @@ import (
 	"strconv"
 
 	"github.com/mholt/archiver"
+	"github.com/pkg/errors"
 	"github.com/replicatedhq/kots/kotsadm/pkg/app"
 	"github.com/replicatedhq/kots/kotsadm/pkg/kotsutil"
 	"github.com/replicatedhq/kots/kotsadm/pkg/logger"
@@ -24,7 +26,11 @@ func DownloadApp(w http.ResponseWriter, r *http.Request) {
 	a, err := app.GetFromSlug(r.URL.Query().Get("slug"))
 	if err != nil {
 		logger.Error(err)
-		w.WriteHeader(500)
+		if errors.Is(err, sql.ErrNoRows) {
+			w.WriteHeader(404)
+		} else {
+			w.WriteHeader(500)
+		}
 		return
 	}
 
diff --git a/kotsadm/pkg/handlers/license.go b/kotsadm/pkg/handlers/license.go
index d1f883ffe..0c9377ddd 100644
--- a/kotsadm/pkg/handlers/license.go
+++ b/kotsadm/pkg/handlers/license.go
@@ -236,7 +236,7 @@ func UploadNewLicense(w http.ResponseWriter, r *http.Request) {
 			Name:        a.Name,
 			LicenseData: uploadLicenseRequest.LicenseData,
 		}
-		kotsKinds, err := online.CreateAppFromOnline(&pendingApp, upstreamURI, false)
+		kotsKinds, err := online.CreateAppFromOnline(&pendingApp, upstreamURI)
 		if err != nil {
 			logger.Error(err)
 			uploadLicenseResponse.Error = err.Error()
@@ -344,7 +344,7 @@ func ResumeInstallOnline(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	kotsKinds, err := online.CreateAppFromOnline(&pendingApp, fmt.Sprintf("replicated://%s", kotsLicense.Spec.AppSlug), false)
+	kotsKinds, err := online.CreateAppFromOnline(&pendingApp, fmt.Sprintf("replicated://%s", kotsLicense.Spec.AppSlug))
 	if err != nil {
 		logger.Error(err)
 		resumeInstallOnlineResponse.Error = err.Error()
diff --git a/kotsadm/pkg/handlers/registry.go b/kotsadm/pkg/handlers/registry.go
index 0ea20bcca..2514a0560 100644
--- a/kotsadm/pkg/handlers/registry.go
+++ b/kotsadm/pkg/handlers/registry.go
@@ -10,7 +10,6 @@ import (
 	"github.com/replicatedhq/kots/kotsadm/pkg/logger"
 	"github.com/replicatedhq/kots/kotsadm/pkg/registry"
 	"github.com/replicatedhq/kots/kotsadm/pkg/task"
-	dockerregistry "github.com/replicatedhq/kots/pkg/docker/registry"
 )
 
 type UpdateAppRegistryRequest struct {
@@ -28,36 +27,6 @@ type UpdateAppRegistryResponse struct {
 	Namespace string `json:"namespace"`
 }
 
-type GetAppRegistryResponse struct {
-	Success   bool   `json:"success"`
-	Error     string `json:"error,omitempty"`
-	Hostname  string `json:"hostname"`
-	Namespace string `json:"namespace"`
-	Username  string `json:"username"`
-	Password  string `json:"password"`
-}
-
-type GetKotsadmRegistryResponse struct {
-	Success   bool   `json:"success"`
-	Error     string `json:"error,omitempty"`
-	Hostname  string `json:"hostname"`
-	Namespace string `json:"namespace"`
-	Username  string `json:"username"`
-	Password  string `json:"password"`
-}
-
-type ValidateAppRegistryRequest struct {
-	Hostname  string `json:"hostname"`
-	Namespace string `json:"namespace"`
-	Username  string `json:"username"`
-	Password  string `json:"password"`
-}
-
-type ValidateAppRegistryResponse struct {
-	Success bool   `json:"success"`
-	Error   string `json:"error,omitempty"`
-}
-
 func UpdateAppRegistry(w http.ResponseWriter, r *http.Request) {
 	w.Header().Set("Access-Control-Allow-Origin", "*")
 	w.Header().Set("Access-Control-Allow-Headers", "content-type, origin, accept, authorization")
@@ -178,163 +147,9 @@ func GetAppRegistry(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	getAppRegistryResponse := GetAppRegistryResponse{
-		Success: false,
-	}
-
-	if err := requireValidSession(w, r); err != nil {
-		logger.Error(err)
-		getAppRegistryResponse.Error = err.Error()
-		JSON(w, 401, getAppRegistryResponse)
-		return
-	}
-
-	foundApp, err := app.GetFromSlug(mux.Vars(r)["appSlug"])
-	if err != nil {
-		logger.Error(err)
-		getAppRegistryResponse.Error = err.Error()
-		JSON(w, 500, getAppRegistryResponse)
-		return
-	}
-
-	settings, err := registry.GetRegistrySettingsForApp(foundApp.ID)
-	if err != nil {
-		logger.Error(err)
-		getAppRegistryResponse.Error = err.Error()
-		JSON(w, 500, getAppRegistryResponse)
-		return
-	}
-
-	getAppRegistryResponse.Success = true
-	getAppRegistryResponse.Hostname = settings.Hostname
-	getAppRegistryResponse.Namespace = settings.Namespace
-	getAppRegistryResponse.Username = settings.Username
-	getAppRegistryResponse.Password = registry.PasswordMask
-
-	JSON(w, 200, getAppRegistryResponse)
-}
-
-func GetKotsadmRegistry(w http.ResponseWriter, r *http.Request) {
-	w.Header().Set("Access-Control-Allow-Origin", "*")
-	w.Header().Set("Access-Control-Allow-Headers", "content-type, origin, accept, authorization")
-
-	if r.Method == "OPTIONS" {
-		w.WriteHeader(http.StatusOK)
-		return
-	}
-
-	getKotsadmRegistryResponse := GetKotsadmRegistryResponse{
-		Success: false,
-	}
-
 	if err := requireValidSession(w, r); err != nil {
 		logger.Error(err)
-		getKotsadmRegistryResponse.Error = err.Error()
-		JSON(w, 401, getKotsadmRegistryResponse)
-		return
-	}
-
-	settings, err := registry.GetKotsadmRegistry()
-	if err != nil {
-		logger.Error(err)
-		getKotsadmRegistryResponse.Error = err.Error()
-		JSON(w, 500, getKotsadmRegistryResponse)
-		return
-	}
-
-	getKotsadmRegistryResponse.Success = true
-	getKotsadmRegistryResponse.Hostname = settings.Hostname
-	getKotsadmRegistryResponse.Namespace = settings.Namespace
-	getKotsadmRegistryResponse.Username = settings.Username
-	getKotsadmRegistryResponse.Password = registry.PasswordMask
-
-	JSON(w, 200, getKotsadmRegistryResponse)
-}
-
-func ValidateAppRegistry(w http.ResponseWriter, r *http.Request) {
-	w.Header().Set("Access-Control-Allow-Origin", "*")
-	w.Header().Set("Access-Control-Allow-Headers", "content-type, origin, accept, authorization")
-
-	if r.Method == "OPTIONS" {
-		w.WriteHeader(http.StatusOK)
-		return
-	}
-
-	validateAppRegistryResponse := ValidateAppRegistryResponse{
-		Success: false,
-	}
-
-	validateAppRegistryRequest := ValidateAppRegistryRequest{}
-	if err := json.NewDecoder(r.Body).Decode(&validateAppRegistryRequest); err != nil {
-		logger.Error(err)
-		validateAppRegistryResponse.Error = err.Error()
-		JSON(w, 500, validateAppRegistryResponse)
-		return
-	}
-
-	if err := requireValidSession(w, r); err != nil {
-		logger.Error(err)
-		validateAppRegistryResponse.Error = err.Error()
-		JSON(w, 401, validateAppRegistryResponse)
-		return
-	}
-
-	foundApp, err := app.GetFromSlug(mux.Vars(r)["appSlug"])
-	if err != nil {
-		logger.Error(err)
-		validateAppRegistryResponse.Error = err.Error()
-		JSON(w, 500, validateAppRegistryResponse)
-		return
-	}
-
-	password := validateAppRegistryRequest.Password
-	if password == registry.PasswordMask {
-		appSettings, err := registry.GetRegistrySettingsForApp(foundApp.ID)
-		if err != nil {
-			logger.Error(err)
-			validateAppRegistryResponse.Error = err.Error()
-			JSON(w, 500, validateAppRegistryResponse)
-			return
-		}
-
-		if appSettings != nil && appSettings.Password != "" {
-			password = appSettings.Password
-
-		} else {
-			kotsadmSettings, err := registry.GetKotsadmRegistry()
-			if err != nil {
-				logger.Error(err)
-				validateAppRegistryResponse.Error = err.Error()
-				JSON(w, 500, validateAppRegistryResponse)
-				return
-			}
-
-			if kotsadmSettings.Hostname != validateAppRegistryRequest.Hostname || kotsadmSettings.Password == "" {
-				err := errors.Errorf("no password found for %s", validateAppRegistryRequest.Hostname)
-				logger.Error(err)
-				validateAppRegistryResponse.Error = err.Error()
-				JSON(w, 400, validateAppRegistryResponse)
-				return
-			}
-			password = kotsadmSettings.Password
-		}
-	}
-	if password == "" || password == registry.PasswordMask {
-		err := errors.Errorf("no password found for %s", validateAppRegistryRequest.Hostname)
-		logger.Error(err)
-		validateAppRegistryResponse.Error = err.Error()
-		JSON(w, 400, validateAppRegistryResponse)
-		return
-	}
-
-	err = dockerregistry.TestPushAccess(validateAppRegistryRequest.Hostname, validateAppRegistryRequest.Username, password, validateAppRegistryRequest.Namespace)
-	if err != nil {
-		logger.Error(err)
-		validateAppRegistryResponse.Error = err.Error()
-		JSON(w, 500, validateAppRegistryResponse)
 		return
 	}
 
-	validateAppRegistryResponse.Success = true
-	JSON(w, 200, validateAppRegistryResponse)
 }
diff --git a/kotsadm/pkg/handlers/snapshots.go b/kotsadm/pkg/handlers/snapshots.go
index 084dae581..48f233a30 100644
--- a/kotsadm/pkg/handlers/snapshots.go
+++ b/kotsadm/pkg/handlers/snapshots.go
@@ -31,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) {
@@ -112,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 {
@@ -150,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 {
@@ -191,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
@@ -228,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
@@ -254,44 +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
-
-		secret, err := kurl.GetS3Secret()
-		if err != nil {
-			logger.Error(err)
-			globalSnapshotSettingsResponse.Error = err.Error()
-			JSON(w, 500, globalSnapshotSettingsResponse)
-			return
-		}
-		if secret == nil {
-			logger.Error(errors.New("s3 secret does not exist"))
-			globalSnapshotSettingsResponse.Error = "s3 secret does not exist"
-			JSON(w, 500, 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 {
@@ -304,8 +261,8 @@ func UpdateGlobalSnapshotSettings(w http.ResponseWriter, r *http.Request) {
 	updatedBackupStorageLocation, err := snapshot.UpdateGlobalStore(store)
 	if err != nil {
 		logger.Error(err)
-		globalSnapshotSettingsResponse.Error = "failed to update global store"
-		JSON(w, 500, globalSnapshotSettingsResponse)
+		globalSnapshotSettingsResponse.Error = "failed to decode request body"
+		JSON(w, 400, globalSnapshotSettingsResponse)
 		return
 	}
 
@@ -313,7 +270,7 @@ func UpdateGlobalSnapshotSettings(w http.ResponseWriter, r *http.Request) {
 	if err := snapshot.RestartVelero(); err != nil {
 		logger.Error(err)
 		globalSnapshotSettingsResponse.Error = "failed to try to restart velero"
-		JSON(w, 500, globalSnapshotSettingsResponse)
+		JSON(w, 400, globalSnapshotSettingsResponse)
 		return
 	}
 
diff --git a/kotsadm/pkg/informers/informers.go b/kotsadm/pkg/informers/informers.go
index f2254ef0e..e6bb46ef5 100644
--- a/kotsadm/pkg/informers/informers.go
+++ b/kotsadm/pkg/informers/informers.go
@@ -10,7 +10,6 @@ import (
 	"github.com/replicatedhq/kots/kotsadm/pkg/supportbundle"
 	velerov1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
 	veleroclientv1 "github.com/vmware-tanzu/velero/pkg/generated/clientset/versioned/typed/velero/v1"
-	kuberneteserrors "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/watch"
 	"sigs.k8s.io/controller-runtime/pkg/client/config"
@@ -33,10 +32,6 @@ func Start() error {
 
 	backupWatch, err := veleroClient.Backups("").Watch(context.TODO(), metav1.ListOptions{ResourceVersion: "0"})
 	if err != nil {
-		if kuberneteserrors.IsNotFound(err) {
-			return nil
-		}
-
 		return errors.Wrap(err, "failed to watch")
 	}
 
diff --git a/kotsadm/pkg/kurl/secrets.go b/kotsadm/pkg/kurl/secrets.go
deleted file mode 100644
index 53b15ea7e..000000000
--- a/kotsadm/pkg/kurl/secrets.go
+++ /dev/null
@@ -1,35 +0,0 @@
-package kurl
-
-import (
-	"context"
-	"os"
-
-	"github.com/pkg/errors"
-	corev1 "k8s.io/api/core/v1"
-	kuberneteserrors "k8s.io/apimachinery/pkg/api/errors"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/client-go/kubernetes"
-	"sigs.k8s.io/controller-runtime/pkg/client/config"
-)
-
-func GetS3Secret() (*corev1.Secret, error) {
-	cfg, err := config.GetConfig()
-	if err != nil {
-		return nil, errors.Wrap(err, "failed to get cluster config")
-	}
-
-	clientset, err := kubernetes.NewForConfig(cfg)
-	if err != nil {
-		return nil, errors.Wrap(err, "failed to create clientset")
-	}
-
-	secret, err := clientset.CoreV1().Secrets(os.Getenv("POD_NAMESPACE")).Get(context.TODO(), "kotsadm-s3", metav1.GetOptions{})
-	if err != nil {
-		if kuberneteserrors.IsNotFound(err) {
-			return nil, nil
-		}
-		return nil, errors.Wrap(err, "failed to get secret")
-	}
-
-	return secret, nil
-}
diff --git a/kotsadm/pkg/online/online.go b/kotsadm/pkg/online/online.go
index 54902f5eb..4ad20fbfc 100644
--- a/kotsadm/pkg/online/online.go
+++ b/kotsadm/pkg/online/online.go
@@ -16,8 +16,6 @@ import (
 	"github.com/replicatedhq/kots/kotsadm/pkg/task"
 	"github.com/replicatedhq/kots/kotsadm/pkg/updatechecker"
 	"github.com/replicatedhq/kots/kotsadm/pkg/version"
-	"github.com/replicatedhq/kots/kotsadm/pkg/downstream"
-	kotsadmconfig "github.com/replicatedhq/kots/kotsadm/pkg/config"
 	"github.com/replicatedhq/kots/pkg/pull"
 	"go.uber.org/zap"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -32,7 +30,7 @@ type PendingApp struct {
 	LicenseData string
 }
 
-func CreateAppFromOnline(pendingApp *PendingApp, upstreamURI string, isAutomated bool) (_ *kotsutil.KotsKinds, finalError error) {
+func CreateAppFromOnline(pendingApp *PendingApp, upstreamURI string) (_ *kotsutil.KotsKinds, finalError error) {
 	logger.Debug("creating app from online",
 		zap.String("upstreamURI", upstreamURI))
 
@@ -58,20 +56,20 @@ func CreateAppFromOnline(pendingApp *PendingApp, upstreamURI string, isAutomated
 	defer func() {
 		if finalError == nil {
 			if err := task.ClearTaskStatus("online-install"); err != nil {
-				logger.Error(errors.Wrap(err, "failed to clear install task status"))
+				logger.Error(errors.Wrap(err, "faild to clear install task status"))
 			}
 			if err := setAppInstallState(pendingApp.ID, "installed"); err != nil {
-				logger.Error(errors.Wrap(err, "failed to set app status to installed"))
+				logger.Error(errors.Wrap(err, "faild to set app status to installed"))
 			}
 			if err := updatechecker.Configure(pendingApp.ID); err != nil {
-				logger.Error(errors.Wrap(err, "failed to configure update checker"))
+				logger.Error(errors.Wrap(err, "faild to configure update checker"))
 			}
 		} else {
 			if err := task.SetTaskStatus("online-install", finalError.Error(), "failed"); err != nil {
-				logger.Error(errors.Wrap(err, "failed to set error on install task status"))
+				logger.Error(errors.Wrap(err, "faild to set error on install task status"))
 			}
 			if err := setAppInstallState(pendingApp.ID, "install_error"); err != nil {
-				logger.Error(errors.Wrap(err, "failed to set app status to error"))
+				logger.Error(errors.Wrap(err, "faild to set app status to error"))
 			}
 		}
 	}()
@@ -195,45 +193,15 @@ func CreateAppFromOnline(pendingApp *PendingApp, upstreamURI string, isAutomated
 		return nil, errors.Wrap(err, "failed to create app version archive")
 	}
 
+	if err := preflight.Run(pendingApp.ID, newSequence, tmpRoot); err != nil {
+		return nil, errors.Wrap(err, "failed to start preflights")
+	}
+
 	kotsKinds, err := kotsutil.LoadKotsKindsFromPath(tmpRoot)
 	if err != nil {
 		return nil, errors.Wrap(err, "failed to load kotskinds from path")
 	}
 
-	if isAutomated && kotsKinds.Config != nil {
-		// bypass the config screen if no configuration is required
-		licenseSpec, err := kotsKinds.Marshal("kots.io", "v1beta1", "License")
-		if err != nil {
-			return nil, errors.Wrap(err, "failed to marshal license spec")
-		}
-
-		configSpec, err := kotsKinds.Marshal("kots.io", "v1beta1", "Config")
-		if err != nil {
-			return nil, errors.Wrap(err, "failed to marshal config spec")
-		}
-
-		configValuesSpec, err := kotsKinds.Marshal("kots.io", "v1beta1", "ConfigValues")
-		if err != nil {
-			return nil, errors.Wrap(err, "failed to marshal configvalues spec")
-		}
-
-		needsConfig, err := kotsadmconfig.NeedsConfiguration(configSpec, configValuesSpec, licenseSpec)
-		if err != nil {
-			return nil, errors.Wrap(err, "failed to check if app needs configuration")
-		}
-
-		if !needsConfig {
-			err := downstream.SetDownstreamVersionPendingPreflight(pendingApp.ID, newSequence)
-			if err != nil {
-				return nil, errors.Wrap(err, "failed to set downstream version status to 'pending preflight'")
-			}
-		}
-	}
-
-	if err := preflight.Run(pendingApp.ID, newSequence, tmpRoot); err != nil {
-		return nil, errors.Wrap(err, "failed to start preflights")
-	}
-
 	return kotsKinds, nil
 }
 
diff --git a/kotsadm/pkg/preflight/execute.go b/kotsadm/pkg/preflight/execute.go
index 1129baa5e..7c25f7091 100644
--- a/kotsadm/pkg/preflight/execute.go
+++ b/kotsadm/pkg/preflight/execute.go
@@ -8,7 +8,6 @@ import (
 	"github.com/pkg/errors"
 	"github.com/replicatedhq/kots/kotsadm/pkg/logger"
 	"github.com/replicatedhq/kots/kotsadm/pkg/persistence"
-	"github.com/replicatedhq/kots/kotsadm/pkg/version"
 	troubleshootv1beta1 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta1"
 	"github.com/replicatedhq/troubleshoot/pkg/preflight"
 	troubleshootpreflight "github.com/replicatedhq/troubleshoot/pkg/preflight"
@@ -105,15 +104,6 @@ where app_id = $3 and parent_sequence = $4`
 		return errors.Wrap(err, "failed to write preflight results")
 	}
 
-	// deploy first version if preflight checks passed
-	preflightState := getPreflightState(uploadPreflightResults)
-	if sequence == 0 && preflightState == "pass" {
-		err := version.DeployVersion(appID, sequence)
-		if err != nil {
-			return errors.Wrap(err, "failed to deploy first version")
-		}
-	}
-
 	return nil
 }
 
@@ -124,24 +114,3 @@ func isPermissionsError(err error) bool {
 	}
 	return strings.Contains(err.Error(), "insufficient permissions to run all collectors")
 }
-
-func getPreflightState(preflightResults *troubleshootpreflight.UploadPreflightResults) string {
-	if len(preflightResults.Errors) > 0 {
-		return "fail"
-	}
-
-	if len(preflightResults.Results) == 0 {
-		return "pass"
-	}
-
-	state := "pass"
-	for _, result := range preflightResults.Results {
-		if result.IsFail {
-			return "fail"
-		} else if result.IsWarn {
-			state = "warn"
-		}
-	}
-
-	return state
-}
diff --git a/kotsadm/pkg/preflight/preflight.go b/kotsadm/pkg/preflight/preflight.go
index adc226a60..d6b3daa19 100644
--- a/kotsadm/pkg/preflight/preflight.go
+++ b/kotsadm/pkg/preflight/preflight.go
@@ -10,7 +10,6 @@ import (
 	"github.com/replicatedhq/kots/kotsadm/pkg/persistence"
 	registrytypes "github.com/replicatedhq/kots/kotsadm/pkg/registry/types"
 	"github.com/replicatedhq/kots/kotsadm/pkg/render"
-	"github.com/replicatedhq/kots/kotsadm/pkg/version"
 	"go.uber.org/zap"
 )
 
@@ -74,10 +73,6 @@ func Run(appID string, sequence int64, archiveDir string) error {
 
 			logger.Debug("preflight checks completed")
 		}()
-	} else if sequence == 0 {
-		if err := version.DeployVersion(appID, int64(sequence)); err != nil {
-			return errors.Wrap(err, "failed to deploy first version")
-		}
 	} else {
 		if err := downstream.SetDownstreamVersionReady(appID, int64(sequence)); err != nil {
 			return errors.Wrap(err, "failed to set downstream version ready")
diff --git a/kotsadm/pkg/registry/registry.go b/kotsadm/pkg/registry/registry.go
index 71b954997..4bb23e250 100644
--- a/kotsadm/pkg/registry/registry.go
+++ b/kotsadm/pkg/registry/registry.go
@@ -9,7 +9,6 @@ import (
 	"io"
 	"os"
 	"path/filepath"
-	"strings"
 	"time"
 
 	"github.com/pkg/errors"
@@ -24,7 +23,6 @@ import (
 	"github.com/replicatedhq/kots/kotsadm/pkg/version"
 	kotsv1beta1 "github.com/replicatedhq/kots/kotskinds/apis/kots/v1beta1"
 	"github.com/replicatedhq/kots/pkg/crypto"
-	"github.com/replicatedhq/kots/pkg/kotsadm"
 	"github.com/replicatedhq/kots/pkg/rewrite"
 	"go.uber.org/zap"
 	corev1 "k8s.io/api/core/v1"
@@ -268,39 +266,3 @@ func HasKurlRegistry() (bool, error) {
 
 	return false, nil
 }
-
-func GetKotsadmRegistry() (*types.RegistrySettings, error) {
-	cfg, err := config.GetConfig()
-	if err != nil {
-		return nil, errors.Wrap(err, "failed to get config")
-	}
-
-	clientset, err := kubernetes.NewForConfig(cfg)
-	if err != nil {
-		return nil, errors.Wrap(err, "failed to get client set")
-	}
-
-	namespace := os.Getenv("POD_NAMESPACE")
-
-	kotsadmOptions, err := kotsadm.GetKotsadmOptionsFromCluster(namespace, clientset)
-	if err != nil {
-		return nil, errors.Wrap(err, "failed to get kotsadm options from cluster")
-	}
-
-	registry := kotsadmOptions.OverrideRegistry
-	registryNamespace := kotsadmOptions.OverrideNamespace
-	hostParts := strings.Split(kotsadmOptions.OverrideRegistry, "/")
-	if len(hostParts) == 2 {
-		registry = hostParts[0]
-		registryNamespace = hostParts[1]
-	}
-
-	registrySettings := types.RegistrySettings{
-		Hostname:  registry,
-		Namespace: registryNamespace,
-		Username:  kotsadmOptions.Username,
-		Password:  kotsadmOptions.Password,
-	}
-
-	return &registrySettings, nil
-}
diff --git a/kotsadm/pkg/registry/types/types.go b/kotsadm/pkg/registry/types/types.go
index ca0ed514a..78fddca9f 100644
--- a/kotsadm/pkg/registry/types/types.go
+++ b/kotsadm/pkg/registry/types/types.go
@@ -4,6 +4,5 @@ type RegistrySettings struct {
 	Hostname    string
 	Username    string
 	PasswordEnc string
-	Password    string
 	Namespace   string
 }
diff --git a/kotsadm/pkg/snapshot/store.go b/kotsadm/pkg/snapshot/store.go
index 43b197297..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"
 
@@ -21,7 +20,6 @@ import (
 	"github.com/aws/aws-sdk-go/aws/session"
 	"github.com/aws/aws-sdk-go/service/s3"
 	"github.com/pkg/errors"
-	"github.com/replicatedhq/kots/kotsadm/pkg/kurl"
 	"github.com/replicatedhq/kots/kotsadm/pkg/logger"
 	"github.com/replicatedhq/kots/kotsadm/pkg/snapshot/providers"
 	"github.com/replicatedhq/kots/kotsadm/pkg/snapshot/types"
@@ -79,9 +77,9 @@ func UpdateGlobalStore(store *types.Store) (*velerov1.BackupStorageLocation, err
 			zap.String("accessKeyId", store.AWS.AccessKeyID),
 			zap.Bool("useInstanceRole", store.AWS.UseInstanceRole))
 
-		kotsadmVeleroBackendStorageLocation.Spec.Config = map[string]string{
-			"region": store.AWS.Region,
-		}
+		kotsadmVeleroBackendStorageLocation.Spec.Config["region"] = store.AWS.Region
+		// s3Url can be set by Other and conflicts with S3
+		delete(kotsadmVeleroBackendStorageLocation.Spec.Config, "s3Url")
 
 		if store.AWS.UseInstanceRole {
 			// delete the secret
@@ -151,77 +149,10 @@ func UpdateGlobalStore(store *types.Store) (*velerov1.BackupStorageLocation, err
 			}
 		}
 	} else if store.Other != nil {
-		kotsadmVeleroBackendStorageLocation.Spec.Config = map[string]string{
-			"region": store.Other.Region,
-			"s3Url": store.Other.Endpoint,
-		}
+		kotsadmVeleroBackendStorageLocation.Spec.Config["region"] = store.Other.Region
+		kotsadmVeleroBackendStorageLocation.Spec.Config["s3Url"] = store.Other.Endpoint
 
 		// create or update the secret
-	} else if store.Internal != nil {
-		kotsadmVeleroBackendStorageLocation.Spec.Config = map[string]string{
-			"region": store.Internal.Region,
-			"s3Url": store.Internal.Endpoint,
-			"publicUrl": fmt.Sprintf("http://%s", store.Internal.ObjectStoreClusterIP),
-			"s3ForcePathStyle": "true",
-		}
-
-		internalCfg := ini.Empty()
-		section, err := internalCfg.NewSection("default")
-		if err != nil {
-			return nil, errors.Wrap(err, "failed to create default section in internal creds")
-		}
-		_, err = section.NewKey("aws_access_key_id", store.Internal.AccessKeyID)
-		if err != nil {
-			return nil, errors.Wrap(err, "failed to create internal access key id")
-		}
-
-		_, err = section.NewKey("aws_secret_access_key", store.Internal.SecretAccessKey)
-		if err != nil {
-			return nil, errors.Wrap(err, "failed to create internal secret access key")
-		}
-
-		var internalCredentials bytes.Buffer
-		writer := bufio.NewWriter(&internalCredentials)
-		_, err = internalCfg.WriteTo(writer)
-		if err != nil {
-			return nil, errors.Wrap(err, "failed to write ini")
-		}
-		if err := writer.Flush(); err != nil {
-			return nil, errors.Wrap(err, "failed to flush buffer")
-		}
-
-		// create or update the secret
-		if kuberneteserrors.IsNotFound(currentSecretErr) {
-			// create
-			toCreate := corev1.Secret{
-				TypeMeta: metav1.TypeMeta{
-					APIVersion: "v1",
-					Kind:       "Secret",
-				},
-				ObjectMeta: metav1.ObjectMeta{
-					Name:      "cloud-credentials",
-					Namespace: kotsadmVeleroBackendStorageLocation.Namespace,
-				},
-				Data: map[string][]byte{
-					"cloud": internalCredentials.Bytes(),
-				},
-			}
-			_, err = clientset.CoreV1().Secrets(kotsadmVeleroBackendStorageLocation.Namespace).Create(context.TODO(), &toCreate, metav1.CreateOptions{})
-			if err != nil {
-				return nil, errors.Wrap(err, "failed to create internal secret")
-			}
-		} else {
-			// update
-			if currentSecret.Data == nil {
-				currentSecret.Data = map[string][]byte{}
-			}
-
-			currentSecret.Data["cloud"] = internalCredentials.Bytes()
-			_, err = clientset.CoreV1().Secrets(kotsadmVeleroBackendStorageLocation.Namespace).Update(context.TODO(), currentSecret, metav1.UpdateOptions{})
-			if err != nil {
-				return nil, errors.Wrap(err, "failed to update internal secret")
-			}
-		}
 	} else if store.Google != nil {
 		if store.Google.UseInstanceRole {
 			kotsadmVeleroBackendStorageLocation.Spec.Config["serviceAccount"] = store.Google.ServiceAccount
@@ -254,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
@@ -265,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")
 				}
 			}
 		}
@@ -301,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
@@ -312,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")
 			}
 		}
 	}
@@ -378,21 +309,9 @@ func GetGlobalStore(kotsadmVeleroBackendStorageLocation *velerov1.BackupStorageL
 	case "aws":
 		endpoint, isS3Compatible := kotsadmVeleroBackendStorageLocation.Spec.Config["s3Url"]
 		if isS3Compatible {
-			s3Secret, err := kurl.GetS3Secret()
-			if err != nil {
-				return nil, errors.Wrap(err, "failed to get s3 secret")
-			}
-			if s3Secret != nil && string(s3Secret.Data["endpoint"]) == endpoint {
-				store.Internal = &types.StoreInternal{
-					Region:               kotsadmVeleroBackendStorageLocation.Spec.Config["region"],
-					Endpoint:             endpoint,
-					ObjectStoreClusterIP: string(s3Secret.Data["object-store-cluster-ip"]),
-				}
-			} else {
-				store.Other = &types.StoreOther{
-					Region:   kotsadmVeleroBackendStorageLocation.Spec.Config["region"],
-					Endpoint: endpoint,
-				}
+			store.Other = &types.StoreOther{
+				Region:   kotsadmVeleroBackendStorageLocation.Spec.Config["region"],
+				Endpoint: endpoint,
 			}
 		} else {
 			store.AWS = &types.StoreAWS{
@@ -417,13 +336,10 @@ func GetGlobalStore(kotsadmVeleroBackendStorageLocation *velerov1.BackupStorageL
 
 			for _, section := range awsCfg.Sections() {
 				if section.Name() == "default" {
-					if store.Internal != nil {
-						store.Internal.AccessKeyID = section.Key("aws_access_key_id").Value()
-						store.Internal.SecretAccessKey = section.Key("aws_secret_access_key").Value()
-					} else if store.Other != nil {
+					if isS3Compatible {
 						store.Other.AccessKeyID = section.Key("aws_access_key_id").Value()
 						store.Other.SecretAccessKey = section.Key("aws_secret_access_key").Value()
-					} else if store.AWS != nil {
+					} else {
 						store.AWS.AccessKeyID = section.Key("aws_access_key_id").Value()
 						store.AWS.SecretAccessKey = section.Key("aws_secret_access_key").Value()
 					}
@@ -540,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")
 }
 
@@ -698,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
@@ -747,17 +630,5 @@ func Redact(store *types.Store) error {
 		}
 	}
 
-	if store.Other != nil {
-		if store.Other.SecretAccessKey != "" {
-			store.Other.SecretAccessKey = "--- REDACTED ---"
-		}
-	}
-
-	if store.Internal != nil {
-		if store.Internal.SecretAccessKey != "" {
-			store.Internal.SecretAccessKey = "--- REDACTED ---"
-		}
-	}
-
 	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/pkg/version/version.go b/kotsadm/pkg/version/version.go
index 048bf149a..c906e7e35 100644
--- a/kotsadm/pkg/version/version.go
+++ b/kotsadm/pkg/version/version.go
@@ -183,20 +183,42 @@ backup_spec = EXCLUDED.backup_spec`
 	for _, d := range downstreams {
 		downstreamStatus := "pending"
 		if currentSequence == nil && kotsKinds.Config != nil {
-			downstreamStatus = "pending_config"
-		} else if kotsKinds.Preflight != nil {
-			downstreamStatus = "pending_preflight"
-		}
+			configSpec, err := kotsKinds.Marshal("kots.io", "v1beta1", "Config")
+			if err != nil {
+				return int64(0), errors.Wrap(err, "failed to render config")
+			}
 
-		if currentSequence != nil {
-			// check if version needs additional configuration
-			t, err := config.NeedsConfiguration(configSpec, configValuesSpec, licenseSpec)
+			configValues := ""
+			if kotsKinds.ConfigValues != nil {
+				configValuesSpec, err := kotsKinds.Marshal("kots.io", "v1beta1", "ConfigValues")
+				if err != nil {
+					return int64(0), errors.Wrap(err, "failed to render config values")
+				}
+
+				configValues = string(configValuesSpec)
+			}
+
+			license := ""
+			if kotsKinds.License != nil {
+				licenseSpec, err := kotsKinds.Marshal("kots.io", "v1beta1", "License")
+				if err != nil {
+					return int64(0), errors.Wrap(err, "failed to render license")
+				}
+
+				license = string(licenseSpec)
+			}
+			needsConfig, err := config.NeedsConfiguration(string(configSpec), configValues, license)
 			if err != nil {
-				return int64(0), errors.Wrap(err, "failed to check if version needs configuration")
+				return int64(0), errors.Wrap(err, "failed to check if app needs configuration")
 			}
-			if t {
+
+			if needsConfig || configValues == "" {
 				downstreamStatus = "pending_config"
+			} else if kotsKinds.Preflight != nil {
+				downstreamStatus = "pending_preflight"
 			}
+		} else if kotsKinds.Preflight != nil {
+			downstreamStatus = "pending_preflight"
 		}
 
 		diffSummary := ""
@@ -211,9 +233,19 @@ backup_spec = EXCLUDED.backup_spec`
 				return int64(0), errors.Wrap(err, "failed to marshal diff")
 			}
 			diffSummary = string(b)
+
+			// check if version needs additional configuration
+			t, err := config.NeedsConfiguration(configSpec, configValuesSpec, licenseSpec)
+			if err != nil {
+				return int64(0), errors.Wrap(err, "failed to check if version needs configuration")
+			}
+			if t {
+				downstreamStatus = "pending_config"
+			}
 		}
 
 		commitURL := ""
+
 		downstreamGitOps, err := gitops.GetDownstreamGitOps(appID, d.ClusterID)
 		if err != nil {
 			return int64(0), errors.Wrap(err, "failed to get downstream gitops")
@@ -227,6 +259,7 @@ backup_spec = EXCLUDED.backup_spec`
 			if err != nil {
 				return int64(0), errors.Wrap(err, "failed to create gitops commit")
 			}
+
 			commitURL = createdCommitURL
 		}
 
diff --git a/kotsadm/skaffold.yaml b/kotsadm/skaffold.yaml
index 78e2780d6..06173bb50 100644
--- a/kotsadm/skaffold.yaml
+++ b/kotsadm/skaffold.yaml
@@ -57,11 +57,6 @@ build:
       docker:
         dockerfile: ./Dockerfile.skaffold
 
-    - image: kotsadm-migrations
-      context: "./migrations"
-      docker:
-        dockerfile: ./Dockerfile.skaffold
-
 deploy:
   statusCheckDeadlineSeconds: 240
   kustomize:
diff --git a/kotsadm/web/src/components/AirgapUploadProgress.jsx b/kotsadm/web/src/components/AirgapUploadProgress.jsx
index a93ef4570..b1f510e4b 100644
--- a/kotsadm/web/src/components/AirgapUploadProgress.jsx
+++ b/kotsadm/web/src/components/AirgapUploadProgress.jsx
@@ -55,7 +55,7 @@ class AirgapUploadProgress extends React.Component {
 
     if (total > 0 && sent > 0) {
       uploadComplete = sent === total
-      percentage = (sent / total * 100).toFixed(2) + "%";
+      percentage = (sent / total * 100).toFixed(1) + "%";
       progressBar = (
         <div className={`progressbar ${smallSize ? "small" : ""}`}>
           <div className={`progressbar-meter ${uploadComplete ? "complete" : ""}`} style={{ width: `${(sent / total) * (smallSize ? 100 : 600)}px` }} />
@@ -84,16 +84,13 @@ class AirgapUploadProgress extends React.Component {
     } catch {
       // empty
     }
-    
+
     let statusDiv = (
       <div
-        className={`u-marginTop--20 u-fontWeight--medium u-lineHeight--medium u-textAlign--center`}
+        className={`u-marginTop--20 u-color--dustyGray u-fontWeight--medium u-lineHeight--medium u-textAlign--center`}
       >
-        <div className="flex flex1 u-marginBottom--10 justifyContent--center alignItems--center u-color--tundora">
-          {statusMsg && <Loader className="flex u-marginRight--5" size="24" />}
-          <p>{statusMsg}</p>
-        </div>
-        <p className="u-color--dustyGray">This may take a while depending on your network connection and size of your bundle</p>
+        <p className="u-marginBottom--5">{statusMsg}</p>
+        <p>This may take a while depending on your network connection and size of your bundle</p>
       </div>
     );
 
diff --git a/kotsadm/web/src/components/PreflightResultPage.jsx b/kotsadm/web/src/components/PreflightResultPage.jsx
index 4c86ff819..7c07e5522 100644
--- a/kotsadm/web/src/components/PreflightResultPage.jsx
+++ b/kotsadm/web/src/components/PreflightResultPage.jsx
@@ -38,15 +38,14 @@ class PreflightResultPage extends Component {
 
       const preflightResults = JSON.parse(preflightResultData?.result);
       const preflightState = getPreflightResultState(preflightResults);
-      if (preflightState !== "pass") {
-        if (!force) {
-          this.showWarningModal();
-          return;
-        }
-        const sequence = match.params.sequence ? parseInt(match.params.sequence, 10) : 0;
-        await this.props.deployKotsVersion(preflightResultData.appSlug, sequence, preflightResultData.clusterSlug);
+      if (preflightState !== "pass" && !force) {
+        this.showWarningModal();
+        return;
       }
 
+      const sequence = match.params.sequence ? parseInt(match.params.sequence, 10) : 0;
+      await this.props.deployKotsVersion(preflightResultData.appSlug, sequence, preflightResultData.clusterSlug);
+
       history.push(`/app/${preflightResultData.appSlug}/version-history`);
     } catch (error) {
       console.log(error);
diff --git a/kotsadm/web/src/components/UploadAirgapBundle.jsx b/kotsadm/web/src/components/UploadAirgapBundle.jsx
index 3f503602c..d9f8bf422 100644
--- a/kotsadm/web/src/components/UploadAirgapBundle.jsx
+++ b/kotsadm/web/src/components/UploadAirgapBundle.jsx
@@ -11,6 +11,8 @@ import AirgapUploadProgress from "@src/components/AirgapUploadProgress";
 import LicenseUploadProgress from "./LicenseUploadProgress";
 import AirgapRegistrySettings from "./shared/AirgapRegistrySettings";
 import { Utilities } from "../utilities/utilities";
+import Loader from "./shared/Loader";
+import { validateRegistryInfo } from "../queries/UserQueries";
 import { getSupportBundleCommand } from "../queries/TroubleshootQueries";
 import { getKotsApp } from "../queries/AppsQueries";
 
@@ -107,43 +109,33 @@ class UploadAirgapBundle extends React.Component {
         });
         return;
       }
-
-      let res;
       try {
-        res = await fetch(`${window.env.API_ENDPOINT}/app/${slug}/registry/validate`, {
-          method: "POST",
-          headers: {
-            "Authorization": Utilities.getToken(),
-            "Content-Type": "application/json",
-          },
-          body: JSON.stringify({
-            hostname: this.state.registryDetails.hostname,
-            namespace: this.state.registryDetails.namespace,
+        const validated = await this.props.client.query({
+          query: validateRegistryInfo,
+          variables: {
+            slug: slug,
+            endpoint: this.state.registryDetails.hostname,
             username: this.state.registryDetails.username,
-            password: this.state.registryDetails.password,          
-          }),
-        });
-      } catch(err) {
-        this.setState({
-          fileUploading: false,
-          uploadSent: 0,
-          uploadTotal: 0,
-          errorMessage: err,
+            password: this.state.registryDetails.password,
+            org: this.state.registryDetails.namespace,
+          }
         });
-        return;
-      }
-
-      const response = await res.json();
-      if (!response.success) {
-        let msg = "An error occurred while uploading your airgap bundle. Please try again";
-        if (response.error) {
-          msg = response.error;
+        if (validated.data.validateRegistryInfo) {
+          this.setState({
+            fileUploading: false,
+            uploadSent: 0,
+            uploadTotal: 0,
+            errorMessage: validated.data.validateRegistryInfo,
+          });
+          return;
         }
+      } catch (error) {
+        console.error(error);
         this.setState({
           fileUploading: false,
           uploadSent: 0,
           uploadTotal: 0,
-          errorMessage: msg,
+          errorMessage: "An error occurred while uploading your airgap bundle. Please try again"
         });
         return;
       }
diff --git a/kotsadm/web/src/components/apps/AppSnapshotDetail.jsx b/kotsadm/web/src/components/apps/AppSnapshotDetail.jsx
index 0790a6c0f..cfe624264 100644
--- a/kotsadm/web/src/components/apps/AppSnapshotDetail.jsx
+++ b/kotsadm/web/src/components/apps/AppSnapshotDetail.jsx
@@ -280,8 +280,8 @@ class AppSnapshotDetail extends Component {
               </div>
             </div>
             <div className="flex flex-column justifyContent--flexEnd">
-              <p className="u-fontSize--small u-fontWeight--normal alignSelf--flexEnd u-marginBottom--8"><span className={`status-indicator ${hook.error ? "failed" : "completed"} u-marginLeft--5`}>{hook.error ? "Failed" : "Completed"}</span></p>
-              {!hook.error &&
+              <p className="u-fontSize--small u-fontWeight--normal alignSelf--flexEnd u-marginBottom--8"><span className={`status-indicator ${hook.errors ? "failed" : "completed"} u-marginLeft--5`}>{hook.errors ? "Failed" : "Completed"}</span></p>
+              {!hook.errors &&
                 <p className="u-fontSize--small u-fontWeight--normal u-marginBottom--8"> Finished in {diffMinutes === 0 ? "less than a minute" : `${diffMinutes} minutes`} </p>}
               {hook.stderr !== "" || hook.stdout !== "" &&
                 <span className="replicated-link u-fontSize--small alignSelf--flexEnd" onClick={() => this.toggleScriptsOutput(hook)}> View output </span>}
@@ -533,7 +533,7 @@ class AppSnapshotDetail extends Component {
               </div>
             </div>
 
-            {(!isEmpty(snapshotDetails?.errors) || !isEmpty(snapshotDetails?.warnings)) &&
+            {!isEmpty(snapshotDetails?.errors) && !isEmpty(snapshotDetails?.warnings) &&
               <div className="flex flex-auto u-marginBottom--30">
                 <div className="flex-column flex1 u-marginRight--20">
                   <div className="dashboard-card-wrapper flex1">
diff --git a/kotsadm/web/src/components/shared/AirgapRegistrySettings.jsx b/kotsadm/web/src/components/shared/AirgapRegistrySettings.jsx
index 1884ccac8..74543a372 100644
--- a/kotsadm/web/src/components/shared/AirgapRegistrySettings.jsx
+++ b/kotsadm/web/src/components/shared/AirgapRegistrySettings.jsx
@@ -2,7 +2,8 @@ import React, { Component } from "react";
 import { withRouter } from "react-router-dom";
 import { graphql, compose, withApollo } from "react-apollo";
 import Loader from "../shared/Loader";
-import { getImageRewriteStatus } from "../../queries/AppsQueries";
+import { getAppRegistryDetails, getImageRewriteStatus } from "../../queries/AppsQueries";
+import { validateRegistryInfo } from "@src/queries/UserQueries";
 import { Repeater } from "../../utilities/repeater";
 import get from "lodash/get";
 import "../../scss/components/watches/WatchDetailPage.scss";
@@ -21,8 +22,6 @@ class AirgapRegistrySettings extends Component {
     } = props?.registryDetails || {};
 
     this.state = {
-      loading: false,
-
       hostname,
       username,
       password,
@@ -44,7 +43,6 @@ class AirgapRegistrySettings extends Component {
   }
 
   componentDidMount = () => {
-    this.fetchRegistryInfo();
     this.triggerStatusUpdates();
   }
 
@@ -89,55 +87,48 @@ class AirgapRegistrySettings extends Component {
       });
   }
 
-  testRegistryConnection = async () => {
+  testRegistryConnection = () => {
     this.setState({
       testInProgress: true,
       testMessage: "",
     });
 
     const { slug } = this.props.app;
-
-    let res;
-    try {
-      res = await fetch(`${window.env.API_ENDPOINT}/app/${slug}/registry/validate`, {
-        method: "POST",
-        headers: {
-          "Authorization": Utilities.getToken(),
-          "Content-Type": "application/json",
-        },
-        body: JSON.stringify({
-          hostname: this.state.hostname,
-          namespace: this.state.namespace,
-          username: this.state.username,
-          password: this.state.password,
-        }),
-      });
-    } catch(err) {
-      this.setState({
-        testInProgress: false,
-        testMessage: err,
-        testFailed: true,
-        lastSync: new Date(),
-      });
-      return;
-    }
-
-    const response = await res.json();
-    if (response.success) {
-      this.setState({
-        testInProgress: false,
-        testMessage: "Success!",
-        testFailed: false,
-        lastSync: new Date(),
-      });
-    } else {
-      this.setState({
-        testInProgress: false,
-        testMessage: response.error,
-        testFailed: true,
-        lastSync: new Date(),
+    this.props.client.query({
+      query: validateRegistryInfo,
+      variables: {
+        slug: slug,
+        endpoint: this.state.hostname,
+        username: this.state.username,
+        password: this.state.password,
+        org: this.state.namespace,
+      }
+    }).then(result => {
+      if (result.data.validateRegistryInfo) {
+        this.setState({
+          testInProgress: false,
+          testMessage: result.data.validateRegistryInfo,
+          testFailed: true,
+          lastSync: new Date(),
+        });
+      } else {
+        this.setState({
+          testInProgress: false,
+          testMessage: "Success!",
+          testFailed: false,
+          lastSync: new Date(),
+        });
+      }
+    }).catch(err => {
+      err.graphQLErrors.map(({ msg }) => {
+        this.setState({
+          testInProgress: false,
+          testMessage: msg,
+          testFailed: true,
+          lastSync: new Date(),
+        });
       });
-    }
+    });
   }
 
   handleFormChange = (field, val) => {
@@ -152,59 +143,17 @@ class AirgapRegistrySettings extends Component {
   }
 
   componentDidUpdate(lastProps) {
-    const { app } = this.props;
-
-    if (app?.slug !== lastProps.app?.slug) {
-      this.fetchRegistryInfo();
+    const { getKotsAppRegistryQuery, app } = this.props;
+    if (getKotsAppRegistryQuery?.getAppRegistryDetails && getKotsAppRegistryQuery?.getAppRegistryDetails !== lastProps.getKotsAppRegistryQuery?.getAppRegistryDetails) {
+      this.setState({
+        hostname: getKotsAppRegistryQuery.getAppRegistryDetails.registryHostname,
+        username: getKotsAppRegistryQuery.getAppRegistryDetails.registryUsername,
+        password: getKotsAppRegistryQuery.getAppRegistryDetails.registryPassword,
+        namespace: getKotsAppRegistryQuery.getAppRegistryDetails.namespace || app.slug,
+      })
     }
   }
 
-  fetchRegistryInfo = () => {
-    if (this.state.loading) {
-      return;
-    }
-
-    this.setState({loading: true});
-
-    let url = `${window.env.API_ENDPOINT}/registry`;
-    if (this.props.app) {
-      url = `${window.env.API_ENDPOINT}/app/${this.props.app.slug}/registry`;
-    }
-
-    fetch(url, {
-      headers: {
-        "Authorization": Utilities.getToken()
-      },
-      method: "GET",
-    })
-    .then(res => res.json())
-    .then(result => {
-      this.setState({loading: false});
-      if (result.success) {
-        this.setState({
-          hostname: result.hostname,
-          username: result.username,
-          password: result.password,
-          namespace: result.namespace,
-        });
-
-        if (this.props.gatherDetails) {
-          const { hostname, username, password, namespace } = result;
-          this.props.gatherDetails({ hostname, username, password, namespace });
-        }
-  
-      } else {
-        // TODO: show error on UI
-        console.log(result);
-      }
-    })
-    .catch(err => {
-      this.setState({loading: false});
-        // TODO: show error on UI
-        console.log(err);
-    })
-  }
-
   triggerStatusUpdates = () => {
     this.props.client.query({
       query: getImageRewriteStatus,
@@ -250,7 +199,7 @@ class AirgapRegistrySettings extends Component {
   }
 
   render() {
-    const { app, hideTestConnection, hideCta, namespaceDescription, showRequiredFields, showHostnameAsRequired, showNamespaceAsRequired } = this.props;
+    const { app, getKotsAppRegistryQuery, hideTestConnection, hideCta, namespaceDescription, showRequiredFields, showHostnameAsRequired, showNamespaceAsRequired } = this.props;
     const { hostname, password, username, namespace, lastSync, testInProgress, testFailed, testMessage } = this.state;
     const { rewriteMessage, rewriteStatus } = this.state;
 
@@ -266,7 +215,7 @@ class AirgapRegistrySettings extends Component {
       // empty
     }
 
-    if (this.state.loading) {
+    if (getKotsAppRegistryQuery?.loading) {
       return (
         <div className="flex-column flex1 alignItems--center justifyContent--center">
           <Loader size="60" />
@@ -365,4 +314,22 @@ class AirgapRegistrySettings extends Component {
 export default compose(
   withRouter,
   withApollo,
+  graphql(getAppRegistryDetails, {
+    name: "getKotsAppRegistryQuery",
+    skip: props => {
+      if (!props.app) {
+        return true;
+      }
+      return false;
+    },
+    options: props => {
+      const { slug } = props.match.params;
+      return {
+        fetchPolicy: "no-cache",
+        variables: {
+          slug: slug
+        }
+      }
+    }
+  }),
 )(AirgapRegistrySettings);
diff --git a/kotsadm/web/src/components/snapshots/SnapshotStorageDestination.jsx b/kotsadm/web/src/components/snapshots/SnapshotStorageDestination.jsx
index f6c859ce8..051f56c57 100644
--- a/kotsadm/web/src/components/snapshots/SnapshotStorageDestination.jsx
+++ b/kotsadm/web/src/components/snapshots/SnapshotStorageDestination.jsx
@@ -25,10 +25,6 @@ const DESTINATIONS = [
   {
     value: "other",
     label: "Other S3-Compatible Storage",
-  },
-  {
-    value: "internal",
-    label: "Internal Storage (Default)",
   }
 ];
 
@@ -204,7 +200,7 @@ class SnapshotStorageDestination extends Component {
     const { store } = snapshotSettings;
 
 
-    if (store?.aws) {
+    if (store?.provider === "aws") {
       return this.setState({
         determiningDestination: false,
         selectedDestination: find(DESTINATIONS, ["value", "aws"]),
@@ -217,7 +213,7 @@ class SnapshotStorageDestination extends Component {
       });
     }
 
-    if (store?.azure) {
+    if (store?.provider === "azure") {
       return this.setState({
         determiningDestination: false,
         selectedDestination: find(DESTINATIONS, ["value", "azure"]),
@@ -233,7 +229,7 @@ class SnapshotStorageDestination extends Component {
       });
     }
 
-    if (store?.gcp) {
+    if (store?.provider === "gcp") {
       return this.setState({
         determiningDestination: false,
         selectedDestination: find(DESTINATIONS, ["value", "gcp"]),
@@ -245,7 +241,7 @@ class SnapshotStorageDestination extends Component {
       });
     }
 
-    if (store?.other) {
+    if (store?.provider === "other") {
       return this.setState({
         determiningDestination: false,
         selectedDestination: find(DESTINATIONS, ["value", "other"]),
@@ -257,14 +253,6 @@ class SnapshotStorageDestination extends Component {
         s3CompatibleRegion: store?.other?.region
       });
     }
-
-    if (store?.internal) {
-      return this.setState({
-        determiningDestination: false,
-        selectedDestination: find(DESTINATIONS, ["value", "internal"])
-      });
-    }
-
     // if nothing exists yet, we've determined default state is good
     this.setState({
       determiningDestination: false,
@@ -309,9 +297,6 @@ class SnapshotStorageDestination extends Component {
       case "other":
         await this.snapshotProviderS3Compatible();
         break;
-      case "internal":
-        await this.snapshotProviderInternal();
-        break;
     }
   }
 
@@ -343,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}`} />;
@@ -359,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>
     );
@@ -590,11 +570,6 @@ class SnapshotStorageDestination extends Component {
           </div>
         )
 
-      case "internal":
-        return (
-          null
-        )
-
       default:
         return (
           <div>No snapshot destination is selected</div>
@@ -627,7 +602,7 @@ class SnapshotStorageDestination extends Component {
             if (snapshotSettings.isKurl) {
               availableDestinations.push({
                 value: "internal",
-                label: "Internal Storage (Default)",
+                label: "Internal Storage",
               });
             }
             break;
@@ -678,27 +653,18 @@ class SnapshotStorageDestination extends Component {
               {!snapshotSettings?.isVeleroRunning &&
                 <div className="flex u-fontWeight--bold u-fontSize--small u-color--red u-marginBottom--10"> Please fix Velero so that the deployment is running. <a href="https://kots.io/kotsadm/snapshots/troubleshooting/" target="_blank" rel="noopener noreferrer" className="replicated-link u-marginLeft--5">View docs</a>  </div>}
               <div className="flex1">
-                {availableDestinations.length > 1 ?
-                  <Select
-                    className="replicated-select-container"
-                    classNamePrefix="replicated-select"
-                    placeholder="Select unit"
-                    options={availableDestinations}
-                    isSearchable={false}
-                    getOptionLabel={(destination) => this.getDestinationLabel(destination, destination.label)}
-                    getOptionValue={(destination) => destination.label}
-                    value={selectedDestination}
-                    onChange={this.handleDestinationChange}
-                    isOptionSelected={(option) => { option.value === selectedDestination }}
-                  />
-                  : 
-                  availableDestinations.length === 1 ?
-                    <div className="u-color--tuna u-fontWeight--medium flex alignItems--center">
-                      {this.getDestinationLabel(availableDestinations[0], availableDestinations[0].label)}
-                    </div>
-                    :
-                    null
-                }
+                <Select
+                  className="replicated-select-container"
+                  classNamePrefix="replicated-select"
+                  placeholder="Select unit"
+                  options={availableDestinations}
+                  isSearchable={false}
+                  getOptionLabel={(destination) => this.getDestinationLabel(destination, destination.label)}
+                  getOptionValue={(destination) => destination.label}
+                  value={selectedDestination}
+                  onChange={this.handleDestinationChange}
+                  isOptionSelected={(option) => { option.value === selectedDestination }}
+                />
               </div>
             </div>
             {!this.state.determiningDestination &&
diff --git a/kotsadm/web/src/components/troubleshoot/AnalyzerFileTree.jsx b/kotsadm/web/src/components/troubleshoot/AnalyzerFileTree.jsx
index 361b6d920..7edbd14d6 100644
--- a/kotsadm/web/src/components/troubleshoot/AnalyzerFileTree.jsx
+++ b/kotsadm/web/src/components/troubleshoot/AnalyzerFileTree.jsx
@@ -29,6 +29,7 @@ class AnalyzerFileTree extends React.Component {
       fileLoadErr: false,
       fileLoadErrMessage: "",
       activeMarkers: [],
+      analysisError: false
     };
   }
 
@@ -181,18 +182,31 @@ class AnalyzerFileTree extends React.Component {
     }
   }
 
+  reAnalyzeBundle = () => {
+    this.setState({ isReanalyzing: true });
+    this.props.reAnalyzeBundle((response, analysisError) => {
+      this.setState({ isReanalyzing: false, analysisError });
+    });
+  }
+
   render() {
-    const { files, fileContents, selectedFile, fileLoadErr, fileLoadErrMessage, fileLoading } = this.state;
+    const { files, fileContents, selectedFile, fileLoadErr, fileLoadErrMessage, fileLoading, analysisError } = this.state;
     const fileToView = find(fileContents, ["key", selectedFile]);
     const format = getFileFormat(selectedFile);
     const isOld = files && has(files[0], "size");
 
+    const analysisErrorExists = analysisError && analysisError.graphQLErrors && analysisError.graphQLErrors.length;
+
     return (
       <div className="flex-column flex1 AnalyzerFileTree--wrapper">
         {!files || !files.length || isOld ?
           <div className="flex-column flex1 justifyContent--center alignItems--center u-textAlign--center">
             <p className="u-color--tuna u-fontSize--normal u-fontWeight--bold">We were unable to detect files from this Support Bundle</p>
             <p className="u-fontSize--small u-color--dustyGray u-fontWeight--medium u-marginTop--10">It's possible that this feature didn't exists when you uploaded the bundle, try re-analyzing it to have files detected.</p>
+            <div className="u-marginTop--20">
+              <button className="btn secondary" onClick={() => this.reAnalyzeBundle()} disabled={this.state.isReanalyzing}>{this.state.isReanalyzing ? "Re-analyzing" : "Re-analyze bundle"}</button>
+            </div>
+            {analysisErrorExists && <span style={{ maxWidth: 420 }} className="u-fontSize--small u-lineHeight--normal u-fontWeight--bold u-color--error u-marginTop--20 u-textAlign--center">{analysisError.graphQLErrors[0].message}</span>}
           </div>
           :
           <div className="flex flex1">
diff --git a/kotsadm/web/src/components/troubleshoot/AnalyzerInsights.jsx b/kotsadm/web/src/components/troubleshoot/AnalyzerInsights.jsx
index eb41473cb..335cb118b 100644
--- a/kotsadm/web/src/components/troubleshoot/AnalyzerInsights.jsx
+++ b/kotsadm/web/src/components/troubleshoot/AnalyzerInsights.jsx
@@ -12,6 +12,7 @@ export class AnalyzerInsights extends React.Component {
       insights: [],
       analyzing: false,
       filterTiles: "0",
+      hasAnalysisError: false
     };
   }
 
@@ -96,9 +97,20 @@ export class AnalyzerInsights extends React.Component {
     });
   }
 
+  reAnalyzeBundle = () => {
+    this.setState({ analyzing: true });
+    this.props.reAnalyzeBundle((_, hasAnalysisError) => {
+      this.setState({ analyzing: false, hasAnalysisError });
+    });
+  }
+
+  renderAnalysisError = () => {
+    return <span style={{ maxWidth: 420 }} className="u-fontSize--small u-fontWeight--bold u-color--red u-marginTop--20 u-textAlign--center">An error occured during analysis</span>;
+  }
+
   render() {
     const { insights, status } = this.props;
-    const { filterTiles, analyzing } = this.state;
+    const { filterTiles, analyzing, hasAnalysisError } = this.state;
     const filteredInsights = this.state.insights;
 
     let noInsightsNode;
@@ -109,6 +121,7 @@ export class AnalyzerInsights extends React.Component {
             <Loader size="40" />
             <p className="u-color--tuna u-fontSize--normal u-fontWeight--bold">We are still analyzing this Support Bundle</p>
             <p className="u-fontSize--small u-fontWeight--regular u-marginTop--10">This can tak up to a minute, you can refresh the page to see if your analysis is ready.</p>
+            {hasAnalysisError && this.renderAnalysisError()}
           </div>
         )
       } else {
@@ -117,6 +130,10 @@ export class AnalyzerInsights extends React.Component {
             <p className="u-color--tuna u-fontSize--normal u-fontWeight--bold">We were unable to surface any insights for this Support Bundle</p>
             <p className="u-fontSize--small u-fontWeight--regular u-marginTop--10">It's possible that the file that was uploaded was not a Replicated Support Bundle,<br />or that collection of OS or Docker stats was not enabled in your spec.</p>
             <p className="u-fontSize--small u-fontWeight--regular u-marginTop--10">We're adding new bundle analyzers all the time, so check back soon.</p>
+            <div className="u-marginTop--20">
+              <button className="btn secondary" onClick={() => this.reAnalyzeBundle()} disabled={analyzing}>{analyzing ? "Re-analyzing" : "Re-analyze bundle"}</button>
+            </div>
+            {hasAnalysisError && this.renderAnalysisError()}
           </div>
         )
       }
@@ -179,6 +196,12 @@ export class AnalyzerInsights extends React.Component {
                 </div>
               </div>
             }
+            <div className="flex-column flex-auto u-paddingTop--20">
+              <div className="flex-auto u-paddingLeft--10">
+                <button className="btn secondary" onClick={() => this.reAnalyzeBundle()} disabled={analyzing}>{analyzing ? "Re-analyzing" : "Re-analyze bundle"}</button>
+              </div>
+              {hasAnalysisError && this.renderAnalysisError()}
+            </div>
           </div>
         }
       </div>
diff --git a/kotsadm/web/src/components/troubleshoot/SupportBundleAnalysis.jsx b/kotsadm/web/src/components/troubleshoot/SupportBundleAnalysis.jsx
index f3c7828ec..4ea9ae3ae 100644
--- a/kotsadm/web/src/components/troubleshoot/SupportBundleAnalysis.jsx
+++ b/kotsadm/web/src/components/troubleshoot/SupportBundleAnalysis.jsx
@@ -21,6 +21,26 @@ export class SupportBundleAnalysis extends React.Component {
     };
   }
 
+  reAnalyzeBundle = async (callback) => {
+    try {
+      const bundleId = this.props.getSupportBundle.getSupportBundle.id;
+      const bundleUrl = `${window.env.API_ENDPOINT}/troubleshoot/analyzebundle/${bundleId}`;
+
+      const response = await fetch(bundleUrl, {
+        method: "POST"
+      });
+      const analyzedBundle = await response.json();
+      if (callback && typeof callback === "function") {
+        callback(analyzedBundle, analyzedBundle.status === "analysis_error");
+      }
+      this.props.getSupportBundle.refetch();
+    } catch (error) {
+      if (callback && typeof callback === "function") {
+        callback(undefined, error);
+      }
+    }
+  }
+
   downloadBundle = async (bundle) => {
     fetch(`${window.env.API_ENDPOINT}/troubleshoot/supportbundle/${bundle.id}/download`, {
       method: "GET",
@@ -126,6 +146,7 @@ export class SupportBundleAnalysis extends React.Component {
                           status={bundle.status}
                           refetchSupportBundle={this.props.getSupportBundle.refetch}
                           insights={bundle.analysis?.insights}
+                          reAnalyzeBundle={this.reAnalyzeBundle}
                         />
                       } />
                       <Route exact path={fileTreeUrl} render={() =>
diff --git a/kotsadm/web/src/queries/AppsQueries.js b/kotsadm/web/src/queries/AppsQueries.js
index 3a4cf2fa0..307d32b1d 100644
--- a/kotsadm/web/src/queries/AppsQueries.js
+++ b/kotsadm/web/src/queries/AppsQueries.js
@@ -274,6 +274,19 @@ export const getKotsDownstreamHistoryRaw = `
 `;
 export const getKotsDownstreamHistory = gql(getKotsDownstreamHistoryRaw);
 
+export const getAppRegistryDetailsRaw = `
+  query getAppRegistryDetails($slug: String!) {
+    getAppRegistryDetails(slug: $slug) {
+      registryHostname
+      registryUsername
+      registryPassword
+      namespace
+      lastSyncedAt
+    }
+  }
+`;
+export const getAppRegistryDetails = gql(getAppRegistryDetailsRaw);
+
 export const getKotsPreflightResultRaw = `
   query getKotsPreflightResult($appSlug: String!, $clusterSlug: String!, $sequence: Int!) {
     getKotsPreflightResult(appSlug: $appSlug, clusterSlug: $clusterSlug, sequence: $sequence) {
diff --git a/kotsadm/web/src/queries/SnapshotQueries.js b/kotsadm/web/src/queries/SnapshotQueries.js
index 374e3f0d1..e0a2bd8b8 100644
--- a/kotsadm/web/src/queries/SnapshotQueries.js
+++ b/kotsadm/web/src/queries/SnapshotQueries.js
@@ -42,11 +42,11 @@ export const snapshotDetailRaw = `
         stderr
         started
         finished
-        warning {
+        warnings {
           title
           message
         }
-        error {
+        errors {
           title
           message
         }
diff --git a/kotsadm/web/src/queries/UserQueries.js b/kotsadm/web/src/queries/UserQueries.js
new file mode 100644
index 000000000..cdc91468c
--- /dev/null
+++ b/kotsadm/web/src/queries/UserQueries.js
@@ -0,0 +1,8 @@
+import gql from "graphql-tag";
+
+export const validateRegistryInfoRaw = `
+  query validateRegistryInfo($slug: String, $endpoint: String, $username: String, $password: String, $org: String) {
+    validateRegistryInfo(slug: $slug, endpoint: $endpoint, username: $username, password: $password, org: $org)
+  }
+`;
+export const validateRegistryInfo = gql(validateRegistryInfoRaw);
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/kotskinds/apis/kots/v1beta1/config_types.go b/kotskinds/apis/kots/v1beta1/config_types.go
index e4b014057..c682b3c89 100644
--- a/kotskinds/apis/kots/v1beta1/config_types.go
+++ b/kotskinds/apis/kots/v1beta1/config_types.go
@@ -60,7 +60,6 @@ type ConfigGroup struct {
 	Name        string       `json:"name"`
 	Title       string       `json:"title"`
 	Description string       `json:"description,omitempty"`
-	When        string       `json:"when,omitempty"`
 	Items       []ConfigItem `json:"items,omitempty"`
 }
 
diff --git a/pkg/docker/registry/registry.go b/pkg/docker/registry/registry.go
index c0ab7d1d3..aaa0c0e36 100644
--- a/pkg/docker/registry/registry.go
+++ b/pkg/docker/registry/registry.go
@@ -5,7 +5,6 @@ import (
 	"encoding/json"
 	"fmt"
 	"net/url"
-	"strings"
 
 	"github.com/pkg/errors"
 	kotsv1beta1 "github.com/replicatedhq/kots/kotskinds/apis/kots/v1beta1"
@@ -19,14 +18,6 @@ type RegistryProxyInfo struct {
 	Proxy    string
 }
 
-type DockercfgAuth struct {
-	Auth string `json:"auth,omitempty"`
-}
-
-type DockerCfgJSON struct {
-	Auths map[string]DockercfgAuth `json:"auths"`
-}
-
 func ProxyEndpointFromLicense(license *kotsv1beta1.License) *RegistryProxyInfo {
 	defaultInfo := &RegistryProxyInfo{
 		Registry: "registry.replicated.com",
@@ -66,12 +57,16 @@ func (r *RegistryProxyInfo) ToSlice() []string {
 }
 
 func PullSecretForRegistries(registries []string, username, password string, namespace string) (*corev1.Secret, error) {
-	dockercfgAuth := DockercfgAuth{
+	dockercfgAuth := struct {
+		Auth string `json:"auth,omitempty"`
+	}{
 		Auth: base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", username, password))),
 	}
 
-	dockerCfgJSON := DockerCfgJSON{
-		Auths: map[string]DockercfgAuth{},
+	dockerCfgJSON := struct {
+		Auths map[string]interface{} `json:"auths"`
+	}{
+		Auths: map[string]interface{}{},
 	}
 
 	for _, r := range registries {
@@ -101,28 +96,3 @@ func PullSecretForRegistries(registries []string, username, password string, nam
 
 	return secret, nil
 }
-
-func GetCredentialsForRegistry(configJson string, registry string) (string, string, error) {
-	dockerCfgJSON := DockerCfgJSON{}
-	err := json.Unmarshal([]byte(configJson), &dockerCfgJSON)
-	if err != nil {
-		return "", "", errors.Wrap(err, "failed to unmarshal config json")
-	}
-
-	auth, ok := dockerCfgJSON.Auths[registry]
-	if !ok {
-		return "", "", nil
-	}
-
-	decoded, err := base64.StdEncoding.DecodeString(auth.Auth)
-	if err != nil {
-		return "", "", errors.Wrap(err, "failed to decode auth string")
-	}
-
-	parts := strings.Split(string(decoded), ":")
-	if len(parts) != 2 {
-		return "", "", errors.Errorf("expected 2 parts in the string, but found %d", len(parts))
-	}
-
-	return parts[0], parts[1], nil
-}
diff --git a/pkg/download/download.go b/pkg/download/download.go
index 60ab66da7..7bcd6f321 100644
--- a/pkg/download/download.go
+++ b/pkg/download/download.go
@@ -89,7 +89,11 @@ func Download(appSlug string, path string, downloadOptions DownloadOptions) erro
 
 	if resp.StatusCode != http.StatusOK {
 		log.FinishSpinnerWithError()
-		return errors.Errorf("unexpected status code from %s: %s", url, resp.Status)
+		if resp.StatusCode == http.StatusNotFound {
+			return errors.Errorf("app with slug %s not found", appSlug)
+		} else {
+			return errors.Errorf("unexpected status code from %s: %s", url, resp.Status)
+		}
 	}
 
 	tmpFile, err := ioutil.TempFile("", "kots")
diff --git a/pkg/kotsadm/api_objects.go b/pkg/kotsadm/api_objects.go
index e98ab3556..6c45633f0 100644
--- a/pkg/kotsadm/api_objects.go
+++ b/pkg/kotsadm/api_objects.go
@@ -81,7 +81,7 @@ func apiClusterRoleBinding(serviceAccountNamespace string) *rbacv1.ClusterRoleBi
 	clusterRoleBinding := &rbacv1.ClusterRoleBinding{
 		TypeMeta: metav1.TypeMeta{
 			APIVersion: "rbac.authorization.k8s.io/v1",
-			Kind:       "ClusterRoleBinding",
+			Kind:       "CluserRoleBinding",
 		},
 		ObjectMeta: metav1.ObjectMeta{
 			Name:   "kotsadm-api-rolebinding",
@@ -183,7 +183,8 @@ func updateApiDeployment(deployment *appsv1.Deployment, deployOptions types.Depl
 		return errors.New("failed to find kotsadm-api container in deployment")
 	}
 
-	deployment.Spec.Template.Spec.Containers[containerIdx].Image = fmt.Sprintf("%s/kotsadm-api:%s", kotsadmRegistry(deployOptions.KotsadmOptions), kotsadmTag(deployOptions.KotsadmOptions))
+	// image
+	deployment.Spec.Template.Spec.Containers[containerIdx].Image = fmt.Sprintf("%s/kotsadm-api:%s", kotsadmRegistry(), kotsadmTag())
 
 	// 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
@@ -217,15 +218,6 @@ 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",
@@ -252,10 +244,9 @@ 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(deployOptions.KotsadmOptions), kotsadmTag(deployOptions.KotsadmOptions)),
+							Image:           fmt.Sprintf("%s/kotsadm-api:%s", kotsadmRegistry(), kotsadmTag()),
 							ImagePullPolicy: corev1.PullAlways,
 							Name:            "kotsadm-api",
 							Ports: []corev1.ContainerPort{
diff --git a/pkg/kotsadm/configmaps.go b/pkg/kotsadm/configmaps.go
deleted file mode 100644
index 7b124b3ca..000000000
--- a/pkg/kotsadm/configmaps.go
+++ /dev/null
@@ -1,55 +0,0 @@
-package kotsadm
-
-import (
-	"bytes"
-	"context"
-
-	"github.com/pkg/errors"
-	"github.com/replicatedhq/kots/pkg/kotsadm/types"
-	kuberneteserrors "k8s.io/apimachinery/pkg/api/errors"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/apimachinery/pkg/runtime/serializer/json"
-	"k8s.io/client-go/kubernetes"
-	"k8s.io/client-go/kubernetes/scheme"
-)
-
-func getConfigMapsYAML(deployOptions types.DeployOptions) (map[string][]byte, error) {
-	docs := map[string][]byte{}
-	s := json.NewYAMLSerializer(json.DefaultMetaFactory, scheme.Scheme, scheme.Scheme)
-
-	var configMap bytes.Buffer
-	if err := s.Encode(kotsadmConfigMap(deployOptions), &configMap); err != nil {
-		return nil, errors.Wrap(err, "failed to marshal minio config map")
-	}
-	docs["kotsadm-config.yaml"] = configMap.Bytes()
-
-	return docs, nil
-}
-
-func ensureKotsadmConfig(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")
-	}
-
-	if err := ensureConfigMaps(deployOptions, clientset); err != nil {
-		return errors.Wrap(err, "failed to ensure private kotsadm registry secret")
-	}
-
-	return nil
-}
-
-func ensureConfigMaps(deployOptions types.DeployOptions, clientset *kubernetes.Clientset) error {
-	_, err := clientset.CoreV1().ConfigMaps(deployOptions.Namespace).Get(context.TODO(), types.KotsadmConfigMap, metav1.GetOptions{})
-	if err != nil {
-		if !kuberneteserrors.IsNotFound(err) {
-			return errors.Wrap(err, "failed to get existing kotsadm config map")
-		}
-
-		_, err := clientset.CoreV1().ConfigMaps(deployOptions.Namespace).Create(context.TODO(), kotsadmConfigMap(deployOptions), metav1.CreateOptions{})
-		if err != nil {
-			return errors.Wrap(err, "failed to create kotsadm config map")
-		}
-	}
-
-	return nil
-}
diff --git a/pkg/kotsadm/configmaps_objects.go b/pkg/kotsadm/configmaps_objects.go
deleted file mode 100644
index 94bc09d44..000000000
--- a/pkg/kotsadm/configmaps_objects.go
+++ /dev/null
@@ -1,29 +0,0 @@
-package kotsadm
-
-import (
-	"github.com/replicatedhq/kots/pkg/kotsadm/types"
-	corev1 "k8s.io/api/core/v1"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-)
-
-func kotsadmConfigMap(deployOptions types.DeployOptions) *corev1.ConfigMap {
-	data := map[string]string{}
-	if kotsadmPullSecret(deployOptions.Namespace, deployOptions.KotsadmOptions) != nil {
-		data["kotsadm-registry"] = kotsadmRegistry(deployOptions.KotsadmOptions)
-	}
-
-	configMap := &corev1.ConfigMap{
-		TypeMeta: metav1.TypeMeta{
-			APIVersion: "v1",
-			Kind:       "ConfigMap",
-		},
-		ObjectMeta: metav1.ObjectMeta{
-			Name:      types.KotsadmConfigMap,
-			Namespace: deployOptions.Namespace,
-			Labels:    types.GetKotsadmLabels(),
-		},
-		Data: data,
-	}
-
-	return configMap
-}
diff --git a/pkg/kotsadm/kotsadm.go b/pkg/kotsadm/kotsadm.go
index 5a76a1ca7..eddb01c98 100644
--- a/pkg/kotsadm/kotsadm.go
+++ b/pkg/kotsadm/kotsadm.go
@@ -23,13 +23,13 @@ func getKotsadmYAML(deployOptions types.DeployOptions) (map[string][]byte, error
 	s := json.NewYAMLSerializer(json.DefaultMetaFactory, scheme.Scheme, scheme.Scheme)
 
 	var role bytes.Buffer
-	if err := s.Encode(kotsadmClusterRole(), &role); err != nil {
+	if err := s.Encode(kotsadmRole(deployOptions.Namespace), &role); err != nil {
 		return nil, errors.Wrap(err, "failed to marshal kotsadm role")
 	}
 	docs["kotsadm-role.yaml"] = role.Bytes()
 
 	var roleBinding bytes.Buffer
-	if err := s.Encode(kotsadmClusterRoleBinding(deployOptions.Namespace), &roleBinding); err != nil {
+	if err := s.Encode(kotsadmRoleBinding(deployOptions.Namespace), &roleBinding); err != nil {
 		return nil, errors.Wrap(err, "failed to marshal kotsadm role binding")
 	}
 	docs["kotsadm-rolebinding.yaml"] = roleBinding.Bytes()
diff --git a/pkg/kotsadm/kotsadm_objects.go b/pkg/kotsadm/kotsadm_objects.go
index a6cded47b..949ea228f 100644
--- a/pkg/kotsadm/kotsadm_objects.go
+++ b/pkg/kotsadm/kotsadm_objects.go
@@ -88,7 +88,7 @@ func kotsadmClusterRoleBinding(serviceAccountNamespace string) *rbacv1.ClusterRo
 	clusterRoleBinding := &rbacv1.ClusterRoleBinding{
 		TypeMeta: metav1.TypeMeta{
 			APIVersion: "rbac.authorization.k8s.io/v1",
-			Kind:       "ClusterRoleBinding",
+			Kind:       "CluserRoleBinding",
 		},
 		ObjectMeta: metav1.ObjectMeta{
 			Name:   "kotsadm-rolebinding",
@@ -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(deployOptions.KotsadmOptions), kotsadmTag(deployOptions.KotsadmOptions))
+	deployment.Spec.Template.Spec.Containers[containerIdx].Image = fmt.Sprintf("%s/kotsadm:%s", kotsadmRegistry(), kotsadmTag())
 
 	// 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,14 +205,6 @@ 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{
@@ -257,10 +249,9 @@ func kotsadmDeployment(deployOptions types.DeployOptions) *appsv1.Deployment {
 					},
 					ServiceAccountName: "kotsadm",
 					RestartPolicy:      corev1.RestartPolicyAlways,
-					ImagePullSecrets:   pullSecrets,
 					InitContainers: []corev1.Container{
 						{
-							Image:           fmt.Sprintf("%s/kotsadm:%s", kotsadmRegistry(deployOptions.KotsadmOptions), kotsadmTag(deployOptions.KotsadmOptions)),
+							Image:           fmt.Sprintf("%s/kotsadm:%s", kotsadmRegistry(), kotsadmTag()),
 							ImagePullPolicy: corev1.PullAlways,
 							Name:            "restore-db",
 							Command: []string{
@@ -287,7 +278,7 @@ func kotsadmDeployment(deployOptions types.DeployOptions) *appsv1.Deployment {
 							},
 						},
 						{
-							Image:           fmt.Sprintf("%s/kotsadm:%s", kotsadmRegistry(deployOptions.KotsadmOptions), kotsadmTag(deployOptions.KotsadmOptions)),
+							Image:           fmt.Sprintf("%s/kotsadm:%s", kotsadmRegistry(), kotsadmTag()),
 							ImagePullPolicy: corev1.PullAlways,
 							Name:            "restore-s3",
 							Command: []string{
@@ -339,7 +330,7 @@ func kotsadmDeployment(deployOptions types.DeployOptions) *appsv1.Deployment {
 					},
 					Containers: []corev1.Container{
 						{
-							Image:           fmt.Sprintf("%s/kotsadm:%s", kotsadmRegistry(deployOptions.KotsadmOptions), kotsadmTag(deployOptions.KotsadmOptions)),
+							Image:           fmt.Sprintf("%s/kotsadm:%s", kotsadmRegistry(), kotsadmTag()),
 							ImagePullPolicy: corev1.PullAlways,
 							Name:            "kotsadm",
 							Ports: []corev1.ContainerPort{
diff --git a/pkg/kotsadm/kotsadm_version.go b/pkg/kotsadm/kotsadm_version.go
index 27717c5a4..6dc36f0d7 100644
--- a/pkg/kotsadm/kotsadm_version.go
+++ b/pkg/kotsadm/kotsadm_version.go
@@ -5,20 +5,25 @@ 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"
-	corev1 "k8s.io/api/core/v1"
+)
+
+var (
+	OverrideVersion   = ""
+	OverrideRegistry  = ""
+	OverrideNamespace = ""
 )
 
 // return "alpha" for all invalid versions of kots,
 // kotsadm tag that matches this version for others
-func kotsadmTag(options types.KotsadmOptions) string {
-	if options.OverrideVersion != "" {
-		return options.OverrideVersion
+func kotsadmTag() string {
+	if OverrideVersion != "" {
+		return OverrideVersion
 	}
 
-	return kotsadmTagForVersionString(version.Version())
+	kotsVersion := version.Version()
+
+	return kotsadmTagForVersionString(kotsVersion)
 }
 
 func kotsadmTagForVersionString(kotsVersion string) string {
@@ -38,44 +43,18 @@ func kotsadmTagForVersionString(kotsVersion string) string {
 	return kotsVersion
 }
 
-func kotsadmRegistry(options types.KotsadmOptions) string {
-	if options.OverrideRegistry == "" {
-		if options.OverrideNamespace == "" {
+func kotsadmRegistry() string {
+	if OverrideRegistry == "" {
+		if OverrideNamespace == "" {
 			return "kotsadm"
 		} else {
-			return options.OverrideNamespace
+			return OverrideNamespace
 		}
 	}
 
-	registry := options.OverrideRegistry
-	namespace := options.OverrideNamespace
-
-	hostParts := strings.Split(options.OverrideRegistry, "/")
-	if len(hostParts) == 2 {
-		registry = hostParts[0]
-		namespace = hostParts[1]
+	if OverrideNamespace == "" {
+		return fmt.Sprintf("%s/kotsadm", OverrideRegistry)
 	}
 
-	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
+	return fmt.Sprintf("%s/%s", OverrideRegistry, OverrideNamespace)
 }
diff --git a/pkg/kotsadm/main.go b/pkg/kotsadm/main.go
index 46d05f70f..bb3e393bf 100644
--- a/pkg/kotsadm/main.go
+++ b/pkg/kotsadm/main.go
@@ -7,7 +7,6 @@ import (
 	"github.com/google/uuid"
 	"github.com/pkg/errors"
 	kotsscheme "github.com/replicatedhq/kots/kotskinds/client/kotsclientset/scheme"
-	"github.com/replicatedhq/kots/pkg/docker/registry"
 	"github.com/replicatedhq/kots/pkg/k8sutil"
 	"github.com/replicatedhq/kots/pkg/kotsadm/types"
 	"github.com/replicatedhq/kots/pkg/logger"
@@ -239,12 +238,7 @@ func CreateRestoreJob(options *types.RestoreJobOptions) error {
 	namespace := os.Getenv("POD_NAMESPACE")
 	isOpenShift := isOpenshift(clientset)
 
-	kotsadmOptions, err := GetKotsadmOptionsFromCluster(namespace, clientset)
-	if err != nil {
-		return errors.Wrap(err, "failed to get kotsadm options from cluster")
-	}
-
-	job := restoreJob(options.BackupName, namespace, isOpenShift, kotsadmOptions)
+	job := restoreJob(options.BackupName, namespace, isOpenShift)
 	_, err = clientset.BatchV1().Jobs(namespace).Create(context.TODO(), job, metav1.CreateOptions{})
 	if err != nil {
 		return errors.Wrap(err, "failed to create restore job")
@@ -349,10 +343,6 @@ func ensureKotsadm(deployOptions types.DeployOptions, clientset *kubernetes.Clie
 		}
 	}
 
-	if err := ensureKotsadmConfig(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")
 	}
@@ -487,42 +477,3 @@ func readDeployOptionsFromCluster(namespace string, kubernetesConfigFlags *gener
 
 	return &deployOptions, nil
 }
-
-func GetKotsadmOptionsFromCluster(namespace string, clientset *kubernetes.Clientset) (types.KotsadmOptions, error) {
-	kotsadmOptions := types.KotsadmOptions{}
-
-	configMap, err := clientset.CoreV1().ConfigMaps(namespace).Get(context.TODO(), types.KotsadmConfigMap, metav1.GetOptions{})
-	if err != nil {
-		if kuberneteserrors.IsNotFound(err) {
-			return kotsadmOptions, nil
-		}
-		return kotsadmOptions, errors.Wrap(err, "failed to get existing kotsadm config map")
-	}
-
-	kotsadmOptions.OverrideRegistry = configMap.Data["kotsadm-registry"]
-	if kotsadmOptions.OverrideRegistry == "" {
-		return kotsadmOptions, nil
-	}
-
-	imagePullSecret, err := clientset.CoreV1().Secrets(namespace).Get(context.TODO(), types.PrivateKotsadmRegistrySecret, metav1.GetOptions{})
-	if err != nil {
-		if kuberneteserrors.IsNotFound(err) {
-			return kotsadmOptions, nil
-		}
-		return kotsadmOptions, errors.Wrap(err, "failed to get existing private kotsadm registry secret")
-	}
-
-	dockerConfigJson := imagePullSecret.Data[".dockerconfigjson"]
-	if len(dockerConfigJson) == 0 {
-		return kotsadmOptions, nil
-	}
-
-	username, password, err := registry.GetCredentialsForRegistry(string(dockerConfigJson), kotsadmOptions.OverrideRegistry)
-	if err != nil {
-		return kotsadmOptions, errors.Wrap(err, "failed to parse dockerconfigjson")
-	}
-
-	kotsadmOptions.Username = username
-	kotsadmOptions.Password = password
-	return kotsadmOptions, nil
-}
diff --git a/pkg/kotsadm/minio_objects.go b/pkg/kotsadm/minio_objects.go
index d6790ab59..be0eed1f4 100644
--- a/pkg/kotsadm/minio_objects.go
+++ b/pkg/kotsadm/minio_objects.go
@@ -51,15 +51,6 @@ 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",
@@ -108,8 +99,7 @@ func minioStatefulset(deployOptions types.DeployOptions) *appsv1.StatefulSet {
 					},
 				},
 				Spec: corev1.PodSpec{
-					SecurityContext:  &securityContext,
-					ImagePullSecrets: pullSecrets,
+					SecurityContext: &securityContext,
 					Volumes: []corev1.Volume{
 						{
 							Name: "kotsadm-minio",
@@ -128,7 +118,7 @@ func minioStatefulset(deployOptions types.DeployOptions) *appsv1.StatefulSet {
 					},
 					Containers: []corev1.Container{
 						{
-							Image:           fmt.Sprintf("%s/minio:%s", kotsadmRegistry(deployOptions.KotsadmOptions), kotsadmTag(deployOptions.KotsadmOptions)),
+							Image:           fmt.Sprintf("%s/minio:%s", kotsadmRegistry(), kotsadmTag()),
 							ImagePullPolicy: corev1.PullIfNotPresent,
 							Name:            "kotsadm-minio",
 							Command: []string{
diff --git a/pkg/kotsadm/operator_objects.go b/pkg/kotsadm/operator_objects.go
index f28aa2d27..8647c6aaf 100644
--- a/pkg/kotsadm/operator_objects.go
+++ b/pkg/kotsadm/operator_objects.go
@@ -38,7 +38,7 @@ func operatorClusterRoleBinding(serviceAccountNamespace string) *rbacv1.ClusterR
 	clusterRoleBinding := &rbacv1.ClusterRoleBinding{
 		TypeMeta: metav1.TypeMeta{
 			APIVersion: "rbac.authorization.k8s.io/v1",
-			Kind:       "ClusterRoleBinding",
+			Kind:       "CluserRoleBinding",
 		},
 		ObjectMeta: metav1.ObjectMeta{
 			Name:   "kotsadm-operator-rolebinding",
@@ -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(deployOptions.KotsadmOptions), kotsadmTag(deployOptions.KotsadmOptions))
+	deployment.Spec.Template.Spec.Containers[containerIdx].Image = fmt.Sprintf("%s/kotsadm-operator:%s", kotsadmRegistry(), kotsadmTag())
 
 	// 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,15 +198,6 @@ 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",
@@ -233,10 +224,9 @@ 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(deployOptions.KotsadmOptions), kotsadmTag(deployOptions.KotsadmOptions)),
+							Image:           fmt.Sprintf("%s/kotsadm-operator:%s", kotsadmRegistry(), kotsadmTag()),
 							ImagePullPolicy: corev1.PullAlways,
 							Name:            "kotsadm-operator",
 							Env: []corev1.EnvVar{
diff --git a/pkg/kotsadm/postgres_objects.go b/pkg/kotsadm/postgres_objects.go
index 627048a28..d528a2159 100644
--- a/pkg/kotsadm/postgres_objects.go
+++ b/pkg/kotsadm/postgres_objects.go
@@ -1,7 +1,6 @@
 package kotsadm
 
 import (
-	"fmt"
 	"os"
 
 	"github.com/replicatedhq/kots/pkg/kotsadm/types"
@@ -16,17 +15,6 @@ 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
@@ -102,8 +90,7 @@ func postgresStatefulset(deployOptions types.DeployOptions) *appsv1.StatefulSet
 					}),
 				},
 				Spec: corev1.PodSpec{
-					SecurityContext:  &securityContext,
-					ImagePullSecrets: pullSecrets,
+					SecurityContext: &securityContext,
 					Volumes: []corev1.Volume{
 						{
 							Name: "kotsadm-postgres",
@@ -116,7 +103,7 @@ func postgresStatefulset(deployOptions types.DeployOptions) *appsv1.StatefulSet
 					},
 					Containers: []corev1.Container{
 						{
-							Image:           image,
+							Image:           "postgres:10.7",
 							ImagePullPolicy: corev1.PullIfNotPresent,
 							Name:            "kotsadm-postgres",
 							Ports: []corev1.ContainerPort{
diff --git a/pkg/kotsadm/push_images.go b/pkg/kotsadm/push_images.go
deleted file mode 100644
index 140223035..000000000
--- a/pkg/kotsadm/push_images.go
+++ /dev/null
@@ -1,217 +0,0 @@
-package kotsadm
-
-import (
-	"archive/tar"
-	"compress/gzip"
-	"context"
-	"fmt"
-	"io"
-	"io/ioutil"
-	"os"
-	"path/filepath"
-
-	"github.com/containers/image/copy"
-	"github.com/containers/image/signature"
-	"github.com/containers/image/transports/alltransports"
-	containerstypes "github.com/containers/image/types"
-	"github.com/pkg/errors"
-	"github.com/replicatedhq/kots/pkg/kotsadm/types"
-)
-
-func PushImages(options types.PushKotsadmImagesOptions) error {
-	imagesRootDir, err := ioutil.TempDir("", "kotsadm-airgap")
-	if err != nil {
-		return errors.Wrap(err, "failed to create temp dir")
-	}
-	defer os.RemoveAll(imagesRootDir)
-
-	err = extractAirgapImages(options.AirgapArchive, imagesRootDir, options.ProgressWriter)
-	if err != nil {
-		return errors.Wrap(err, "failed to extract images")
-	}
-
-	err = readImageFormats(imagesRootDir, options)
-	if err != nil {
-		return errors.Wrap(err, "failed to list image formats")
-	}
-
-	return nil
-}
-
-func extractAirgapImages(archive string, destDir string, progressWriter io.Writer) error {
-	reader, err := os.Open(archive)
-	if err != nil {
-		return errors.Wrap(err, "failed to open airgap archive")
-	}
-	defer reader.Close()
-
-	gzipReader, err := gzip.NewReader(reader)
-	if err != nil {
-		return errors.Wrap(err, "failed to get new gzip reader")
-	}
-	defer gzipReader.Close()
-
-	tarReader := tar.NewReader(gzipReader)
-	for {
-		header, err := tarReader.Next()
-		if err == io.EOF {
-			break
-		}
-
-		if err != nil {
-			return errors.Wrap(err, "failed to read tar header")
-		}
-
-		if header.Typeflag != tar.TypeReg {
-			continue
-		}
-
-		dstFileName := filepath.Join(destDir, header.Name)
-		if err := os.MkdirAll(filepath.Dir(dstFileName), 0755); err != nil {
-			return errors.Wrap(err, "failed to create path")
-		}
-
-		err = func() error {
-			writeProgressLine(progressWriter, fmt.Sprintf("Extracting %s", dstFileName))
-
-			dstFile, err := os.Create(dstFileName)
-			if err != nil {
-				return errors.Wrap(err, "failed to create file")
-			}
-			defer dstFile.Close()
-
-			if _, err := io.Copy(dstFile, tarReader); err != nil {
-				return errors.Wrap(err, "failed to copy file data")
-			}
-
-			return nil
-		}()
-		if err != nil {
-			return err
-		}
-	}
-
-	return nil
-}
-
-func readImageFormats(rootDir string, options types.PushKotsadmImagesOptions) error {
-	fileInfos, err := ioutil.ReadDir(rootDir)
-	if err != nil {
-		return errors.Wrap(err, "failed to read dir")
-	}
-
-	for _, info := range fileInfos {
-		if !info.IsDir() {
-			continue
-		}
-
-		err = readImageNames(rootDir, info.Name(), options)
-		if err != nil {
-			return errors.Wrapf(err, "failed list images names for format %s", info.Name())
-		}
-	}
-
-	return nil
-}
-
-func readImageNames(rootDir string, format string, options types.PushKotsadmImagesOptions) error {
-	fileInfos, err := ioutil.ReadDir(filepath.Join(rootDir, format))
-	if err != nil {
-		return errors.Wrap(err, "failed to read dir")
-	}
-
-	for _, info := range fileInfos {
-		if !info.IsDir() {
-			continue
-		}
-
-		err = readImageTags(rootDir, format, info.Name(), options)
-		if err != nil {
-			return errors.Wrapf(err, "failed list tags for image %s", info.Name())
-		}
-	}
-
-	return nil
-}
-
-func readImageTags(rootDir string, format string, imageName string, options types.PushKotsadmImagesOptions) error {
-	fileInfos, err := ioutil.ReadDir(filepath.Join(rootDir, format, imageName))
-	if err != nil {
-		return errors.Wrap(err, "failed to read dir")
-	}
-
-	for _, info := range fileInfos {
-		if info.IsDir() {
-			continue
-		}
-
-		err = pushOneImage(rootDir, format, imageName, info.Name(), options)
-		if err != nil {
-			return errors.Wrapf(err, "failed push image %s:%s", imageName, info.Name())
-		}
-	}
-
-	return nil
-}
-
-func pushOneImage(rootDir string, format string, imageName string, tag string, options types.PushKotsadmImagesOptions) error {
-	var imagePolicy = []byte(`{
-		"default": [{"type": "insecureAcceptAnything"}]
-	  }`)
-
-	policy, err := signature.NewPolicyFromBytes(imagePolicy)
-	if err != nil {
-		return errors.Wrap(err, "failed to read default policy")
-	}
-	policyContext, err := signature.NewPolicyContext(policy)
-	if err != nil {
-		return errors.Wrap(err, "failed to create policy")
-	}
-
-	destCtx := &containerstypes.SystemContext{
-		DockerInsecureSkipTLSVerify: containerstypes.OptionalBoolTrue,
-	}
-	if options.Username != "" && options.Password != "" {
-		destCtx.DockerAuthConfig = &containerstypes.DockerAuthConfig{
-			Username: options.Username,
-			Password: options.Password,
-		}
-	}
-	if os.Getenv("KOTSADM_INSECURE_SRCREGISTRY") == "true" {
-		// allow pulling images from http/invalid https docker repos
-		// intended for development only, _THIS MAKES THINGS INSECURE_
-		destCtx.DockerInsecureSkipTLSVerify = containerstypes.OptionalBoolTrue
-	}
-
-	destStr := fmt.Sprintf("%s/%s:%s", options.Registry, imageName, tag)
-	destRef, err := alltransports.ParseImageName(fmt.Sprintf("docker://%s", destStr))
-	if err != nil {
-		return errors.Wrapf(err, "failed to parse dest image name %s", destStr)
-	}
-
-	imageFile := filepath.Join(rootDir, format, imageName, tag)
-	localRef, err := alltransports.ParseImageName(fmt.Sprintf("%s:%s", format, imageFile))
-	if err != nil {
-		return errors.Wrapf(err, "failed to parse local image name: %s:%s", format, imageFile)
-	}
-
-	writeProgressLine(options.ProgressWriter, fmt.Sprintf("Pushing %s", destStr))
-
-	_, err = copy.Image(context.Background(), policyContext, destRef, localRef, &copy.Options{
-		RemoveSignatures:      true,
-		SignBy:                "",
-		ReportWriter:          options.ProgressWriter,
-		SourceCtx:             nil,
-		DestinationCtx:        destCtx,
-		ForceManifestMIMEType: "",
-	})
-	if err != nil {
-		return errors.Wrapf(err, "failed to push image")
-	}
-
-	return nil
-}
-
-func writeProgressLine(progressWriter io.Writer, line string) {
-	fmt.Fprint(progressWriter, fmt.Sprintf("%s\n", line))
-}
diff --git a/pkg/kotsadm/restore_job.go b/pkg/kotsadm/restore_job.go
index 04badcc0c..2a0a6da4b 100644
--- a/pkg/kotsadm/restore_job.go
+++ b/pkg/kotsadm/restore_job.go
@@ -10,7 +10,7 @@ import (
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
-func restoreJob(backupName string, namespace string, isOpenShift bool, kotsadmOptions types.KotsadmOptions) *batchv1.Job {
+func restoreJob(backupName string, namespace string, isOpenShift bool) *batchv1.Job {
 	var securityContext corev1.PodSecurityContext
 	if !isOpenShift {
 		securityContext = corev1.PodSecurityContext{
@@ -19,15 +19,6 @@ func restoreJob(backupName string, namespace string, isOpenShift bool, kotsadmOp
 		}
 	}
 
-	var pullSecrets []corev1.LocalObjectReference
-	if s := kotsadmPullSecret(namespace, kotsadmOptions); s != nil {
-		pullSecrets = []corev1.LocalObjectReference{
-			{
-				Name: s.ObjectMeta.Name,
-			},
-		}
-	}
-
 	job := &batchv1.Job{
 		TypeMeta: metav1.TypeMeta{
 			APIVersion: "batch/v1",
@@ -49,10 +40,9 @@ func restoreJob(backupName string, namespace string, isOpenShift bool, kotsadmOp
 					SecurityContext:    &securityContext,
 					RestartPolicy:      corev1.RestartPolicyNever,
 					ServiceAccountName: "kotsadm",
-					ImagePullSecrets:   pullSecrets,
 					Containers: []corev1.Container{
 						{
-							Image:           fmt.Sprintf("%s/kotsadm:%s", kotsadmRegistry(kotsadmOptions), kotsadmTag(kotsadmOptions)),
+							Image:           fmt.Sprintf("%s/kotsadm:%s", kotsadmRegistry(), kotsadmTag()),
 							ImagePullPolicy: corev1.PullAlways,
 							Name:            "kotsadm-restore",
 							Command: []string{
diff --git a/pkg/kotsadm/schemahero_objects.go b/pkg/kotsadm/schemahero_objects.go
index 53e7f8d10..3d7ddf909 100644
--- a/pkg/kotsadm/schemahero_objects.go
+++ b/pkg/kotsadm/schemahero_objects.go
@@ -21,15 +21,6 @@ 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",
@@ -41,31 +32,13 @@ func migrationsPod(deployOptions types.DeployOptions) *corev1.Pod {
 			Labels:    types.GetKotsadmLabels(),
 		},
 		Spec: corev1.PodSpec{
-			SecurityContext:  &securityContext,
-			RestartPolicy:    corev1.RestartPolicyOnFailure,
-			ImagePullSecrets: pullSecrets,
-			Volumes: []corev1.Volume{
-				{
-					Name: "migrations",
-					VolumeSource: corev1.VolumeSource{
-						EmptyDir: &corev1.EmptyDirVolumeSource{
-							Medium: corev1.StorageMediumMemory,
-						},
-					},
-				},
-			},
-			InitContainers: []corev1.Container{
+			SecurityContext: &securityContext,
+			RestartPolicy:   corev1.RestartPolicyOnFailure,
+			Containers: []corev1.Container{
 				{
-					Image:           fmt.Sprintf("%s/kotsadm-migrations:%s", kotsadmRegistry(deployOptions.KotsadmOptions), kotsadmTag(deployOptions.KotsadmOptions)),
+					Image:           fmt.Sprintf("%s/kotsadm-migrations:%s", kotsadmRegistry(), kotsadmTag()),
 					ImagePullPolicy: corev1.PullAlways,
-					Name:            "plan",
-					Args:            []string{"plan"},
-					VolumeMounts: []corev1.VolumeMount{
-						{
-							Name:      "migrations",
-							MountPath: "/migrations",
-						},
-					},
+					Name:            name,
 					Env: []corev1.EnvVar{
 						{
 							Name:  "SCHEMAHERO_DRIVER",
@@ -75,45 +48,6 @@ func migrationsPod(deployOptions types.DeployOptions) *corev1.Pod {
 							Name:  "SCHEMAHERO_SPEC_FILE",
 							Value: "/tables",
 						},
-						{
-							Name:  "SCHEMAHERO_OUT",
-							Value: "/migrations/plan.yaml",
-						},
-						{
-							Name: "SCHEMAHERO_URI",
-							ValueFrom: &corev1.EnvVarSource{
-								SecretKeyRef: &corev1.SecretKeySelector{
-									LocalObjectReference: corev1.LocalObjectReference{
-										Name: "kotsadm-postgres",
-									},
-									Key: "uri",
-								},
-							},
-						},
-					},
-				},
-			},
-			Containers: []corev1.Container{
-				{
-					Image:           fmt.Sprintf("%s/kotsadm-migrations:%s", kotsadmRegistry(deployOptions.KotsadmOptions), kotsadmTag(deployOptions.KotsadmOptions)),
-					ImagePullPolicy: corev1.PullAlways,
-					Name:            "apply",
-					Args:            []string{"apply"},
-					VolumeMounts: []corev1.VolumeMount{
-						{
-							Name:      "migrations",
-							MountPath: "/migrations",
-						},
-					},
-					Env: []corev1.EnvVar{
-						{
-							Name:  "SCHEMAHERO_DRIVER",
-							Value: "postgres",
-						},
-						{
-							Name:  "SCHEMAHERO_DDL",
-							Value: "/migrations/plan.yaml",
-						},
 						{
 							Name: "SCHEMAHERO_URI",
 							ValueFrom: &corev1.EnvVarSource{
diff --git a/pkg/kotsadm/secrets.go b/pkg/kotsadm/secrets.go
index 88d56db46..b994039bd 100644
--- a/pkg/kotsadm/secrets.go
+++ b/pkg/kotsadm/secrets.go
@@ -73,20 +73,11 @@ func getSecretsYAML(deployOptions *types.DeployOptions) (map[string][]byte, erro
 	}
 	docs["secret-s3.yaml"] = s3.Bytes()
 
-	var tokenSecret bytes.Buffer
-	if err := s.Encode(apiClusterTokenSecret(*deployOptions), &tokenSecret); err != nil {
+	var secret bytes.Buffer
+	if err := s.Encode(apiClusterTokenSecret(*deployOptions), &secret); err != nil {
 		return nil, errors.Wrap(err, "failed to marshal api cluster token secret")
 	}
-	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()
-	}
+	docs["secret-api-cluster-token.yaml"] = secret.Bytes()
 
 	return docs, nil
 }
@@ -337,7 +328,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")
 		}
 	}
 
@@ -363,24 +354,3 @@ 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 21e2ddbbd..007d59679 100644
--- a/pkg/kotsadm/secrets_objects.go
+++ b/pkg/kotsadm/secrets_objects.go
@@ -130,7 +130,3 @@ 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 de14aaef0..9a95e71bc 100644
--- a/pkg/kotsadm/types/constants.go
+++ b/pkg/kotsadm/types/constants.go
@@ -4,8 +4,6 @@ const KotsadmKey = "kots.io/kotsadm"
 const KotsadmLabelValue = "true"
 
 const ClusterTokenSecret = "kotsadm-cluster-token"
-const PrivateKotsadmRegistrySecret = "kotsadm-private-registry"
-const KotsadmConfigMap = "kotsadm-confg"
 
 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 44e30b8e4..aa86d6635 100644
--- a/pkg/kotsadm/types/deployoptions.go
+++ b/pkg/kotsadm/types/deployoptions.go
@@ -32,6 +32,4 @@ type DeployOptions struct {
 	IncludeMinio              bool
 	IncludeDockerDistribution bool
 	Timeout                   time.Duration
-
-	KotsadmOptions KotsadmOptions
 }
diff --git a/pkg/kotsadm/types/pushkotsadmoptions.go b/pkg/kotsadm/types/pushkotsadmoptions.go
deleted file mode 100644
index dda6c6480..000000000
--- a/pkg/kotsadm/types/pushkotsadmoptions.go
+++ /dev/null
@@ -1,11 +0,0 @@
-package types
-
-import "io"
-
-type PushKotsadmImagesOptions struct {
-	AirgapArchive  string
-	Registry       string
-	Username       string
-	Password       string
-	ProgressWriter io.Writer // using a writer because of skopeo
-}
diff --git a/pkg/kotsadm/types/registryoptions.go b/pkg/kotsadm/types/registryoptions.go
deleted file mode 100644
index 91653afac..000000000
--- a/pkg/kotsadm/types/registryoptions.go
+++ /dev/null
@@ -1,9 +0,0 @@
-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 b3c3993b4..fcd59b5d9 100644
--- a/pkg/kotsadm/types/upgradeoptions.go
+++ b/pkg/kotsadm/types/upgradeoptions.go
@@ -11,6 +11,4 @@ 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 148912b6f..c1e6bba14 100644
--- a/pkg/kotsadm/kotsad_version_test.go
+++ b/pkg/kotsadm/kotsad_version_test.go
@@ -3,7 +3,6 @@ package kotsadm
 import (
 	"testing"
 
-	"github.com/replicatedhq/kots/pkg/kotsadm/types"
 	"github.com/stretchr/testify/assert"
 	"go.undefinedlabs.com/scopeagent"
 )
@@ -37,14 +36,11 @@ 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
 
-			options := types.KotsadmOptions{
-				OverrideVersion:   test.overrideVersion,
-				OverrideRegistry:  test.overrideRegistry,
-				OverrideNamespace: test.overrideNamespace,
-			}
-
-			actual := kotsadmRegistry(options)
+			actual := kotsadmRegistry()
 			assert.Equal(t, test.expected, actual)
 		})
 	}