Repository: replicatedhq/kots
Pull request #1140 (2020-09-29 23:32:48)

License reporting


Patch:
diff --git a/go.mod b/go.mod
index 00a602906..52e04119c 100644
--- a/go.mod
+++ b/go.mod
@@ -37,7 +37,7 @@ require (
 	github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2
 	github.com/pkg/errors v0.9.1
 	github.com/replicatedhq/kurl/kurlkinds v0.0.0-20200617224429-55f493a72baf
-	github.com/replicatedhq/troubleshoot v0.9.42
+	github.com/replicatedhq/troubleshoot v0.9.43
 	github.com/replicatedhq/yaml/v3 v3.0.0-beta5-replicatedhq
 	github.com/sergi/go-diff v1.0.0
 	github.com/spf13/cobra v1.0.0
diff --git a/go.sum b/go.sum
index 0b59be3a5..2c59db474 100644
--- a/go.sum
+++ b/go.sum
@@ -875,8 +875,8 @@ github.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446/go.mod h1:uY
 github.com/replicatedhq/kurl/kurlkinds v0.0.0-20200617224429-55f493a72baf h1:QPJidrFEmB3ej7ugwss3iYnVM+k6uyL1AJ85xdcpDgg=
 github.com/replicatedhq/kurl/kurlkinds v0.0.0-20200617224429-55f493a72baf/go.mod h1:ZSyX/rt3a2f6r6TgCARPUlbZ1IUAxNIUrUxR9vzEUKQ=
 github.com/replicatedhq/termui/v3 v3.1.1-0.20200811145416-f40076d26851/go.mod h1:JDxG6+uubnk9/BZ2yUsyAJJwlptjrnmB2MPF5d2Xe/8=
-github.com/replicatedhq/troubleshoot v0.9.42 h1:UrDNrGmnQNckaa3UO0EN+Z0NTBWF4QFY0cigx0CpbpI=
-github.com/replicatedhq/troubleshoot v0.9.42/go.mod h1:H6pikMAa8tpPN6iF94HANX2Ju2YpwKZs1qnmifITbPQ=
+github.com/replicatedhq/troubleshoot v0.9.43 h1:Mub5qXheT0vs7Pe6HhqJBt88rNFIMwtg7Ckcy6ltEcU=
+github.com/replicatedhq/troubleshoot v0.9.43/go.mod h1:H6pikMAa8tpPN6iF94HANX2Ju2YpwKZs1qnmifITbPQ=
 github.com/replicatedhq/yaml/v3 v3.0.0-beta5-replicatedhq h1:PwPggruelq2336c1Ayg5STFqgbn/QB1tWLQwrVlU7ZQ=
 github.com/replicatedhq/yaml/v3 v3.0.0-beta5-replicatedhq/go.mod h1:Txa7LopbYCU8aRgmNe0n+y/EPMz50NbCPcVVJBquwag=
 github.com/robfig/cron v1.1.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k=
diff --git a/kotsadm/go.mod b/kotsadm/go.mod
index 19c60ec08..0d7dfce05 100644
--- a/kotsadm/go.mod
+++ b/kotsadm/go.mod
@@ -32,7 +32,7 @@ require (
 	github.com/pkg/errors v0.9.1
 	github.com/prometheus/client_golang v1.5.0 // indirect
 	github.com/replicatedhq/kots v0.0.0-00010101000000-000000000000
-	github.com/replicatedhq/troubleshoot v0.9.42
+	github.com/replicatedhq/troubleshoot v0.9.43
 	github.com/replicatedhq/yaml/v3 v3.0.0-beta5-replicatedhq
 	github.com/robfig/cron v1.1.0
 	github.com/robfig/cron/v3 v3.0.0
diff --git a/kotsadm/go.sum b/kotsadm/go.sum
index 98a8aa77c..cb4d669d4 100644
--- a/kotsadm/go.sum
+++ b/kotsadm/go.sum
@@ -973,8 +973,8 @@ github.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446/go.mod h1:uY
 github.com/replicatedhq/kurl/kurlkinds v0.0.0-20200617224429-55f493a72baf h1:QPJidrFEmB3ej7ugwss3iYnVM+k6uyL1AJ85xdcpDgg=
 github.com/replicatedhq/kurl/kurlkinds v0.0.0-20200617224429-55f493a72baf/go.mod h1:ZSyX/rt3a2f6r6TgCARPUlbZ1IUAxNIUrUxR9vzEUKQ=
 github.com/replicatedhq/termui/v3 v3.1.1-0.20200811145416-f40076d26851/go.mod h1:JDxG6+uubnk9/BZ2yUsyAJJwlptjrnmB2MPF5d2Xe/8=
-github.com/replicatedhq/troubleshoot v0.9.42 h1:UrDNrGmnQNckaa3UO0EN+Z0NTBWF4QFY0cigx0CpbpI=
-github.com/replicatedhq/troubleshoot v0.9.42/go.mod h1:H6pikMAa8tpPN6iF94HANX2Ju2YpwKZs1qnmifITbPQ=
+github.com/replicatedhq/troubleshoot v0.9.43 h1:Mub5qXheT0vs7Pe6HhqJBt88rNFIMwtg7Ckcy6ltEcU=
+github.com/replicatedhq/troubleshoot v0.9.43/go.mod h1:H6pikMAa8tpPN6iF94HANX2Ju2YpwKZs1qnmifITbPQ=
 github.com/replicatedhq/yaml/v3 v3.0.0-beta5-replicatedhq h1:PwPggruelq2336c1Ayg5STFqgbn/QB1tWLQwrVlU7ZQ=
 github.com/replicatedhq/yaml/v3 v3.0.0-beta5-replicatedhq/go.mod h1:Txa7LopbYCU8aRgmNe0n+y/EPMz50NbCPcVVJBquwag=
 github.com/robfig/cron v1.1.0 h1:jk4/Hud3TTdcrJgUOBgsqrZBarcxl6ADIjSC2iniwLY=
diff --git a/kotsadm/operator/Dockerfile.skaffold b/kotsadm/operator/Dockerfile.skaffold
index 1eb78a64e..dfc1ef1d7 100644
--- a/kotsadm/operator/Dockerfile.skaffold
+++ b/kotsadm/operator/Dockerfile.skaffold
@@ -25,7 +25,7 @@ RUN /install-krew.sh
 ENV PATH="/root/.krew/bin:$PATH"
 
 # Install our plugins
-ENV TROUBLESHOOT_VERSION=0.9.42
+ENV TROUBLESHOOT_VERSION=0.9.43
 
 ## This is used when we aren't on a release of preflight
 RUN curl -L "https://github.com/replicatedhq/troubleshoot/releases/download/v${TROUBLESHOOT_VERSION}/preflight_linux_amd64.tar.gz" > /tmp/preflight.tar.gz && \
diff --git a/kotsadm/operator/deploy/Dockerfile b/kotsadm/operator/deploy/Dockerfile
index 2d13ca38e..206153ba8 100644
--- a/kotsadm/operator/deploy/Dockerfile
+++ b/kotsadm/operator/deploy/Dockerfile
@@ -37,7 +37,7 @@ COPY --chown=kotsadm-operator:kotsadm-operator ./bin/kotsadm-operator /kotsadm-o
 
 # Install our plugins
 USER root
-ENV TROUBLESHOOT_VERSION=0.9.42
+ENV TROUBLESHOOT_VERSION=0.9.43
 RUN curl -L "https://github.com/replicatedhq/troubleshoot/releases/download/v${TROUBLESHOOT_VERSION}/preflight_linux_amd64.tar.gz" > /tmp/preflight.tar.gz && \
   cd /tmp && tar xzvf preflight.tar.gz && \
   mv /tmp/preflight /usr/local/bin/kubectl-preflight
diff --git a/kotsadm/operator/go.mod b/kotsadm/operator/go.mod
index b90ec1be8..59902057f 100644
--- a/kotsadm/operator/go.mod
+++ b/kotsadm/operator/go.mod
@@ -9,7 +9,7 @@ require (
 	github.com/mitchellh/hashstructure v1.0.0
 	github.com/pact-foundation/pact-go v1.0.0-beta.5
 	github.com/pkg/errors v0.9.1
-	github.com/replicatedhq/troubleshoot v0.9.42
+	github.com/replicatedhq/troubleshoot v0.9.43
 	github.com/spf13/cobra v0.0.5
 	github.com/spf13/viper v1.4.0
 	gopkg.in/yaml.v2 v2.3.0
diff --git a/kotsadm/operator/go.sum b/kotsadm/operator/go.sum
index 8fbc4de2a..e1c5d9050 100644
--- a/kotsadm/operator/go.sum
+++ b/kotsadm/operator/go.sum
@@ -2,6 +2,7 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT
 cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
 cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
 cloud.google.com/go v0.36.0/go.mod h1:RUoy9p/M4ge0HzT8L+SDZ8jg+Q6fth0CiBuhFJpSV40=
+cloud.google.com/go v0.38.0 h1:ROfEUZz+Gh5pa62DJWXSaonyu3StP6EA6lPEXPI6mCo=
 cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
 dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU=
 dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU=
@@ -26,6 +27,7 @@ github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbt
 github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
 github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
 github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM=
+github.com/ahmetalpbalkan/go-cursor v0.0.0-20131010032410-8136607ea412 h1:vOVO0ypMfTt6tZacyI0kp+iCZb1XSNiYDqnzBWYgfe4=
 github.com/ahmetalpbalkan/go-cursor v0.0.0-20131010032410-8136607ea412/go.mod h1:AI9hp1tkp10pAlK5TCwL+7yWbRgtDm9jhToq6qij2xs=
 github.com/alecthomas/gometalinter v2.0.11+incompatible/go.mod h1:qfIpQGGz3d+NmgyPBqv+LSh50emm1pt72EtcX2vKYQk=
 github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
@@ -36,12 +38,14 @@ github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5
 github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
 github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
 github.com/aws/aws-sdk-go v1.15.78/go.mod h1:E3/ieXAlvM0XWO57iftYVDLLvQ824smPP3ATZkfNZeM=
+github.com/aws/aws-sdk-go v1.25.18 h1:fMEkpli4r+FS4xZRqjgjYHP+uKaSwfk2MOcDUpLYwIE=
 github.com/aws/aws-sdk-go v1.25.18/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
 github.com/beevik/ntp v0.2.0 h1:sGsd+kAXzT0bfVfzJfce04g+dSRfrs+tbQW8lweuYgw=
 github.com/beevik/ntp v0.2.0/go.mod h1:hIHWr+l3+/clUnF44zdK+CWW7fO8dR5cIylAQ76NRpg=
 github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
 github.com/beorn7/perks v1.0.0 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0=
 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
+github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas=
 github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4=
 github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
 github.com/blang/semver v3.5.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
@@ -53,6 +57,7 @@ github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghf
 github.com/cheggaaa/pb v1.0.27/go.mod h1:pQciLPpbU0oxA0h+VJYYLxO+XeDQb5pZijXscXHm81s=
 github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
 github.com/chzyer/logex v1.1.11-0.20160617073814-96a4d311aa9b/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
+github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8=
 github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
 github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
@@ -80,8 +85,10 @@ github.com/docker/docker v0.7.3-0.20190327010347-be7ac8be2ae0/go.mod h1:eEKB0N0r
 github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
 github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
 github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=
+github.com/docker/spdystream v0.0.0-20181023171402-6480d4af844c h1:ZfSZ3P3BedhKGUhzj7BQlPSU4OvT6tfOKe3DVHzOA7s=
 github.com/docker/spdystream v0.0.0-20181023171402-6480d4af844c/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=
 github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
+github.com/dsnet/compress v0.0.1 h1:PlZu0n3Tuv04TzpfPbrnI0HW/YwodEXDS+oPKahKF0Q=
 github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo=
 github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
 github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
@@ -163,7 +170,9 @@ github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh
 github.com/go-openapi/validate v0.18.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4=
 github.com/go-openapi/validate v0.19.2/go.mod h1:1tRCw7m3jtI8eNWEEliiAqUIcBztB2KDnRCRMUi7GTA=
 github.com/go-openapi/validate v0.19.5/go.mod h1:8DJv2CVJQ6kGNpFW6eV9N3JviE1C85nY1c2z52x1Gk4=
+github.com/go-redis/redis/v7 v7.2.0 h1:CrCexy/jYWZjW0AyVoHlcJUeZN19VWlbepTh1Vq6dJs=
 github.com/go-redis/redis/v7 v7.2.0/go.mod h1:JDNMw23GTyLNC4GZu9njt15ctBQVn7xjRfnwdHj/Dcg=
+github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
 github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
 github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
 github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
@@ -188,6 +197,7 @@ github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg
 github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
 github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
 github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
 github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo=
@@ -210,8 +220,10 @@ github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3
 github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
 github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/googleapis/gax-go v2.0.0+incompatible h1:j0GKcs05QVmm7yesiZq2+9cxHkNK9YM6zKx4D2qucQU=
 github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
 github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg=
+github.com/googleapis/gax-go/v2 v2.0.4 h1:hU4mGcQI4DaAYW+IbTun+2qEZVFxK0ySjQLTbS0VQKc=
 github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
 github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
 github.com/googleapis/gnostic v0.1.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
@@ -230,10 +242,15 @@ github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgf
 github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
 github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
 github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
+github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
 github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/go-cleanhttp v0.5.0 h1:wvCrVc9TjDls6+YGAF2hAifE1E5U1+b4tH6KdvN3Gig=
 github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
+github.com/hashicorp/go-getter v1.3.1-0.20190627223108-da0323b9545e h1:6krcdHPiS+aIP9XKzJzSahfjD7jG7Z+4+opm0z39V1M=
 github.com/hashicorp/go-getter v1.3.1-0.20190627223108-da0323b9545e/go.mod h1:/O1k/AizTN0QmfEKknCYGvICeyKUDqCYA8vvWtGWDeQ=
+github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o=
 github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
+github.com/hashicorp/go-safetemp v1.0.0 h1:2HR189eFNrjHQyENnQMMpCiBAsRxzbTMIgBhEyExpmo=
 github.com/hashicorp/go-safetemp v1.0.0/go.mod h1:oaerMy3BhqiTbVye6QuFhFtIceqFoDHxNAB65b+Rj1I=
 github.com/hashicorp/go-version v1.1.0 h1:bPIoEKD27tNdebFGGxxYwcL4nepeY4j1QP23PFRGzg0=
 github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
@@ -254,6 +271,7 @@ github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NH
 github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
 github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU=
 github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
+github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM=
 github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
 github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
 github.com/json-iterator/go v1.1.6 h1:MrUvLMLTMxbqFJ9kzlvat/rYZqZnW3u4wkLzWTaFwKs=
@@ -262,6 +280,7 @@ github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/u
 github.com/json-iterator/go v1.1.8 h1:QiWkFLKq0T7mpzwOTu6BzNDbfTE8OLrYhVKYMLF46Ok=
 github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
 github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
+github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a h1:FaWFmfWdAUKbSCtOU2QjDaorUexogfaMgbipgYATUMU=
 github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a/go.mod h1:UJSiEoRfvx3hP73CvoARgeLjaIOjybY9vj8PUPPFGeU=
 github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
 github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
@@ -278,8 +297,10 @@ github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA=
 github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/lib/pq v1.3.0 h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU=
 github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
 github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE=
+github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a h1:weJVJJRzAJBFRlAiJQROKQs8oC9vOxvm4rZmBBk0ONw=
 github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
 github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
 github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
@@ -289,22 +310,27 @@ github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN
 github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
 github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
 github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs=
+github.com/manifoldco/promptui v0.3.2 h1:rir7oByTERac6jhpHUPErHuopoRDvO3jxS+FdadEns8=
 github.com/manifoldco/promptui v0.3.2/go.mod h1:8JU+igZ+eeiiRku4T5BjtKh2ms8sziGpSYl1gN8Bazw=
 github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4=
 github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
+github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
 github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
 github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
 github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
+github.com/mattn/go-isatty v0.0.9 h1:d5US/mDsogSGW37IV293h//ZFaeajb69h+EHFsv2xGg=
 github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
 github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
 github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
 github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
 github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
+github.com/mholt/archiver v3.1.1+incompatible h1:1dCVxuqs0dJseYEhi5pl7MYPH9zDa1wBi7mF09cbNkU=
 github.com/mholt/archiver v3.1.1+incompatible/go.mod h1:Dh2dOXnSdiLxRiPoVfIr/fI1TwETms9B8CTWfeh7ROU=
 github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
 github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
 github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
 github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0=
 github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
 github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
 github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
@@ -327,6 +353,7 @@ github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJE
 github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
 github.com/nicksnyder/go-i18n v1.10.1/go.mod h1:e4Di5xjP9oTVrC6y3C7C0HoSYXjSbhh/dU0eUV32nB4=
 github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ=
+github.com/nwaples/rardecode v1.0.0 h1:r7vGuS5akxOnR4JQSkko62RJ1ReCMXxQRPtxsiFMBOs=
 github.com/nwaples/rardecode v1.0.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
 github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
 github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
@@ -353,6 +380,7 @@ github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtP
 github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
 github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
 github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
+github.com/pierrec/lz4 v2.2.6+incompatible h1:6aCX4/YZ9v8q69hTyiR7dNLnTA3fgtKHVVW5BCd5Znw=
 github.com/pierrec/lz4 v2.2.6+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
 github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
@@ -385,8 +413,8 @@ github.com/prometheus/procfs v0.0.5 h1:3+auTFlqw+ZaQYJARz6ArODtkaIwtvBTx3N2NehQl
 github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
 github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
 github.com/replicatedhq/termui/v3 v3.1.1-0.20200811145416-f40076d26851/go.mod h1:JDxG6+uubnk9/BZ2yUsyAJJwlptjrnmB2MPF5d2Xe/8=
-github.com/replicatedhq/troubleshoot v0.9.42 h1:UrDNrGmnQNckaa3UO0EN+Z0NTBWF4QFY0cigx0CpbpI=
-github.com/replicatedhq/troubleshoot v0.9.42/go.mod h1:H6pikMAa8tpPN6iF94HANX2Ju2YpwKZs1qnmifITbPQ=
+github.com/replicatedhq/troubleshoot v0.9.43 h1:Mub5qXheT0vs7Pe6HhqJBt88rNFIMwtg7Ckcy6ltEcU=
+github.com/replicatedhq/troubleshoot v0.9.43/go.mod h1:H6pikMAa8tpPN6iF94HANX2Ju2YpwKZs1qnmifITbPQ=
 github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
 github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc=
 github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
@@ -448,6 +476,7 @@ github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H
 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
 github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
 github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
+github.com/tj/go-spin v1.1.0 h1:lhdWZsvImxvZ3q1C5OIB7d72DuOwP4O2NdBg9PyzNds=
 github.com/tj/go-spin v1.1.0/go.mod h1:Mg1mzmePZm4dva8Qz60H2lHwmJ2loum4VIrLgVnKwh4=
 github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
 github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
@@ -455,6 +484,7 @@ github.com/tsenart/deadcode v0.0.0-20160724212837-210d2dc333e9/go.mod h1:q+QjxYv
 github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
 github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
 github.com/ulikunitz/xz v0.5.5/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
+github.com/ulikunitz/xz v0.5.6 h1:jGHAfXawEGZQ3blwU5wnWKQJvAraT7Ftq9EXjnXYgt8=
 github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
 github.com/undefinedlabs/go-mpatch v0.0.0-20200122175732-0044123dbb98 h1:Z/megzdoMbuZ0H/oLmfTw92PAEfyTi1DkvAr8AUzFgw=
 github.com/undefinedlabs/go-mpatch v0.0.0-20200122175732-0044123dbb98/go.mod h1:TyJZDQ/5AgyN7FSLiBJ8RO9u2c6wbtRvK827b6AVqY4=
@@ -462,6 +492,7 @@ github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijb
 github.com/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw=
 github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI=
 github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
+github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
 github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
 github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
 github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
@@ -473,6 +504,7 @@ go.mongodb.org/mongo-driver v1.1.1/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qL
 go.mongodb.org/mongo-driver v1.1.2/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM=
 go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
 go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
+go.opencensus.io v0.22.0 h1:C9hSCOW830chIVkdja34wa6Ky+IzWllkUinR+BtRZd4=
 go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
 go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
 go.uber.org/atomic v1.4.0 h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU=
@@ -599,6 +631,7 @@ gomodules.xyz/jsonpatch/v2 v2.0.1/go.mod h1:IhYNNY4jnS53ZnfE4PAmpKtDpTCj1JFXc+3m
 google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
 google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
 google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y=
+google.golang.org/api v0.4.0 h1:KKgc1aqhV8wDPbDzlDtpvyjZFY3vjz85FP7p4wcQUyI=
 google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
 google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
 google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
diff --git a/kotsadm/pkg/airgap/airgap.go b/kotsadm/pkg/airgap/airgap.go
index d60eefe42..aa0ba6f7c 100644
--- a/kotsadm/pkg/airgap/airgap.go
+++ b/kotsadm/pkg/airgap/airgap.go
@@ -19,6 +19,7 @@ import (
 	"github.com/replicatedhq/kots/kotsadm/pkg/registry"
 	registrytypes "github.com/replicatedhq/kots/kotsadm/pkg/registry/types"
 	"github.com/replicatedhq/kots/kotsadm/pkg/store"
+	"github.com/replicatedhq/kots/kotsadm/pkg/supportbundle"
 	"github.com/replicatedhq/kots/kotsadm/pkg/version"
 	kotsv1beta1 "github.com/replicatedhq/kots/kotskinds/apis/kots/v1beta1"
 	"github.com/replicatedhq/kots/pkg/archives"
@@ -229,6 +230,11 @@ func CreateAppFromAirgap(pendingApp *types.PendingApp, airgapPath string, regist
 		return errors.Wrap(err, "failed to load kotskinds from path")
 	}
 
+	err = supportbundle.CreateRenderedSpec(a.ID, a.CurrentSequence, "", true, kotsKinds.SupportBundle)
+	if err != nil {
+		return errors.Wrap(err, "failed to create rendered support bundle spec")
+	}
+
 	if isAutomated && kotsKinds.Config != nil {
 		// bypass the config screen if no configuration is required
 		licenseSpec, err := kotsKinds.Marshal("kots.io", "v1beta1", "License")
diff --git a/kotsadm/pkg/apiserver/server.go b/kotsadm/pkg/apiserver/server.go
index 376389e88..64548e9ce 100644
--- a/kotsadm/pkg/apiserver/server.go
+++ b/kotsadm/pkg/apiserver/server.go
@@ -34,6 +34,8 @@ func Start() {
 		panic(err)
 	}
 
+	store.GetStore().RunMigrations()
+
 	if err := informers.Start(); err != nil {
 		log.Println("Failed to start informers", err)
 	}
@@ -73,11 +75,8 @@ func Start() {
 	r.HandleFunc("/api/v1/logout", handlers.Logout) // this route uses its own auth
 	r.Path("/api/v1/metadata").Methods("GET").HandlerFunc(handlers.Metadata)
 
-	r.Path("/api/v1/troubleshoot").Methods("GET").HandlerFunc(handlers.GetDefaultTroubleshoot)
-	r.Path("/api/v1/troubleshoot/{appSlug}").Methods("GET").HandlerFunc(handlers.GetTroubleshoot)
 	r.Path("/api/v1/troubleshoot/{appId}/{bundleId}").Methods("PUT").HandlerFunc(handlers.UploadSupportBundle)
 	r.Path("/api/v1/troubleshoot/supportbundle/{bundleId}/redactions").Methods("PUT").HandlerFunc(handlers.SetSupportBundleRedactions)
-	r.Path("/api/v1/preflight/app/{appSlug}/sequence/{sequence}").Methods("GET").HandlerFunc(handlers.GetPreflightStatus)
 	r.Path("/api/v1/preflight/app/{appSlug}/sequence/{sequence}").Methods("POST").HandlerFunc(handlers.PostPreflightStatus)
 
 	// This the handler for license API and should be called by the application only.
@@ -115,7 +114,7 @@ func Start() {
 	// Support Bundles
 	sessionAuthRouter.Path("/api/v1/troubleshoot/supportbundle/{bundleSlug}").Methods("GET").HandlerFunc(handlers.GetSupportBundle)
 	sessionAuthRouter.Path("/api/v1/troubleshoot/app/{appSlug}/supportbundles").Methods("GET").HandlerFunc(handlers.ListSupportBundles)
-	sessionAuthRouter.Path("/api/v1/troubleshoot/app/{appSlug}/supportbundlecommand").Methods("GET").HandlerFunc(handlers.GetSupportBundleCommand)
+	sessionAuthRouter.Path("/api/v1/troubleshoot/app/{appSlug}/supportbundlecommand").Methods("POST").HandlerFunc(handlers.GetSupportBundleCommand)
 	sessionAuthRouter.Path("/api/v1/troubleshoot/supportbundle/{bundleId}/files").Methods("GET").HandlerFunc(handlers.GetSupportBundleFiles)
 	sessionAuthRouter.Path("/api/v1/troubleshoot/supportbundle/{bundleId}/redactions").Methods("GET").HandlerFunc(handlers.GetSupportBundleRedactions)
 	sessionAuthRouter.Path("/api/v1/troubleshoot/supportbundle/{bundleId}/download").Methods("GET").HandlerFunc(handlers.DownloadSupportBundle)
@@ -151,7 +150,7 @@ func Start() {
 	sessionAuthRouter.Path("/api/v1/app/{appSlug}/sequence/{sequence}/preflight/ignore-rbac").Methods("POST").HandlerFunc(handlers.IgnorePreflightRBACErrors)
 	sessionAuthRouter.Path("/api/v1/app/{appSlug}/sequence/{sequence}/preflight/run").Methods("POST").HandlerFunc(handlers.StartPreflightChecks)
 	sessionAuthRouter.Path("/api/v1/app/{appSlug}/sequence/{sequence}/preflight/result").Methods("GET").HandlerFunc(handlers.GetPreflightResult)
-	sessionAuthRouter.Path("/api/v1/app/{appSlug}/sequence/{sequence}/preflightcommand").Methods("GET").HandlerFunc(handlers.GetPreflightCommand)
+	sessionAuthRouter.Path("/api/v1/app/{appSlug}/sequence/{sequence}/preflightcommand").Methods("POST").HandlerFunc(handlers.GetPreflightCommand)
 	sessionAuthRouter.Path("/api/v1/preflight/result").Methods("GET").HandlerFunc(handlers.GetLatestPreflightResult)
 	sessionAuthRouter.Path("/api/v1/app/{appSlug}/sequence/{sequence}/deploy").Methods("POST").HandlerFunc(handlers.DeployAppVersion)
 	sessionAuthRouter.Path("/api/v1/app/{appSlug}/sequence/{sequence}/renderedcontents").Methods("GET").HandlerFunc(handlers.GetAppRenderedContents)
diff --git a/kotsadm/pkg/app/types/types.go b/kotsadm/pkg/app/types/types.go
index f4d03b118..78ffa59f6 100644
--- a/kotsadm/pkg/app/types/types.go
+++ b/kotsadm/pkg/app/types/types.go
@@ -23,7 +23,6 @@ type App struct {
 	UpdatedAt             *time.Time     `json:"createdAt"`
 	CreatedAt             time.Time      `json:"updatedAt"`
 	LastUpdateCheckAt     string         `json:"lastUpdateCheckAt"`
-	BundleCommand         string         `json:"bundleCommand"`
 	HasPreflight          bool           `json:"hasPreflight"`
 	IsConfigurable        bool           `json:"isConfigurable"`
 	SnapshotTTL           string         `json:"snapshotTtl"`
diff --git a/kotsadm/pkg/handlers/app.go b/kotsadm/pkg/handlers/app.go
index 3653e36f5..e83e45f0d 100644
--- a/kotsadm/pkg/handlers/app.go
+++ b/kotsadm/pkg/handlers/app.go
@@ -12,6 +12,7 @@ import (
 	"github.com/replicatedhq/kots/kotsadm/pkg/gitops"
 	"github.com/replicatedhq/kots/kotsadm/pkg/logger"
 	"github.com/replicatedhq/kots/kotsadm/pkg/store"
+	"github.com/replicatedhq/kots/kotsadm/pkg/supportbundle"
 	"github.com/replicatedhq/kots/kotsadm/pkg/version"
 	versiontypes "github.com/replicatedhq/kots/kotsadm/pkg/version/types"
 )
@@ -31,7 +32,7 @@ type ResponseApp struct {
 	CreatedAt         time.Time  `json:"createdAt"`
 	UpdatedAt         *time.Time `json:"updatedAt"`
 	LastUpdateCheckAt string     `json:"lastUpdateCheckAt"`
-	BundleCommand     string     `json:"bundleCommand"`
+	BundleCommand     []string   `json:"bundleCommand"`
 	HasPreflight      bool       `json:"hasPreflight"`
 	IsConfigurable    bool       `json:"isConfigurable"`
 	UpdateCheckerSpec string     `json:"updateCheckerSpec"`
@@ -235,7 +236,7 @@ func responseAppFromApp(a *apptypes.App) (*ResponseApp, error) {
 		CreatedAt:         a.CreatedAt,
 		UpdatedAt:         a.UpdatedAt,
 		LastUpdateCheckAt: a.LastUpdateCheckAt,
-		BundleCommand:     a.BundleCommand,
+		BundleCommand:     supportbundle.GetBundleCommand(a.Slug),
 		HasPreflight:      a.HasPreflight,
 		IsConfigurable:    a.IsConfigurable,
 		UpdateCheckerSpec: a.UpdateCheckerSpec,
diff --git a/kotsadm/pkg/handlers/deploy.go b/kotsadm/pkg/handlers/deploy.go
index db62e8fd7..c6a5c0ade 100644
--- a/kotsadm/pkg/handlers/deploy.go
+++ b/kotsadm/pkg/handlers/deploy.go
@@ -3,6 +3,7 @@ package handlers
 import (
 	"encoding/json"
 	"net/http"
+	"os"
 	"strconv"
 	"time"
 
@@ -14,7 +15,9 @@ import (
 	downstreamtypes "github.com/replicatedhq/kots/kotsadm/pkg/downstream/types"
 	"github.com/replicatedhq/kots/kotsadm/pkg/logger"
 	"github.com/replicatedhq/kots/kotsadm/pkg/store"
+	"github.com/replicatedhq/kots/kotsadm/pkg/supportbundle"
 	"github.com/replicatedhq/kots/kotsadm/pkg/version"
+	"github.com/replicatedhq/kots/pkg/kotsutil"
 	"go.uber.org/zap"
 )
 
@@ -90,6 +93,11 @@ func UpdateDeployResult(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	if err := createSupportBundle(updateDeployResultRequest.AppID, currentSequence, "", true); err != nil {
+		// support bundle is not essential.  keep processing deployment request
+		logger.Error(errors.Wrapf(err, "failed to create support bundle for sequence %d after deploying", currentSequence))
+	}
+
 	alreadySuccessful, err := downstream.IsDownstreamDeploySuccessful(updateDeployResultRequest.AppID, clusterID, currentSequence)
 	if err != nil {
 		logger.Error(err)
@@ -120,6 +128,26 @@ func UpdateDeployResult(w http.ResponseWriter, r *http.Request) {
 	return
 }
 
+func createSupportBundle(appID string, sequence int64, origin string, inCluster bool) error {
+	archivePath, err := store.GetStore().GetAppVersionArchive(appID, sequence)
+	if err != nil {
+		return errors.Wrap(err, "failed to get current archive")
+	}
+	defer os.RemoveAll(archivePath)
+
+	kotsKinds, err := kotsutil.LoadKotsKindsFromPath(archivePath)
+	if err != nil {
+		return errors.Wrap(err, "failed to load current kotskinds")
+	}
+
+	err = supportbundle.CreateRenderedSpec(appID, sequence, origin, inCluster, kotsKinds.SupportBundle)
+	if err != nil {
+		return errors.Wrap(err, "failed to create rendered support bundle spec")
+	}
+
+	return nil
+}
+
 // NOTE: this uses special cluster authorization
 func UpdateUndeployResult(w http.ResponseWriter, r *http.Request) {
 	auth, err := parseClusterAuthorization(r.Header.Get("Authorization"))
diff --git a/kotsadm/pkg/handlers/preflight.go b/kotsadm/pkg/handlers/preflight.go
index 2aa1155e8..9f8e229d8 100644
--- a/kotsadm/pkg/handlers/preflight.go
+++ b/kotsadm/pkg/handlers/preflight.go
@@ -1,7 +1,7 @@
 package handlers
 
 import (
-	"fmt"
+	"encoding/json"
 	"io/ioutil"
 	"net/http"
 	"os"
@@ -12,7 +12,6 @@ import (
 	"github.com/replicatedhq/kots/kotsadm/pkg/logger"
 	"github.com/replicatedhq/kots/kotsadm/pkg/preflight"
 	preflighttypes "github.com/replicatedhq/kots/kotsadm/pkg/preflight/types"
-	"github.com/replicatedhq/kots/kotsadm/pkg/render/helper"
 	"github.com/replicatedhq/kots/kotsadm/pkg/store"
 	"github.com/replicatedhq/kots/pkg/kotsutil"
 )
@@ -21,6 +20,10 @@ type GetPreflightResultResponse struct {
 	PreflightResult preflighttypes.PreflightResult `json:"preflightResult"`
 }
 
+type GetPreflightCommandRequest struct {
+	Origin string `json:"origin"`
+}
+
 type GetPreflightCommandResponse struct {
 	Command []string `json:"command"`
 }
@@ -152,101 +155,54 @@ func GetPreflightCommand(w http.ResponseWriter, r *http.Request) {
 	appSlug := mux.Vars(r)["appSlug"]
 	sequence, err := strconv.ParseInt(mux.Vars(r)["sequence"], 10, 64)
 	if err != nil {
-		logger.Error(err)
-		w.WriteHeader(400)
+		logger.Error(errors.Wrap(err, "failed to parse sequence"))
+		w.WriteHeader(http.StatusBadRequest)
 		return
 	}
 
-	command := []string{
-		"curl https://krew.sh/preflight | bash",
-		fmt.Sprintf("kubectl preflight API_ADDRESS/api/v1/preflight/app/%s/sequence/%d", appSlug, sequence),
-	}
+	inCluster := r.URL.Query().Get("inCluster") == "true"
 
-	response := GetPreflightCommandResponse{
-		Command: command,
-	}
-	JSON(w, 200, response)
-}
-
-// GetPreflightStatus route is UNAUTHENTICATED
-// This request comes from the `kubectl preflight` command.
-func GetPreflightStatus(w http.ResponseWriter, r *http.Request) {
-	appSlug := mux.Vars(r)["appSlug"]
-	sequence, err := strconv.ParseInt(mux.Vars(r)["sequence"], 10, 64)
-	if err != nil {
-		err = errors.Wrap(err, "failed to parse sequence")
-		logger.Error(err)
-		w.WriteHeader(400)
+	getPreflightCommandRequest := GetPreflightCommandRequest{}
+	if err := json.NewDecoder(r.Body).Decode(&getPreflightCommandRequest); err != nil {
+		logger.Error(errors.Wrap(err, "failed to decode request body"))
+		w.WriteHeader(http.StatusBadRequest)
 		return
 	}
 
-	inCluster := r.URL.Query().Get("inCluster")
-
 	foundApp, err := store.GetStore().GetAppFromSlug(appSlug)
 	if err != nil {
-		err = errors.Wrap(err, "failed to get app from slug")
-		logger.Error(err)
-		w.WriteHeader(500)
+		logger.Error(errors.Wrap(err, "failed to get app"))
+		w.WriteHeader(http.StatusInternalServerError)
 		return
 	}
 
-	archiveDir, err := store.GetStore().GetAppVersionArchive(foundApp.ID, sequence)
+	archivePath, err := store.GetStore().GetAppVersionArchive(foundApp.ID, sequence)
 	if err != nil {
-		err = errors.Wrap(err, "failed to get app version archive")
-		logger.Error(err)
-		w.WriteHeader(500)
+		logger.Error(errors.Wrap(err, "failed to get app archive"))
+		w.WriteHeader(http.StatusInternalServerError)
 		return
 	}
+	defer os.RemoveAll(archivePath)
 
-	// we need a few objects from the app to check for updates
-	renderedKotsKinds, err := kotsutil.LoadKotsKindsFromPath(archiveDir)
+	kotsKinds, err := kotsutil.LoadKotsKindsFromPath(archivePath)
 	if err != nil {
-		err = errors.Wrap(err, "failed to load kotskinds")
-		logger.Error(err)
-		w.WriteHeader(500)
-		return
-	}
-
-	if renderedKotsKinds.Preflight == nil {
-		w.WriteHeader(404)
+		logger.Error(errors.Wrap(err, "failed to load kots kinds"))
+		w.WriteHeader(http.StatusInternalServerError)
 		return
 	}
 
-	// render the preflight file
-	// we need to convert to bytes first, so that we can reuse the renderfile function
-	renderedMarshalledPreflights, err := renderedKotsKinds.Marshal("troubleshoot.replicated.com", "v1beta1", "Preflight")
+	err = preflight.CreateRenderedSpec(foundApp.ID, sequence, getPreflightCommandRequest.Origin, inCluster, kotsKinds.Preflight)
 	if err != nil {
-		err = errors.Wrap(err, "failed to marshal rendered preflight")
-		logger.Error(err)
-		w.WriteHeader(500)
+		logger.Error(errors.Wrap(err, "failed to render preflight spec"))
+		w.WriteHeader(http.StatusInternalServerError)
 		return
 	}
 
-	renderedPreflight, err := helper.RenderAppFile(foundApp, &sequence, []byte(renderedMarshalledPreflights))
-	if err != nil {
-		err = errors.Wrap(err, "failed to render preflights")
-		logger.Error(err)
-		w.WriteHeader(500)
-		return
-	}
-
-	specJSON, err := kotsutil.LoadPreflightFromContents(renderedPreflight)
-	if err != nil {
-		err = errors.Wrap(err, "failed to load rendered preflight")
-		logger.Error(err)
-		w.WriteHeader(500)
-		return
-	}
-
-	var baseURL string
-	if inCluster == "true" {
-		baseURL = os.Getenv("API_ENDPOINT")
-	} else {
-		baseURL = os.Getenv("API_ADVERTISE_ENDPOINT")
+	response := GetPreflightCommandResponse{
+		Command: preflight.GetPreflightCommand(foundApp.Slug),
 	}
-	specJSON.Spec.UploadResultsTo = fmt.Sprintf("%s/api/v1/preflight/app/%s/sequence/%d", baseURL, appSlug, sequence)
 
-	YAML(w, 200, specJSON)
+	JSON(w, http.StatusOK, response)
 }
 
 // PostPreflightStatus route is UNAUTHENTICATED
diff --git a/kotsadm/pkg/handlers/registry.go b/kotsadm/pkg/handlers/registry.go
index 459602224..fd24a92b3 100644
--- a/kotsadm/pkg/handlers/registry.go
+++ b/kotsadm/pkg/handlers/registry.go
@@ -4,6 +4,7 @@ import (
 	"encoding/json"
 	"net/http"
 	"net/url"
+	"os"
 
 	"github.com/aws/aws-sdk-go/aws/awserr"
 	"github.com/containers/image/v5/docker"
@@ -11,9 +12,11 @@ import (
 	"github.com/gorilla/mux"
 	"github.com/pkg/errors"
 	"github.com/replicatedhq/kots/kotsadm/pkg/logger"
+	"github.com/replicatedhq/kots/kotsadm/pkg/preflight"
 	"github.com/replicatedhq/kots/kotsadm/pkg/registry"
 	registrytypes "github.com/replicatedhq/kots/kotsadm/pkg/registry/types"
 	"github.com/replicatedhq/kots/kotsadm/pkg/store"
+	"github.com/replicatedhq/kots/kotsadm/pkg/version"
 	dockerregistry "github.com/replicatedhq/kots/pkg/docker/registry"
 )
 
@@ -115,7 +118,7 @@ func UpdateAppRegistry(w http.ResponseWriter, r *http.Request) {
 	if err != nil {
 		logger.Error(err)
 		updateAppRegistryResponse.Error = err.Error()
-		JSON(w, 500, updateAppRegistryResponse)
+		JSON(w, http.StatusInternalServerError, updateAppRegistryResponse)
 		return
 	}
 
@@ -127,12 +130,12 @@ func UpdateAppRegistry(w http.ResponseWriter, r *http.Request) {
 				if err != nil {
 					logger.Error(err)
 					updateAppRegistryResponse.Error = err.Error()
-					JSON(w, 500, updateAppRegistryResponse)
+					JSON(w, http.StatusInternalServerError, updateAppRegistryResponse)
 					return
 				}
 
 				updateAppRegistryResponse.Success = true
-				JSON(w, 200, updateAppRegistryResponse)
+				JSON(w, http.StatusOK, updateAppRegistryResponse)
 				return
 			}
 		}
@@ -141,11 +144,11 @@ func UpdateAppRegistry(w http.ResponseWriter, r *http.Request) {
 	// in a goroutine, start pushing the images to the remote registry
 	// we will let this function return while this happens
 	go func() {
-		if err := registry.RewriteImages(
+		appDir, err := registry.RewriteImages(
 			foundApp.ID, foundApp.CurrentSequence, updateAppRegistryRequest.Hostname,
 			updateAppRegistryRequest.Username, updateAppRegistryRequest.Password,
-			updateAppRegistryRequest.Namespace, nil,
-		); err != nil {
+			updateAppRegistryRequest.Namespace, nil)
+		if err != nil {
 			// log credential errors at info level
 			causeErr := errors.Cause(err)
 			switch causeErr.(type) {
@@ -161,6 +164,22 @@ func UpdateAppRegistry(w http.ResponseWriter, r *http.Request) {
 			}
 			return
 		}
+		defer os.RemoveAll(appDir)
+
+		newSequence, err := version.CreateVersion(foundApp.ID, appDir, "Registry Change", foundApp.CurrentSequence)
+		if err != nil {
+			logger.Error(err)
+			updateAppRegistryResponse.Error = err.Error()
+			JSON(w, http.StatusInternalServerError, updateAppRegistryResponse)
+			return
+		}
+
+		if err := preflight.Run(foundApp.ID, newSequence, foundApp.IsAirgap, appDir); err != nil {
+			logger.Error(err)
+			updateAppRegistryResponse.Error = err.Error()
+			JSON(w, http.StatusInternalServerError, updateAppRegistryResponse)
+			return
+		}
 
 		err = store.GetStore().UpdateRegistry(foundApp.ID, updateAppRegistryRequest.Hostname, updateAppRegistryRequest.Username, updateAppRegistryRequest.Password, updateAppRegistryRequest.Namespace)
 		if err != nil {
diff --git a/kotsadm/pkg/handlers/troubleshoot.go b/kotsadm/pkg/handlers/troubleshoot.go
index 44ca62f52..1cf1fd4e2 100644
--- a/kotsadm/pkg/handlers/troubleshoot.go
+++ b/kotsadm/pkg/handlers/troubleshoot.go
@@ -1,42 +1,30 @@
 package handlers
 
 import (
-	"archive/tar"
 	"bytes"
 	"encoding/json"
 	"fmt"
 	"io"
 	"io/ioutil"
 	"net/http"
-	"net/url"
 	"os"
-	"path/filepath"
-	"strings"
 	"time"
 
 	"github.com/gorilla/mux"
 	"github.com/pkg/errors"
-	apptypes "github.com/replicatedhq/kots/kotsadm/pkg/app/types"
-	"github.com/replicatedhq/kots/kotsadm/pkg/license"
+	"github.com/replicatedhq/kots/kotsadm/pkg/downstream"
 	"github.com/replicatedhq/kots/kotsadm/pkg/logger"
-	"github.com/replicatedhq/kots/kotsadm/pkg/redact"
 	"github.com/replicatedhq/kots/kotsadm/pkg/render/helper"
-	"github.com/replicatedhq/kots/kotsadm/pkg/snapshot"
 	"github.com/replicatedhq/kots/kotsadm/pkg/store"
 	"github.com/replicatedhq/kots/kotsadm/pkg/supportbundle"
 	"github.com/replicatedhq/kots/kotsadm/pkg/supportbundle/types"
 	"github.com/replicatedhq/kots/pkg/kotsutil"
-	"github.com/replicatedhq/kots/pkg/template"
 	troubleshootanalyze "github.com/replicatedhq/troubleshoot/pkg/analyze"
 	troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
 	"github.com/replicatedhq/troubleshoot/pkg/convert"
 	redact2 "github.com/replicatedhq/troubleshoot/pkg/redact"
-	"github.com/replicatedhq/yaml/v3"
-	"go.uber.org/multierr"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	k8sjson "k8s.io/apimachinery/pkg/runtime/serializer/json"
-	"k8s.io/apimachinery/pkg/util/rand"
 	"k8s.io/client-go/kubernetes/scheme"
 )
 
@@ -77,6 +65,10 @@ type ResponseSupportBundle struct {
 	Analysis   *types.SupportBundleAnalysis `json:"analysis"`
 }
 
+type GetSupportBundleCommandRequest struct {
+	Origin string `json:"origin"`
+}
+
 type GetSupportBundleCommandResponse struct {
 	Command []string `json:"command"`
 }
@@ -196,15 +188,54 @@ func ListSupportBundles(w http.ResponseWriter, r *http.Request) {
 func GetSupportBundleCommand(w http.ResponseWriter, r *http.Request) {
 	appSlug := mux.Vars(r)["appSlug"]
 
-	command := []string{
-		"curl https://krew.sh/support-bundle | bash",
-		fmt.Sprintf("kubectl support-bundle API_ADDRESS/api/v1/troubleshoot/%s", appSlug),
+	// in case of an error, return a generic command
+	response := GetSupportBundleCommandResponse{
+		Command: []string{
+			"curl https://krew.sh/support-bundle | bash",
+			fmt.Sprintf("kubectl support-bundle secret/%s/%s", os.Getenv("POD_NAMESPACE"), supportbundle.GetSpecSecretName(appSlug)),
+		},
 	}
 
-	response := GetSupportBundleCommandResponse{
-		Command: command,
+	getSupportBundleCommandRequest := GetSupportBundleCommandRequest{}
+	if err := json.NewDecoder(r.Body).Decode(&getSupportBundleCommandRequest); err != nil {
+		logger.Error(errors.Wrap(err, "failed to decode request"))
+		JSON(w, 200, response)
+		return
+	}
+
+	foundApp, err := store.GetStore().GetAppFromSlug(appSlug)
+	if err != nil {
+		logger.Error(errors.Wrap(err, "failed to get app"))
+		JSON(w, 200, response)
+		return
+	}
+
+	sequence := int64(0)
+
+	downstreams, err := store.GetStore().ListDownstreamsForApp(foundApp.ID)
+	if err != nil {
+		logger.Error(errors.Wrap(err, "failed to get downstreams for app"))
+		JSON(w, 200, response)
+		return
+	} else if len(downstreams) > 0 {
+		currentVersion, err := downstream.GetCurrentVersion(foundApp.ID, downstreams[0].ClusterID)
+		if err != nil {
+			logger.Error(errors.Wrap(err, "failed to get deployed app sequence"))
+			JSON(w, 200, response)
+			return
+		}
+
+		sequence = currentVersion.Sequence
 	}
 
+	if err := createSupportBundle(foundApp.ID, sequence, getSupportBundleCommandRequest.Origin, false); err != nil {
+		logger.Error(errors.Wrap(err, "failed to create support bundle spec"))
+		JSON(w, 200, response)
+		return
+	}
+
+	response.Command = supportbundle.GetBundleCommand(foundApp.Slug)
+
 	JSON(w, 200, response)
 }
 
@@ -367,126 +398,6 @@ func UploadSupportBundle(w http.ResponseWriter, r *http.Request) {
 	w.WriteHeader(http.StatusOK)
 }
 
-// GetDefaultTroubleshoot route is UNAUTHENTICATED
-// This request comes from the `kubectl support-bundle` command.
-func GetDefaultTroubleshoot(w http.ResponseWriter, r *http.Request) {
-	defaultTroubleshootSpec := addDefaultTroubleshoot(nil, "", nil)
-	defaultBytes, err := yaml.Marshal(defaultTroubleshootSpec)
-	if err != nil {
-		logger.Error(err)
-		w.WriteHeader(500)
-		return
-	}
-	fullTroubleshoot := string(defaultBytes)
-	redactSpec, _, err := redact.GetRedactSpec()
-	if err != nil {
-		logger.Error(err)
-		w.WriteHeader(500)
-		return
-	}
-	if redactSpec != "" {
-		fullTroubleshoot = fmt.Sprintf("%s\n---\n%s", string(defaultBytes), redactSpec)
-	}
-
-	w.WriteHeader(200)
-	w.Write([]byte(fullTroubleshoot))
-}
-
-// GetTroubleshoot route is UNAUTHENTICATED
-// This request comes from the `kubectl support-bundle` command.
-func GetTroubleshoot(w http.ResponseWriter, r *http.Request) {
-	appSlug := mux.Vars(r)["appSlug"]
-	inCluster := r.URL.Query().Get("incluster")
-
-	// get app from slug
-	foundApp, err := store.GetStore().GetAppFromSlug(appSlug)
-	if err != nil {
-		logger.Error(err)
-		w.WriteHeader(500)
-		return
-	}
-	// TODO get from watch ID, not just app id
-
-	// get troubleshoot spec from db
-	existingSpec, err := store.GetStore().GetSupportBundleSpecForApp(foundApp.ID)
-	if err != nil {
-		logger.Error(err)
-		w.WriteHeader(500)
-		return
-	}
-
-	decode := scheme.Codecs.UniversalDeserializer().Decode
-	obj, _, err := decode([]byte(existingSpec), nil, nil)
-	if err != nil {
-		logger.Error(err)
-		w.WriteHeader(500)
-		return
-	}
-	existingTs := obj.(*troubleshootv1beta2.Collector)
-
-	existingTs = populateNamespaces(existingTs)
-
-	// determine an upload URL
-	var uploadURL string
-	var redactURL string
-	randomBundleID := strings.ToLower(rand.String(32))
-	if r.Header.Get("Bundle-Upload-Host") != "" {
-		uploadURL = fmt.Sprintf("%s/api/v1/troubleshoot/%s/%s", r.Header.Get("Bundle-Upload-Host"), foundApp.ID, randomBundleID)
-		redactURL = fmt.Sprintf("%s/api/v1/troubleshoot/supportbundle/%s/redactions", r.Header.Get("Bundle-Upload-Host"), randomBundleID)
-	} else if inCluster == "true" {
-		uploadURL = fmt.Sprintf("%s/api/v1/troubleshoot/%s/%s", fmt.Sprintf("http://kotsadm.%s.svc.cluster.local:3000", os.Getenv("POD_NAMESPACE")), foundApp.ID, randomBundleID)
-		redactURL = fmt.Sprintf("%s/api/v1/troubleshoot/supportbundle/%s/redactions", fmt.Sprintf("http://kotsadm.%s.svc.cluster.local:3000", os.Getenv("POD_NAMESPACE")), randomBundleID)
-	} else {
-		uploadURL = fmt.Sprintf("%s/api/v1/troubleshoot/%s/%s", os.Getenv("API_ADVERTISE_ENDPOINT"), foundApp.ID, randomBundleID)
-		redactURL = fmt.Sprintf("%s/api/v1/troubleshoot/supportbundle/%s/redactions", os.Getenv("API_ADVERTISE_ENDPOINT"), randomBundleID)
-	}
-
-	licenseString, err := license.GetCurrentLicenseString(foundApp)
-	if err != nil {
-		logger.Error(err)
-		w.WriteHeader(500)
-		return
-	}
-
-	tsSpec := addDefaultTroubleshoot(existingTs, licenseString, foundApp)
-	tsSpec.Spec.AfterCollection = []*troubleshootv1beta2.AfterCollection{
-		{
-			UploadResultsTo: &troubleshootv1beta2.ResultRequest{
-				URI:       uploadURL,
-				Method:    "PUT",
-				RedactURI: redactURL,
-			},
-		},
-	}
-
-	specBytes, err := yaml.Marshal(tsSpec)
-	if err != nil {
-		logger.Error(err)
-		w.WriteHeader(500)
-		return
-	}
-	fullTroubleshoot := string(specBytes)
-	redactSpec, _, err := redact.GetRedactSpec()
-	if err != nil {
-		logger.Error(err)
-		w.WriteHeader(500)
-		return
-	}
-	if redactSpec != "" {
-		fullTroubleshoot = fmt.Sprintf("%s\n---\n%s", string(specBytes), redactSpec)
-	}
-
-	renderedTroubleshoot, err := helper.RenderAppFile(foundApp, nil, []byte(fullTroubleshoot))
-	if err != nil {
-		logger.Error(err)
-		w.WriteHeader(500)
-		return
-	}
-
-	w.WriteHeader(200)
-	w.Write(renderedTroubleshoot)
-}
-
 func GetSupportBundleRedactions(w http.ResponseWriter, r *http.Request) {
 	getSupportBundleRedactionsResponse := GetSupportBundleRedactionsResponse{
 		Success: false,
@@ -536,349 +447,3 @@ func SetSupportBundleRedactions(w http.ResponseWriter, r *http.Request) {
 	w.WriteHeader(201)
 	return
 }
-
-// if a namespace is not set for a secret/run/logs/exec/copy collector, set it to the current namespace
-func populateNamespaces(existingSpec *troubleshootv1beta2.Collector) *troubleshootv1beta2.Collector {
-	if existingSpec == nil {
-		return nil
-	} else if existingSpec.Spec.Collectors == nil {
-		return existingSpec
-	}
-
-	builder := template.Builder{}
-	builder.AddCtx(template.StaticCtx{})
-
-	ns := func(ns string) string {
-		templated, err := builder.RenderTemplate("ns", ns)
-		if err != nil {
-			logger.Error(err)
-		}
-		if templated != "" {
-			return templated
-		}
-		return os.Getenv("POD_NAMESPACE")
-	}
-
-	collects := []*troubleshootv1beta2.Collect{}
-	for _, collect := range existingSpec.Spec.Collectors {
-		if collect.Secret != nil {
-			collect.Secret.Namespace = ns(collect.Secret.Namespace)
-		}
-		if collect.Run != nil {
-			collect.Run.Namespace = ns(collect.Run.Namespace)
-		}
-		if collect.Logs != nil {
-			collect.Logs.Namespace = ns(collect.Logs.Namespace)
-		}
-		if collect.Exec != nil {
-			collect.Exec.Namespace = ns(collect.Exec.Namespace)
-		}
-		if collect.Copy != nil {
-			collect.Copy.Namespace = ns(collect.Copy.Namespace)
-		}
-		collects = append(collects, collect)
-	}
-	existingSpec.Spec.Collectors = collects
-	return existingSpec
-}
-
-func addDefaultTroubleshoot(existingSpec *troubleshootv1beta2.Collector, licenseData string, foundApp *apptypes.App) *troubleshootv1beta2.Collector {
-	if existingSpec == nil {
-		existingSpec = &troubleshootv1beta2.Collector{
-			TypeMeta: v1.TypeMeta{
-				Kind:       "Collector",
-				APIVersion: "troubleshoot.sh/v1beta2",
-			},
-			ObjectMeta: v1.ObjectMeta{
-				Name: "default-collector",
-			},
-		}
-	}
-
-	if licenseData != "" {
-		existingSpec.Spec.Collectors = append(existingSpec.Spec.Collectors, &troubleshootv1beta2.Collect{
-			Data: &troubleshootv1beta2.Data{
-				CollectorMeta: troubleshootv1beta2.CollectorMeta{
-					CollectorName: "license.yaml",
-				},
-				Name: "kots/admin-console",
-				Data: licenseData,
-			},
-		})
-	}
-
-	existingSpec.Spec.Collectors = append(existingSpec.Spec.Collectors, &troubleshootv1beta2.Collect{
-		Secret: &troubleshootv1beta2.Secret{
-			CollectorMeta: troubleshootv1beta2.CollectorMeta{
-				CollectorName: "kotsadm-replicated-registry",
-			},
-			SecretName:   "kotsadm-replicated-registry",
-			Namespace:    os.Getenv("POD_NAMESPACE"),
-			Key:          ".dockerconfigjson",
-			IncludeValue: false,
-		},
-	})
-
-	existingSpec.Spec.Collectors = append(existingSpec.Spec.Collectors, makeDbCollectors()...)
-	existingSpec.Spec.Collectors = append(existingSpec.Spec.Collectors, makeKotsadmCollectors()...)
-	existingSpec.Spec.Collectors = append(existingSpec.Spec.Collectors, makeRookCollectors()...)
-	existingSpec.Spec.Collectors = append(existingSpec.Spec.Collectors, makeKurlCollectors()...)
-	existingSpec.Spec.Collectors = append(existingSpec.Spec.Collectors, makeVeleroCollectors()...)
-
-	apps := []*apptypes.App{}
-	if foundApp != nil {
-		apps = append(apps, foundApp)
-	} else {
-		var err error
-		apps, err = store.GetStore().ListInstalledApps()
-		if err != nil {
-			logger.Errorf("Failed to list installed apps: %v", err)
-		}
-	}
-
-	if len(apps) > 0 {
-		collectors, err := makeAppVersionArchiveCollectors(apps)
-		if err != nil {
-			logger.Errorf("Failed to make app version archive collectors: %v", err)
-		}
-		existingSpec.Spec.Collectors = append(existingSpec.Spec.Collectors, collectors...)
-	}
-
-	return existingSpec
-}
-
-func makeDbCollectors() []*troubleshootv1beta2.Collect {
-	dbCollectors := []*troubleshootv1beta2.Collect{}
-
-	pgConnectionString := os.Getenv("POSTGRES_URI")
-	parsedPg, err := url.Parse(pgConnectionString)
-	if err == nil {
-		username := "kotsadm"
-		if parsedPg.User != nil {
-			username = parsedPg.User.Username()
-		}
-		dbCollectors = append(dbCollectors, &troubleshootv1beta2.Collect{
-			Exec: &troubleshootv1beta2.Exec{
-				CollectorMeta: troubleshootv1beta2.CollectorMeta{
-					CollectorName: "kotsadm-postgres-db",
-				},
-				Name:          "kots/admin-console",
-				Selector:      []string{fmt.Sprintf("app=%s", parsedPg.Host)},
-				Namespace:     os.Getenv("POD_NAMESPACE"),
-				ContainerName: parsedPg.Host,
-				Command:       []string{"pg_dump"},
-				Args:          []string{"-U", username},
-				Timeout:       "10s",
-			},
-		})
-	}
-	return dbCollectors
-}
-
-func makeKotsadmCollectors() []*troubleshootv1beta2.Collect {
-	names := []string{
-		"kotsadm-postgres",
-		"kotsadm",
-		"kotsadm-operator",
-		"kurl-proxy-kotsadm",
-	}
-	rookCollectors := []*troubleshootv1beta2.Collect{}
-	for _, name := range names {
-		rookCollectors = append(rookCollectors, &troubleshootv1beta2.Collect{
-			Logs: &troubleshootv1beta2.Logs{
-				CollectorMeta: troubleshootv1beta2.CollectorMeta{
-					CollectorName: name,
-				},
-				Name:      "kots/admin-console",
-				Selector:  []string{fmt.Sprintf("app=%s", name)},
-				Namespace: os.Getenv("POD_NAMESPACE"),
-			},
-		})
-	}
-	return rookCollectors
-}
-
-func makeRookCollectors() []*troubleshootv1beta2.Collect {
-	names := []string{
-		"rook-ceph-agent",
-		"rook-ceph-mgr",
-		"rook-ceph-mon",
-		"rook-ceph-operator",
-		"rook-ceph-osd",
-		"rook-ceph-osd-prepare",
-		"rook-ceph-rgw",
-		"rook-discover",
-	}
-	rookCollectors := []*troubleshootv1beta2.Collect{}
-	for _, name := range names {
-		rookCollectors = append(rookCollectors, &troubleshootv1beta2.Collect{
-			Logs: &troubleshootv1beta2.Logs{
-				CollectorMeta: troubleshootv1beta2.CollectorMeta{
-					CollectorName: name,
-				},
-				Name:      "kots/rook",
-				Selector:  []string{fmt.Sprintf("app=%s", name)},
-				Namespace: "rook-ceph",
-			},
-		})
-	}
-	return rookCollectors
-}
-
-func makeKurlCollectors() []*troubleshootv1beta2.Collect {
-	names := []string{
-		"registry",
-	}
-	rookCollectors := []*troubleshootv1beta2.Collect{}
-	for _, name := range names {
-		rookCollectors = append(rookCollectors, &troubleshootv1beta2.Collect{
-			Logs: &troubleshootv1beta2.Logs{
-				CollectorMeta: troubleshootv1beta2.CollectorMeta{
-					CollectorName: name,
-				},
-				Name:      "kots/kurl",
-				Selector:  []string{fmt.Sprintf("app=%s", name)},
-				Namespace: "kurl",
-			},
-		})
-	}
-	return rookCollectors
-}
-
-func makeVeleroCollectors() []*troubleshootv1beta2.Collect {
-	collectors := []*troubleshootv1beta2.Collect{}
-
-	veleroNamespace, err := snapshot.DetectVeleroNamespace()
-	if err != nil {
-		logger.Error(err)
-		return collectors
-	}
-
-	if veleroNamespace == "" {
-		return collectors
-	}
-
-	selectors := []string{
-		"component=velero",
-		"app.kubernetes.io/name=velero",
-	}
-
-	for _, selector := range selectors {
-		collectors = append(collectors, &troubleshootv1beta2.Collect{
-			Logs: &troubleshootv1beta2.Logs{
-				CollectorMeta: troubleshootv1beta2.CollectorMeta{
-					CollectorName: "velero",
-				},
-				Name:      "velero",
-				Selector:  []string{selector},
-				Namespace: veleroNamespace,
-			},
-		})
-	}
-
-	return collectors
-}
-
-func makeAppVersionArchiveCollectors(apps []*apptypes.App) ([]*troubleshootv1beta2.Collect, error) {
-	dirPrefix, err := ioutil.TempDir("", "app-version-archive")
-	if err != nil {
-		return nil, errors.Wrap(err, "failed to create temp dir")
-	}
-	go func() {
-		<-time.After(10 * time.Minute)
-		_ = os.RemoveAll(dirPrefix)
-	}()
-
-	collectors := []*troubleshootv1beta2.Collect{}
-	for _, app := range apps {
-		collector, err := makeAppVersionArchiveCollector(app, dirPrefix)
-		if err != nil {
-			err = multierr.Append(err, errors.Wrapf(err, "failed to make collector for app %s", app.Slug))
-		} else {
-			collectors = append(collectors, collector)
-		}
-	}
-
-	return collectors, err
-}
-
-func makeAppVersionArchiveCollector(app *apptypes.App, dirPrefix string) (*troubleshootv1beta2.Collect, error) {
-	fileName := filepath.Join(dirPrefix, fmt.Sprintf("%s.tar", app.Slug))
-	archive, err := os.Create(fileName)
-	if err != nil {
-		return nil, errors.Wrapf(err, "failed to create temp file %s", fileName)
-	}
-
-	tempPath, err := store.GetStore().GetAppVersionArchive(app.ID, app.CurrentSequence)
-	if err != nil {
-		return nil, errors.Wrap(err, "failed to get app version archive")
-	}
-	defer os.RemoveAll(tempPath)
-
-	tarWriter := tar.NewWriter(archive)
-	defer tarWriter.Close()
-
-	// only upstream includes files that don't contain any secrets
-	archivePath := filepath.Join(tempPath, "upstream")
-
-	err = filepath.Walk(archivePath, func(path string, fi os.FileInfo, err error) error {
-		if err != nil {
-			return err
-		}
-		if archivePath == path {
-			// root dir itself is the first item in the walk
-			return nil
-		}
-
-		trimmedPath := strings.TrimPrefix(path, archivePath)
-
-		// do not include userdata in archive
-		if filepath.HasPrefix(trimmedPath, "/userdata") {
-			return nil
-		}
-
-		tarHeader, err := tar.FileInfoHeader(fi, "")
-		if err != nil {
-			return errors.Wrapf(err, "failed to get tar header from file info header for file %s", trimmedPath)
-		}
-		tarHeader.Name = trimmedPath
-
-		if err := tarWriter.WriteHeader(tarHeader); err != nil {
-			return errors.Wrapf(err, "failed to write tar header for file %s", trimmedPath)
-		}
-
-		if fi.IsDir() {
-			return nil
-		}
-
-		file, err := os.Open(path)
-		if err != nil {
-			return errors.Wrapf(err, "failed to open file %s", trimmedPath)
-		}
-		defer file.Close()
-
-		if _, err := io.Copy(tarWriter, file); err != nil {
-			return errors.Wrapf(err, "failed to write file %s contents", trimmedPath)
-		}
-
-		return nil
-	})
-	if err != nil {
-		return nil, errors.Wrapf(err, "failed to walk archive dir %s", archivePath)
-	}
-
-	return &troubleshootv1beta2.Collect{
-		Copy: &troubleshootv1beta2.Copy{
-			CollectorMeta: troubleshootv1beta2.CollectorMeta{
-				CollectorName: fmt.Sprintf("spec-%s", app.Slug),
-			},
-			Selector: []string{
-				"app=kotsadm", // can we assume this?
-			},
-			Namespace:     os.Getenv("POD_NAMESPACE"),
-			ContainerName: "kotsadm", // can we assume this? kotsadm-api
-			ContainerPath: fileName,
-			Name:          fmt.Sprintf("kots/admin-console/app/%s", app.Slug),
-		},
-	}, nil
-}
diff --git a/kotsadm/pkg/online/online.go b/kotsadm/pkg/online/online.go
index 174473b51..428c8d3aa 100644
--- a/kotsadm/pkg/online/online.go
+++ b/kotsadm/pkg/online/online.go
@@ -14,6 +14,7 @@ import (
 	"github.com/replicatedhq/kots/kotsadm/pkg/online/types"
 	"github.com/replicatedhq/kots/kotsadm/pkg/preflight"
 	"github.com/replicatedhq/kots/kotsadm/pkg/store"
+	"github.com/replicatedhq/kots/kotsadm/pkg/supportbundle"
 	"github.com/replicatedhq/kots/kotsadm/pkg/updatechecker"
 	"github.com/replicatedhq/kots/kotsadm/pkg/version"
 	"github.com/replicatedhq/kots/pkg/kotsutil"
@@ -159,6 +160,11 @@ func CreateAppFromOnline(pendingApp *types.PendingApp, upstreamURI string, isAut
 		return nil, errors.Wrap(err, "failed to load kotskinds from path")
 	}
 
+	err = supportbundle.CreateRenderedSpec(pendingApp.ID, 0, "", true, kotsKinds.SupportBundle)
+	if err != nil {
+		return nil, errors.Wrap(err, "failed to create rendered support bundle spec")
+	}
+
 	if isAutomated && kotsKinds.Config != nil {
 		// bypass the config screen if no configuration is required
 		licenseSpec, err := kotsKinds.Marshal("kots.io", "v1beta1", "License")
diff --git a/kotsadm/pkg/preflight/preflight.go b/kotsadm/pkg/preflight/preflight.go
index bb67f128d..8402f273e 100644
--- a/kotsadm/pkg/preflight/preflight.go
+++ b/kotsadm/pkg/preflight/preflight.go
@@ -1,15 +1,35 @@
 package preflight
 
 import (
+	"bytes"
+	"context"
+	"fmt"
+	"os"
+
 	"github.com/pkg/errors"
 	"github.com/replicatedhq/kots/kotsadm/pkg/downstream"
 	"github.com/replicatedhq/kots/kotsadm/pkg/logger"
+	"github.com/replicatedhq/kots/kotsadm/pkg/registry"
 	"github.com/replicatedhq/kots/kotsadm/pkg/render"
+	"github.com/replicatedhq/kots/kotsadm/pkg/render/helper"
 	"github.com/replicatedhq/kots/kotsadm/pkg/store"
 	"github.com/replicatedhq/kots/kotsadm/pkg/version"
+	"github.com/replicatedhq/kots/kotskinds/client/kotsclientset/scheme"
 	"github.com/replicatedhq/kots/pkg/kotsutil"
+	troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
 	troubleshootpreflight "github.com/replicatedhq/troubleshoot/pkg/preflight"
 	"go.uber.org/zap"
+	corev1 "k8s.io/api/core/v1"
+	kuberneteserrors "k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	serializer "k8s.io/apimachinery/pkg/runtime/serializer/json"
+	"k8s.io/client-go/kubernetes"
+	"sigs.k8s.io/controller-runtime/pkg/client/config"
+)
+
+const (
+	SpecDataKey = "preflight-spec"
 )
 
 func Run(appID string, sequence int64, isAirgap bool, archiveDir string) error {
@@ -62,6 +82,11 @@ func Run(appID string, sequence int64, isAirgap bool, archiveDir string) error {
 		if err != nil {
 			return errors.Wrap(err, "failed to load rendered preflight")
 		}
+		collectors, err := registry.UpdateCollectorSpecsWithRegistryData(p.Spec.Collectors, registrySettings, renderedKotsKinds.Installation.Spec.KnownImages, renderedKotsKinds.License)
+		if err != nil {
+			return errors.Wrap(err, "failed to rewrite images in preflight")
+		}
+		p.Spec.Collectors = collectors
 
 		go func() {
 			logger.Debug("preflight checks beginning")
@@ -151,3 +176,110 @@ func getPreflightState(preflightResults *troubleshootpreflight.UploadPreflightRe
 
 	return state
 }
+
+func GetSpecSecretName(appSlug string) string {
+	return fmt.Sprintf("kotsadm-%s-preflight", appSlug)
+}
+
+func GetSpecURI(appSlug string) string {
+	return fmt.Sprintf("secret/%s/%s", os.Getenv("POD_NAMESPACE"), GetSpecSecretName(appSlug))
+}
+
+func GetPreflightCommand(appSlug string) []string {
+	comamnd := []string{
+		"curl https://krew.sh/preflight | bash",
+		fmt.Sprintf("kubectl preflight %s\n", GetSpecURI(appSlug)),
+	}
+
+	return comamnd
+}
+
+func CreateRenderedSpec(appID string, sequence int64, origin string, inCluster bool, preflight *troubleshootv1beta2.Preflight) error {
+	if preflight == nil {
+		preflight = &troubleshootv1beta2.Preflight{
+			TypeMeta: v1.TypeMeta{
+				Kind:       "Preflight",
+				APIVersion: "troubleshoot.sh/v1beta2",
+			},
+			ObjectMeta: v1.ObjectMeta{
+				Name: "default-preflight",
+			},
+		}
+	}
+
+	app, err := store.GetStore().GetApp(appID)
+	if err != nil {
+		return errors.Wrap(err, "failed to get app")
+	}
+
+	baseURL := os.Getenv("API_ADVERTISE_ENDPOINT")
+	if inCluster {
+		baseURL = os.Getenv("API_ENDPOINT")
+	} else if origin != "" {
+		baseURL = origin
+	}
+	preflight.Spec.UploadResultsTo = fmt.Sprintf("%s/api/v1/preflight/app/%s/sequence/%d", baseURL, app.Slug, sequence)
+
+	s := serializer.NewYAMLSerializer(serializer.DefaultMetaFactory, scheme.Scheme, scheme.Scheme)
+	var b bytes.Buffer
+	if err := s.Encode(preflight, &b); err != nil {
+		return errors.Wrap(err, "failed to encode preflight")
+	}
+
+	templatedSpec := b.Bytes()
+
+	renderedSpec, err := helper.RenderAppFile(app, &sequence, templatedSpec)
+	if err != nil {
+		return errors.Wrap(err, "failed render preflight spec")
+	}
+
+	cfg, err := config.GetConfig()
+	if err != nil {
+		return errors.Wrap(err, "failed to get cluster config")
+	}
+
+	clientset, err := kubernetes.NewForConfig(cfg)
+	if err != nil {
+		return errors.Wrap(err, "failed to create clientset")
+	}
+
+	secretName := GetSpecSecretName(app.Slug)
+
+	existingSecret, err := clientset.CoreV1().Secrets(os.Getenv("POD_NAMESPACE")).Get(context.TODO(), secretName, metav1.GetOptions{})
+	if err != nil && !kuberneteserrors.IsNotFound(err) {
+		return errors.Wrap(err, "failed to read preflight secret")
+	} else if kuberneteserrors.IsNotFound(err) {
+		secret := &corev1.Secret{
+			TypeMeta: metav1.TypeMeta{
+				APIVersion: "v1",
+				Kind:       "Secret",
+			},
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      secretName,
+				Namespace: os.Getenv("POD_NAMESPACE"),
+			},
+			Data: map[string][]byte{
+				SpecDataKey: renderedSpec,
+			},
+		}
+
+		_, err = clientset.CoreV1().Secrets(os.Getenv("POD_NAMESPACE")).Create(context.TODO(), secret, metav1.CreateOptions{})
+		if err != nil {
+			return errors.Wrap(err, "failed to create preflight secret")
+		}
+
+		return nil
+	}
+
+	if existingSecret.Data == nil {
+		existingSecret.Data = map[string][]byte{}
+	}
+	existingSecret.Data[SpecDataKey] = renderedSpec
+
+	_, err = clientset.CoreV1().Secrets(os.Getenv("POD_NAMESPACE")).Update(context.TODO(), existingSecret, metav1.UpdateOptions{})
+	if err != nil {
+		return errors.Wrap(err, "failed to update preflight secret")
+	}
+
+	return nil
+}
diff --git a/kotsadm/pkg/registry/registry.go b/kotsadm/pkg/registry/registry.go
index 178deafe0..a4e4bef30 100644
--- a/kotsadm/pkg/registry/registry.go
+++ b/kotsadm/pkg/registry/registry.go
@@ -12,10 +12,8 @@ import (
 
 	"github.com/pkg/errors"
 	"github.com/replicatedhq/kots/kotsadm/pkg/logger"
-	"github.com/replicatedhq/kots/kotsadm/pkg/preflight"
 	"github.com/replicatedhq/kots/kotsadm/pkg/registry/types"
 	"github.com/replicatedhq/kots/kotsadm/pkg/store"
-	"github.com/replicatedhq/kots/kotsadm/pkg/version"
 	kotsv1beta1 "github.com/replicatedhq/kots/kotskinds/apis/kots/v1beta1"
 	"github.com/replicatedhq/kots/pkg/kotsadm"
 	"github.com/replicatedhq/kots/pkg/kotsutil"
@@ -29,9 +27,10 @@ import (
 
 // RewriteImages will use the app (a) and send the images to the registry specified. It will create patches for these
 // and create a new version of the application
-func RewriteImages(appID string, sequence int64, hostname string, username string, password string, namespace string, configValues *kotsv1beta1.ConfigValues) (finalError error) {
+// the caller is responsible for deleting the appDir returned
+func RewriteImages(appID string, sequence int64, hostname string, username string, password string, namespace string, configValues *kotsv1beta1.ConfigValues) (appDir string, finalError error) {
 	if err := store.GetStore().SetTaskStatus("image-rewrite", "Updating registry settings", "running"); err != nil {
-		return errors.Wrap(err, "failed to set task status")
+		return "", errors.Wrap(err, "failed to set task status")
 	}
 
 	finishedCh := make(chan struct{})
@@ -66,24 +65,24 @@ func RewriteImages(appID string, sequence int64, hostname string, username strin
 	// get the archive and store it in a temporary location
 	appDir, err := store.GetStore().GetAppVersionArchive(appID, sequence)
 	if err != nil {
-		return errors.Wrap(err, "failed to get app version archive")
+		return "", errors.Wrap(err, "failed to get app version archive")
 	}
 	defer os.RemoveAll(appDir)
 
 	installation, err := kotsutil.LoadInstallationFromPath(filepath.Join(appDir, "upstream", "userdata", "installation.yaml"))
 	if err != nil {
-		return errors.Wrap(err, "failed to load installation from path")
+		return "", errors.Wrap(err, "failed to load installation from path")
 	}
 
 	license, err := kotsutil.LoadLicenseFromPath(filepath.Join(appDir, "upstream", "userdata", "license.yaml"))
 	if err != nil {
-		return errors.Wrap(err, "failed to load license from path")
+		return "", errors.Wrap(err, "failed to load license from path")
 	}
 
 	if configValues == nil {
 		previousConfigValues, err := kotsutil.LoadConfigValuesFromFile(filepath.Join(appDir, "upstream", "userdata", "config.yaml"))
 		if err != nil && !os.IsNotExist(errors.Cause(err)) {
-			return errors.Wrap(err, "failed to load config values from path")
+			return "", errors.Wrap(err, "failed to load config values from path")
 		}
 
 		configValues = previousConfigValues
@@ -92,7 +91,7 @@ func RewriteImages(appID string, sequence int64, hostname string, username strin
 	// get the downstream names only
 	downstreams, err := store.GetStore().ListDownstreamsForApp(appID)
 	if err != nil {
-		return errors.Wrap(err, "failed to list downstreams")
+		return "", errors.Wrap(err, "failed to list downstreams")
 	}
 
 	downstreamNames := []string{}
@@ -102,7 +101,7 @@ func RewriteImages(appID string, sequence int64, hostname string, username strin
 
 	a, err := store.GetStore().GetApp(appID)
 	if err != nil {
-		return errors.Wrap(err, "failed to get app")
+		return "", errors.Wrap(err, "failed to get app")
 	}
 
 	// dev_namespace makes the dev env work
@@ -146,19 +145,10 @@ func RewriteImages(appID string, sequence int64, hostname string, username strin
 	}
 
 	if err := rewrite.Rewrite(options); err != nil {
-		return errors.Wrap(err, "failed to rewrite images")
+		return "", errors.Wrap(err, "failed to rewrite images")
 	}
 
-	newSequence, err := version.CreateVersion(appID, appDir, "Registry Change", a.CurrentSequence)
-	if err != nil {
-		return errors.Wrap(err, "failed to create new version")
-	}
-
-	if err := preflight.Run(appID, newSequence, a.IsAirgap, appDir); err != nil {
-		return errors.Wrap(err, "failed to run preflights")
-	}
-
-	return nil
+	return appDir, nil
 }
 
 func HasKurlRegistry() (bool, error) {
diff --git a/kotsadm/pkg/registry/troubleshoot.go b/kotsadm/pkg/registry/troubleshoot.go
new file mode 100644
index 000000000..392469a49
--- /dev/null
+++ b/kotsadm/pkg/registry/troubleshoot.go
@@ -0,0 +1,124 @@
+package registry
+
+import (
+	"encoding/base64"
+	"fmt"
+	"strings"
+
+	"github.com/pkg/errors"
+	"github.com/replicatedhq/kots/kotsadm/pkg/registry/types"
+	kotsv1beta1 "github.com/replicatedhq/kots/kotskinds/apis/kots/v1beta1"
+	kotsregistry "github.com/replicatedhq/kots/pkg/docker/registry"
+	troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
+)
+
+// UpdateCollectorSpecsWithRegistryData takes an array of collectors and some environment data (local registry info and license, etc)
+// any image that needs to be rewritten to be compatible with the local registry settings or proxy pull
+// will be updated and replaced in the spec.  any required image pull secret will be automatically
+// inserted into the spec
+// an error is returned if anything failed, but the collectors param can always be used after calling (assuming no error)
+func UpdateCollectorSpecsWithRegistryData(collectors []*troubleshootv1beta2.Collect, localRegistryInfo *types.RegistrySettings, knownImages []kotsv1beta1.InstallationImage, license *kotsv1beta1.License) ([]*troubleshootv1beta2.Collect, error) {
+	// if there's a local registry, always attach that image pull secret for all, and
+	// always rewrite
+	updatedCollectors := make([]*troubleshootv1beta2.Collect, len(collectors))
+
+	if localRegistryInfo != nil {
+		for idx, collect := range collectors {
+			// only the run collector supports an image currently
+			// this is written as in if statement to support additional collectors that include images
+			if collect.Run != nil {
+				run := collect.Run
+
+				run.Image = rewriteImage(localRegistryInfo.Hostname, localRegistryInfo.Namespace, run.Image)
+				pullSecret, err := kotsregistry.PullSecretForRegistries([]string{localRegistryInfo.Hostname}, localRegistryInfo.Username, localRegistryInfo.Password, run.Namespace)
+				if err != nil {
+					return nil, errors.Wrap(err, "failed to generate pull secret for registry")
+				}
+
+				run.ImagePullSecret = &troubleshootv1beta2.ImagePullSecrets{
+					SecretType: "kubernetes.io/dockerconfigjson",
+					Data: map[string]string{
+						".dockerconfigjson": base64.StdEncoding.EncodeToString(pullSecret.Data[".dockerconfigjson"]),
+					},
+				}
+				collect.Run = run
+
+				updatedCollectors[idx] = collect
+
+			} else {
+				updatedCollectors[idx] = collect
+			}
+		}
+
+		return updatedCollectors, nil
+	}
+
+	registryProxyInfo := kotsregistry.ProxyEndpointFromLicense(license)
+
+	// for all known private images, rewrite to the replicated proxy and add license image pull secret
+	for idx, collect := range collectors {
+		// all collectors that include images in the spec should have an if / else statement here
+		if collect.Run != nil {
+			for _, knownImage := range knownImages {
+				if knownImage.Image == collect.Run.Image && knownImage.IsPrivate {
+					run := collect.Run
+
+					// if it's the replicated registry, no change, just add image pull secret
+					registryHost := strings.Split(run.Image, "/")[0]
+					if registryHost != registryProxyInfo.Registry {
+						tag := strings.Split(run.Image, ":")
+						run.Image = kotsregistry.MakeProxiedImageURL(registryProxyInfo.Proxy, license.Spec.AppSlug, run.Image)
+						if len(tag) > 1 {
+							run.Image = fmt.Sprintf("%s:%s", run.Image, tag[len(tag)-1])
+						}
+						pullSecret, err := kotsregistry.PullSecretForRegistries([]string{registryProxyInfo.Proxy}, license.Spec.LicenseID, license.Spec.LicenseID, run.Namespace)
+						if err != nil {
+							return nil, errors.Wrap(err, "failed to generate pull secret for proxy registry")
+						}
+
+						run.ImagePullSecret = &troubleshootv1beta2.ImagePullSecrets{
+							SecretType: "kubernetes.io/dockerconfigjson",
+							Data: map[string]string{
+								".dockerconfigjson": base64.StdEncoding.EncodeToString(pullSecret.Data[".dockerconfigjson"]),
+							},
+						}
+
+						collect.Run = run
+					} else {
+						pullSecret, err := kotsregistry.PullSecretForRegistries([]string{registryProxyInfo.Registry}, license.Spec.LicenseID, license.Spec.LicenseID, run.Namespace)
+						if err != nil {
+							return nil, errors.Wrap(err, "failed to generate pull secret for replicated registry")
+						}
+
+						run.ImagePullSecret = &troubleshootv1beta2.ImagePullSecrets{
+							SecretType: "kubernetes.io/dockerconfigjson",
+							Data: map[string]string{
+								".dockerconfigjson": base64.StdEncoding.EncodeToString(pullSecret.Data[".dockerconfigjson"]),
+							},
+						}
+
+						collect.Run = run
+					}
+
+					collectors[idx].Run = run
+				}
+			}
+
+			updatedCollectors[idx] = collect
+		} else {
+			updatedCollectors[idx] = collect
+		}
+	}
+	return updatedCollectors, nil
+}
+
+func rewriteImage(newHost string, newNamespace string, image string) string {
+	imageParts := strings.Split(image, "/")
+	imageNameWithOptionalTag := imageParts[len(imageParts)-1]
+
+	return strings.Join([]string{
+		newHost,
+		newNamespace,
+		imageNameWithOptionalTag,
+	}, "/")
+}
diff --git a/kotsadm/pkg/socketservice/socketservice.go b/kotsadm/pkg/socketservice/socketservice.go
index fb6f8846f..9969399c0 100644
--- a/kotsadm/pkg/socketservice/socketservice.go
+++ b/kotsadm/pkg/socketservice/socketservice.go
@@ -364,8 +364,34 @@ func (s *SocketService) processSupportBundle(clusterSocket ClusterSocket, pendin
 		return errors.Wrap(err, "failed to get socket channel from server")
 	}
 
+	sequence := int64(0)
+
+	currentVersion, err := downstream.GetCurrentVersion(a.ID, clusterSocket.ClusterID)
+	if err != nil {
+		return errors.Wrap(err, "failed to get current downstream version")
+	}
+	if currentVersion != nil {
+		sequence = currentVersion.Sequence
+	}
+
+	archivePath, err := store.GetStore().GetAppVersionArchive(a.ID, sequence)
+	if err != nil {
+		return errors.Wrap(err, "failed to get current archive")
+	}
+	defer os.RemoveAll(archivePath)
+
+	kotsKinds, err := kotsutil.LoadKotsKindsFromPath(archivePath)
+	if err != nil {
+		return errors.Wrap(err, "failed to load current kotskinds")
+	}
+
+	err = supportbundle.CreateRenderedSpec(a.ID, sequence, "", true, kotsKinds.SupportBundle)
+	if err != nil {
+		return errors.Wrap(err, "failed to create rendered support bundle spec")
+	}
+
 	supportBundleArgs := SupportBundleArgs{
-		URI: fmt.Sprintf(`%s/api/v1/troubleshoot/%s?incluster=true`, os.Getenv("API_ENDPOINT"), a.Slug),
+		URI: supportbundle.GetSpecURI(a.Slug),
 	}
 	c.Emit("supportbundle", supportBundleArgs)
 
@@ -476,6 +502,11 @@ func checkRestoreComplete(a *apptypes.App, restore *velerov1.Restore) error {
 			return errors.Wrap(err, "failed to deploy version")
 		}
 
+		if err := createSupportBundle(a.ID, sequence, "", true); err != nil {
+			// support bundle is not essential.  keep processing restore status
+			logger.Error(errors.Wrapf(err, "failed to create support bundle for sequence %d post restore", sequence))
+		}
+
 		if err := app.ResetRestore(a.ID); err != nil {
 			return errors.Wrap(err, "failed to reset restore")
 		}
@@ -497,6 +528,26 @@ func checkRestoreComplete(a *apptypes.App, restore *velerov1.Restore) error {
 	return nil
 }
 
+func createSupportBundle(appID string, sequence int64, origin string, inCluster bool) error {
+	archivePath, err := store.GetStore().GetAppVersionArchive(appID, sequence)
+	if err != nil {
+		return errors.Wrap(err, "failed to get current archive")
+	}
+	defer os.RemoveAll(archivePath)
+
+	kotsKinds, err := kotsutil.LoadKotsKindsFromPath(archivePath)
+	if err != nil {
+		return errors.Wrap(err, "failed to load current kotskinds")
+	}
+
+	err = supportbundle.CreateRenderedSpec(appID, sequence, origin, inCluster, kotsKinds.SupportBundle)
+	if err != nil {
+		return errors.Wrap(err, "failed to create rendered support bundle spec")
+	}
+
+	return nil
+}
+
 func (s *SocketService) undeployApp(a *apptypes.App, d *downstreamtypes.Downstream, clusterSocket ClusterSocket) error {
 	deployedVersion, err := downstream.GetCurrentVersion(a.ID, d.ClusterID)
 	if err != nil {
diff --git a/kotsadm/pkg/store/ocistore/migrations.go b/kotsadm/pkg/store/ocistore/migrations.go
new file mode 100644
index 000000000..dc155434b
--- /dev/null
+++ b/kotsadm/pkg/store/ocistore/migrations.go
@@ -0,0 +1,4 @@
+package ocistore
+
+func (_ OCIStore) RunMigrations() {
+}
diff --git a/kotsadm/pkg/store/s3pg/app_store.go b/kotsadm/pkg/store/s3pg/app_store.go
index 2c490c9da..fd3235f11 100644
--- a/kotsadm/pkg/store/s3pg/app_store.go
+++ b/kotsadm/pkg/store/s3pg/app_store.go
@@ -152,8 +152,6 @@ func (s S3PGStore) GetApp(id string) (*apptypes.App, error) {
 		}
 	}
 
-	app.BundleCommand = fmt.Sprintf("curl https://krew.sh/support-bundle | bash\nkubectl support-bundle API_ADDRESS/api/v1/troubleshoot/%s\n", app.Slug)
-
 	isGitOps, err := s.IsGitOpsEnabledForApp(id)
 	if err != nil {
 		return nil, errors.Wrap(err, "failed to check if gitops is enabled")
diff --git a/kotsadm/pkg/store/s3pg/migrations.go b/kotsadm/pkg/store/s3pg/migrations.go
new file mode 100644
index 000000000..3188bfc27
--- /dev/null
+++ b/kotsadm/pkg/store/s3pg/migrations.go
@@ -0,0 +1,427 @@
+package s3pg
+
+import (
+	"fmt"
+	"os"
+
+	"github.com/pkg/errors"
+	"github.com/replicatedhq/kots/kotsadm/pkg/logger"
+	"github.com/replicatedhq/kots/kotsadm/pkg/persistence"
+	"github.com/replicatedhq/kots/pkg/kotsutil"
+)
+
+func (s S3PGStore) RunMigrations() {
+	if err := s.migrateKotsAppSpec(); err != nil {
+		logger.Error(errors.Wrap(err, "failed to migrate kots_app_spec"))
+	}
+
+	if err := s.migrateKotsInstallationSpec(); err != nil {
+		logger.Error(errors.Wrap(err, "failed to migrate kots_installation_spec"))
+	}
+
+	if err := s.migrateSupportBundleSpec(); err != nil {
+		logger.Error(errors.Wrap(err, "failed to migrate supportbundle_spec"))
+	}
+
+	if err := s.migratePreflightSpec(); err != nil {
+		logger.Error(errors.Wrap(err, "failed to migrate preflight_spec"))
+	}
+
+	if err := s.migrateAnalyzerSpec(); err != nil {
+		logger.Error(errors.Wrap(err, "failed to migrate analyzer_spec"))
+	}
+
+	if err := s.migrateAppSpec(); err != nil {
+		logger.Error(errors.Wrap(err, "failed to migrate app_spec"))
+	}
+}
+
+func (s S3PGStore) migrateKotsAppSpec() error {
+	db := persistence.MustGetPGSession()
+	query := `select app_id, sequence from app_version where kots_app_spec is null or not kots_app_spec like '%apiVersion%'`
+
+	rows, err := db.Query(query)
+	if err != nil {
+		return errors.Wrap(err, "failed to query db")
+	}
+	defer rows.Close()
+
+	type versionType struct {
+		appID    string
+		sequence int64
+	}
+
+	versions := make([]versionType, 0)
+	for rows.Next() {
+		var appID string
+		var sequence int64
+
+		if err := rows.Scan(&appID, &sequence); err != nil {
+			return errors.Wrap(err, "failed to scan")
+		}
+
+		versions = append(versions, versionType{
+			appID:    appID,
+			sequence: sequence,
+		})
+	}
+
+	for _, version := range versions {
+		logger.Info(fmt.Sprintf("Migrating kots_app_spec for app %s sequence %d", version.appID, version.sequence))
+		err := func() error {
+			archiveDir, err := s.GetAppVersionArchive(version.appID, version.sequence)
+			if err != nil {
+				return errors.Wrap(err, "failed to get app version archive")
+			}
+			defer os.RemoveAll(archiveDir)
+
+			kotsKinds, err := kotsutil.LoadKotsKindsFromPath(archiveDir)
+			if err != nil {
+				return errors.Wrap(err, "failed to load kots kinds from path")
+			}
+
+			spec, err := kotsKinds.Marshal("kots.io", "v1beta1", "Application")
+			if err != nil {
+				return errors.Wrap(err, "failed to marshal kots kinds")
+			}
+
+			query := `update app_version set kots_app_spec = $1 where app_id = $2 and sequence = $3`
+			_, err = db.Exec(query, spec, version.appID, version.sequence)
+			if err != nil {
+				return errors.Wrap(err, "failed to set kots_app_spec")
+			}
+
+			return nil
+		}()
+		if err != nil {
+			logger.Error(errors.Wrapf(err, "failed to migrate kots_app_spec, app %v sequece %v", version.appID, version.sequence))
+		}
+	}
+
+	return nil
+}
+
+func (s S3PGStore) migrateKotsInstallationSpec() error {
+	db := persistence.MustGetPGSession()
+	query := `select app_id, sequence from app_version where kots_installation_spec is null or not kots_installation_spec like '%apiVersion%'`
+
+	rows, err := db.Query(query)
+	if err != nil {
+		return errors.Wrap(err, "failed to query db")
+	}
+	defer rows.Close()
+
+	type versionType struct {
+		appID    string
+		sequence int64
+	}
+
+	versions := make([]versionType, 0)
+	for rows.Next() {
+		var appID string
+		var sequence int64
+
+		if err := rows.Scan(&appID, &sequence); err != nil {
+			return errors.Wrap(err, "failed to scan")
+		}
+
+		versions = append(versions, versionType{
+			appID:    appID,
+			sequence: sequence,
+		})
+	}
+
+	for _, version := range versions {
+		logger.Info(fmt.Sprintf("Migrating kots_installation_spec for app %s sequence %d", version.appID, version.sequence))
+		err := func() error {
+			archiveDir, err := s.GetAppVersionArchive(version.appID, version.sequence)
+			if err != nil {
+				return errors.Wrap(err, "failed to get app version archive")
+			}
+			defer os.RemoveAll(archiveDir)
+
+			kotsKinds, err := kotsutil.LoadKotsKindsFromPath(archiveDir)
+			if err != nil {
+				return errors.Wrap(err, "failed to load kots kinds from path")
+			}
+
+			spec, err := kotsKinds.Marshal("kots.io", "v1beta1", "Installation")
+			if err != nil {
+				return errors.Wrap(err, "failed to marshal kots kinds")
+			}
+
+			query := `update app_version set kots_installation_spec = $1 where app_id = $2 and sequence = $3`
+			_, err = db.Exec(query, spec, version.appID, version.sequence)
+			if err != nil {
+				return errors.Wrap(err, "failed to set kots_installation_spec")
+			}
+
+			return nil
+		}()
+		if err != nil {
+			logger.Error(errors.Wrapf(err, "failed to migrate kots_installation_spec app %v sequece %v", version.appID, version.sequence))
+		}
+	}
+
+	return nil
+}
+
+func (s S3PGStore) migrateSupportBundleSpec() error {
+	db := persistence.MustGetPGSession()
+	query := `select app_id, sequence from app_version where supportbundle_spec is null or not supportbundle_spec like '%apiVersion%'`
+
+	rows, err := db.Query(query)
+	if err != nil {
+		return errors.Wrap(err, "failed to query db")
+	}
+	defer rows.Close()
+
+	type versionType struct {
+		appID    string
+		sequence int64
+	}
+
+	versions := make([]versionType, 0)
+	for rows.Next() {
+		var appID string
+		var sequence int64
+
+		if err := rows.Scan(&appID, &sequence); err != nil {
+			return errors.Wrap(err, "failed to scan")
+		}
+
+		versions = append(versions, versionType{
+			appID:    appID,
+			sequence: sequence,
+		})
+	}
+
+	for _, version := range versions {
+		logger.Info(fmt.Sprintf("Migrating kots_installation_spec for app %s sequence %d", version.appID, version.sequence))
+		err := func() error {
+			archiveDir, err := s.GetAppVersionArchive(version.appID, version.sequence)
+			if err != nil {
+				return errors.Wrap(err, "failed to get app version archive")
+			}
+			defer os.RemoveAll(archiveDir)
+
+			kotsKinds, err := kotsutil.LoadKotsKindsFromPath(archiveDir)
+			if err != nil {
+				return errors.Wrap(err, "failed to load kots kinds from path")
+			}
+
+			spec, err := kotsKinds.Marshal("troubleshoot.sh", "v1beta2", "SupportBundle")
+			if err != nil {
+				return errors.Wrap(err, "failed to marshal kots kinds")
+			}
+
+			query := `update app_version set supportbundle_spec = $1 where app_id = $2 and sequence = $3`
+			_, err = db.Exec(query, spec, version.appID, version.sequence)
+			if err != nil {
+				return errors.Wrap(err, "failed to set supportbundle_spec")
+			}
+
+			return nil
+		}()
+		if err != nil {
+			logger.Error(errors.Wrapf(err, "failed to migrate supportbundle_spec app %v sequece %v", version.appID, version.sequence))
+		}
+	}
+
+	return nil
+}
+
+func (s S3PGStore) migratePreflightSpec() error {
+	db := persistence.MustGetPGSession()
+	query := `select app_id, sequence from app_version where preflight_spec is null or not preflight_spec like '%apiVersion%'`
+
+	rows, err := db.Query(query)
+	if err != nil {
+		return errors.Wrap(err, "failed to query db")
+	}
+	defer rows.Close()
+
+	type versionType struct {
+		appID    string
+		sequence int64
+	}
+
+	versions := make([]versionType, 0)
+	for rows.Next() {
+		var appID string
+		var sequence int64
+
+		if err := rows.Scan(&appID, &sequence); err != nil {
+			return errors.Wrap(err, "failed to scan")
+		}
+
+		versions = append(versions, versionType{
+			appID:    appID,
+			sequence: sequence,
+		})
+	}
+
+	for _, version := range versions {
+		logger.Info(fmt.Sprintf("Migrating preflight_spec for app %s sequence %d", version.appID, version.sequence))
+		err := func() error {
+			archiveDir, err := s.GetAppVersionArchive(version.appID, version.sequence)
+			if err != nil {
+				return errors.Wrap(err, "failed to get app version archive")
+			}
+			defer os.RemoveAll(archiveDir)
+
+			kotsKinds, err := kotsutil.LoadKotsKindsFromPath(archiveDir)
+			if err != nil {
+				return errors.Wrap(err, "failed to load kots kinds from path")
+			}
+
+			spec, err := kotsKinds.Marshal("troubleshoot.sh", "v1beta2", "Preflight")
+			if err != nil {
+				return errors.Wrap(err, "failed to marshal kots kinds")
+			}
+
+			query := `update app_version set preflight_spec = $1 where app_id = $2 and sequence = $3`
+			_, err = db.Exec(query, spec, version.appID, version.sequence)
+			if err != nil {
+				return errors.Wrap(err, "failed to set preflight_spec")
+			}
+
+			return nil
+		}()
+		if err != nil {
+			logger.Error(errors.Wrapf(err, "failed to migrate preflight_spec app %v sequece %v", version.appID, version.sequence))
+		}
+	}
+
+	return nil
+}
+
+func (s S3PGStore) migrateAnalyzerSpec() error {
+	db := persistence.MustGetPGSession()
+	query := `select app_id, sequence from app_version where analyzer_spec is null or not analyzer_spec like '%apiVersion%'`
+
+	rows, err := db.Query(query)
+	if err != nil {
+		return errors.Wrap(err, "failed to query db")
+	}
+	defer rows.Close()
+
+	type versionType struct {
+		appID    string
+		sequence int64
+	}
+
+	versions := make([]versionType, 0)
+	for rows.Next() {
+		var appID string
+		var sequence int64
+
+		if err := rows.Scan(&appID, &sequence); err != nil {
+			return errors.Wrap(err, "failed to scan")
+		}
+
+		versions = append(versions, versionType{
+			appID:    appID,
+			sequence: sequence,
+		})
+	}
+
+	for _, version := range versions {
+		logger.Info(fmt.Sprintf("Migrating analyzer_spec for app %s sequence %d", version.appID, version.sequence))
+		err := func() error {
+			archiveDir, err := s.GetAppVersionArchive(version.appID, version.sequence)
+			if err != nil {
+				return errors.Wrap(err, "failed to get app version archive")
+			}
+			defer os.RemoveAll(archiveDir)
+
+			kotsKinds, err := kotsutil.LoadKotsKindsFromPath(archiveDir)
+			if err != nil {
+				return errors.Wrap(err, "failed to load kots kinds from path")
+			}
+
+			spec, err := kotsKinds.Marshal("troubleshoot.sh", "v1beta2", "Analyzer")
+			if err != nil {
+				return errors.Wrap(err, "failed to marshal kots kinds")
+			}
+
+			query := `update app_version set analyzer_spec = $1 where app_id = $2 and sequence = $3`
+			_, err = db.Exec(query, spec, version.appID, version.sequence)
+			if err != nil {
+				return errors.Wrap(err, "failed to set analyzer_spec")
+			}
+
+			return nil
+		}()
+		if err != nil {
+			logger.Error(errors.Wrapf(err, "failed to migrate analyzer_spec app %v sequece %v", version.appID, version.sequence))
+		}
+	}
+
+	return nil
+}
+
+func (s S3PGStore) migrateAppSpec() error {
+	db := persistence.MustGetPGSession()
+	query := `select app_id, sequence from app_version where app_spec is null or not app_spec like '%apiVersion%'`
+
+	rows, err := db.Query(query)
+	if err != nil {
+		return errors.Wrap(err, "failed to query db")
+	}
+	defer rows.Close()
+
+	type versionType struct {
+		appID    string
+		sequence int64
+	}
+
+	versions := make([]versionType, 0)
+	for rows.Next() {
+		var appID string
+		var sequence int64
+
+		if err := rows.Scan(&appID, &sequence); err != nil {
+			return errors.Wrap(err, "failed to scan")
+		}
+
+		versions = append(versions, versionType{
+			appID:    appID,
+			sequence: sequence,
+		})
+	}
+
+	for _, version := range versions {
+		logger.Info(fmt.Sprintf("Migrating app_spec for app %s sequence %d", version.appID, version.sequence))
+		err := func() error {
+			archiveDir, err := s.GetAppVersionArchive(version.appID, version.sequence)
+			if err != nil {
+				return errors.Wrap(err, "failed to get app version archive")
+			}
+			defer os.RemoveAll(archiveDir)
+
+			kotsKinds, err := kotsutil.LoadKotsKindsFromPath(archiveDir)
+			if err != nil {
+				return errors.Wrap(err, "failed to load kots kinds from path")
+			}
+
+			spec, err := kotsKinds.Marshal("app.k8s.io", "v1beta1", "Application")
+			if err != nil {
+				return errors.Wrap(err, "failed to marshal kots kinds")
+			}
+
+			query := `update app_version set app_spec = $1 where app_id = $2 and sequence = $3`
+			_, err = db.Exec(query, spec, version.appID, version.sequence)
+			if err != nil {
+				return errors.Wrap(err, "failed to set app_spec")
+			}
+
+			return nil
+		}()
+		if err != nil {
+			logger.Error(errors.Wrapf(err, "failed to migrate app_spec app %v sequece %v", version.appID, version.sequence))
+		}
+	}
+
+	return nil
+}
diff --git a/kotsadm/pkg/store/s3pg/version_store.go b/kotsadm/pkg/store/s3pg/version_store.go
index 6c5b67547..5fe385111 100644
--- a/kotsadm/pkg/store/s3pg/version_store.go
+++ b/kotsadm/pkg/store/s3pg/version_store.go
@@ -522,15 +522,16 @@ func (s S3PGStore) addAppVersionToDownstream(tx *sql.Tx, appID string, clusterID
 
 func (s S3PGStore) GetAppVersion(appID string, sequence int64) (*versiontypes.AppVersion, error) {
 	db := persistence.MustGetPGSession()
-	query := `select sequence, created_at, status, applied_at, kots_installation_spec from app_version where app_id = $1 and sequence = $2`
+	query := `select sequence, created_at, status, applied_at, kots_installation_spec, kots_app_spec from app_version where app_id = $1 and sequence = $2`
 	row := db.QueryRow(query, appID, sequence)
 
 	var status sql.NullString
 	var deployedAt sql.NullTime
 	var installationSpec sql.NullString
+	var kotsAppSpec sql.NullString
 
 	v := versiontypes.AppVersion{}
-	if err := row.Scan(&v.Sequence, &v.CreatedOn, &status, &deployedAt, &installationSpec); err != nil {
+	if err := row.Scan(&v.Sequence, &v.CreatedOn, &status, &deployedAt, &installationSpec, &kotsAppSpec); err != nil {
 		if err == sql.ErrNoRows {
 			return nil, ErrNotFound
 		}
@@ -539,16 +540,28 @@ func (s S3PGStore) GetAppVersion(appID string, sequence int64) (*versiontypes.Ap
 
 	kotsKinds := kotsutil.KotsKinds{}
 
+	// why is this a nullstring but we don't check if it's null?
 	installation, err := kotsutil.LoadInstallationFromContents([]byte(installationSpec.String))
 	if err != nil {
 		return nil, errors.Wrap(err, "failed to read installation spec")
 	}
 	kotsKinds.Installation = *installation
 
+	if kotsAppSpec.Valid {
+		kotsApp, err := kotsutil.LoadKotsAppFromContents([]byte(kotsAppSpec.String))
+		if err != nil {
+			return nil, errors.Wrap(err, "failed to read kotsapp spec")
+		}
+		if kotsApp != nil {
+			kotsKinds.KotsApplication = *kotsApp
+		}
+	}
+
 	if deployedAt.Valid {
 		v.DeployedAt = &deployedAt.Time
 	}
 
+	v.KOTSKinds = &kotsKinds
 	v.Status = status.String
 
 	return &v, nil
diff --git a/kotsadm/pkg/store/store_interface.go b/kotsadm/pkg/store/store_interface.go
index afb430e9e..b6a9a224f 100644
--- a/kotsadm/pkg/store/store_interface.go
+++ b/kotsadm/pkg/store/store_interface.go
@@ -23,6 +23,7 @@ import (
 )
 
 type KOTSStore interface {
+	Migrations
 	RegistryStore
 	SupportBundleStore
 	PreflightStore
@@ -43,6 +44,10 @@ type KOTSStore interface {
 	IsNotFound(err error) bool
 }
 
+type Migrations interface {
+	RunMigrations()
+}
+
 type RegistryStore interface {
 	GetRegistryDetailsForApp(appID string) (*registrytypes.RegistrySettings, error)
 	UpdateRegistry(appID string, hostname string, username string, password string, namespace string) error
diff --git a/kotsadm/pkg/supportbundle/supportbundle.go b/kotsadm/pkg/supportbundle/supportbundle.go
index c56df512b..6aa455c08 100644
--- a/kotsadm/pkg/supportbundle/supportbundle.go
+++ b/kotsadm/pkg/supportbundle/supportbundle.go
@@ -1,17 +1,48 @@
 package supportbundle
 
 import (
+	"archive/tar"
+	"bytes"
+	"context"
 	"encoding/json"
+	"fmt"
+	"io"
 	"io/ioutil"
+	"net/url"
 	"os"
 	"path/filepath"
+	"strings"
+	"time"
 
 	"github.com/mholt/archiver"
 	"github.com/pkg/errors"
+	apptypes "github.com/replicatedhq/kots/kotsadm/pkg/app/types"
+	"github.com/replicatedhq/kots/kotsadm/pkg/license"
+	"github.com/replicatedhq/kots/kotsadm/pkg/logger"
 	"github.com/replicatedhq/kots/kotsadm/pkg/persistence"
+	"github.com/replicatedhq/kots/kotsadm/pkg/registry"
+	"github.com/replicatedhq/kots/kotsadm/pkg/render/helper"
+	"github.com/replicatedhq/kots/kotsadm/pkg/snapshot"
 	"github.com/replicatedhq/kots/kotsadm/pkg/store"
 	"github.com/replicatedhq/kots/kotsadm/pkg/supportbundle/types"
+	"github.com/replicatedhq/kots/kotskinds/client/kotsclientset/scheme"
+	"github.com/replicatedhq/kots/pkg/kotsutil"
+	"github.com/replicatedhq/kots/pkg/template"
+	troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
 	"github.com/segmentio/ksuid"
+	"go.uber.org/multierr"
+	corev1 "k8s.io/api/core/v1"
+	kuberneteserrors "k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	serializer "k8s.io/apimachinery/pkg/runtime/serializer/json"
+	"k8s.io/apimachinery/pkg/util/rand"
+	"k8s.io/client-go/kubernetes"
+	"sigs.k8s.io/controller-runtime/pkg/client/config"
+)
+
+const (
+	SpecDataKey = "support-bundle-spec"
 )
 
 // Collect will queue collection of a new support bundle
@@ -90,3 +121,516 @@ func ClearPending(id string) error {
 
 	return nil
 }
+
+func GetSpecSecretName(appSlug string) string {
+	return fmt.Sprintf("kotsadm-%s-supportbundle", appSlug)
+}
+
+func GetSpecURI(appSlug string) string {
+	return fmt.Sprintf("secret/%s/%s", os.Getenv("POD_NAMESPACE"), GetSpecSecretName(appSlug))
+}
+
+func GetBundleCommand(appSlug string) []string {
+	comamnd := []string{
+		"curl https://krew.sh/support-bundle | bash",
+		fmt.Sprintf("kubectl support-bundle %s\n", GetSpecURI(appSlug)),
+	}
+
+	return comamnd
+}
+
+func CreateRenderedSpec(appID string, sequence int64, origin string, inCluster bool, supportBundle *troubleshootv1beta2.SupportBundle) error {
+	builtBundle := supportBundle.DeepCopy()
+	if builtBundle == nil {
+		builtBundle = &troubleshootv1beta2.SupportBundle{
+			TypeMeta: v1.TypeMeta{
+				Kind:       "SupportBundle",
+				APIVersion: "troubleshoot.sh/v1beta2",
+			},
+			ObjectMeta: v1.ObjectMeta{
+				Name: "default-supportbundle",
+			},
+		}
+	}
+
+	app, err := store.GetStore().GetApp(appID)
+	if err != nil {
+		return errors.Wrap(err, "failed to get app")
+	}
+
+	err = injectDefaults(app, origin, inCluster, builtBundle)
+	if err != nil {
+		return errors.Wrap(err, "failed to inject defaults")
+	}
+
+	s := serializer.NewYAMLSerializer(serializer.DefaultMetaFactory, scheme.Scheme, scheme.Scheme)
+	var b bytes.Buffer
+	if err := s.Encode(builtBundle, &b); err != nil {
+		return errors.Wrap(err, "failed to encode support bundle")
+	}
+
+	templatedSpec := b.Bytes()
+
+	renderedSpec, err := helper.RenderAppFile(app, &sequence, templatedSpec)
+	if err != nil {
+		return errors.Wrap(err, "failed render support bundle spec")
+	}
+
+	// unmarshal the spec, look for image replacements in collectors and then remarshal
+	// we do this after template rendering to support templating and then replacement
+	supportBundle, err = kotsutil.LoadSupportBundleFromContents(renderedSpec)
+	if err != nil {
+		return errors.Wrap(err, "failed to unmarshal rendered support bundle spec")
+	}
+
+	archiveDir, err := store.GetStore().GetAppVersionArchive(appID, sequence)
+	if err != nil {
+		return errors.Wrap(err, "failed to get app version archive")
+	}
+	defer os.RemoveAll(archiveDir)
+
+	kotsKinds, err := kotsutil.LoadKotsKindsFromPath(archiveDir)
+	if err != nil {
+		return errors.Wrap(err, "failed to load rendered kots kinds")
+	}
+
+	registrySettings, err := store.GetStore().GetRegistryDetailsForApp(appID)
+	if err != nil {
+		return errors.Wrap(err, "failed to get registry settings for app")
+	}
+
+	collectors, err := registry.UpdateCollectorSpecsWithRegistryData(supportBundle.Spec.Collectors, registrySettings, kotsKinds.Installation.Spec.KnownImages, kotsKinds.License)
+	if err != nil {
+		return errors.Wrap(err, "failed to update collectors")
+	}
+	supportBundle.Spec.Collectors = collectors
+
+	if err := s.Encode(supportBundle, &b); err != nil {
+		return errors.Wrap(err, "failed to encode support bundle")
+	}
+
+	templatedSpec = b.Bytes()
+
+	cfg, err := config.GetConfig()
+	if err != nil {
+		return errors.Wrap(err, "failed to get cluster config")
+	}
+
+	clientset, err := kubernetes.NewForConfig(cfg)
+	if err != nil {
+		return errors.Wrap(err, "failed to create clientset")
+	}
+
+	secretName := GetSpecSecretName(app.Slug)
+
+	existingSecret, err := clientset.CoreV1().Secrets(os.Getenv("POD_NAMESPACE")).Get(context.TODO(), secretName, metav1.GetOptions{})
+	if err != nil && !kuberneteserrors.IsNotFound(err) {
+		return errors.Wrap(err, "failed to read support bundle secret")
+	} else if kuberneteserrors.IsNotFound(err) {
+		secret := &corev1.Secret{
+			TypeMeta: metav1.TypeMeta{
+				APIVersion: "v1",
+				Kind:       "Secret",
+			},
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      secretName,
+				Namespace: os.Getenv("POD_NAMESPACE"),
+			},
+			Data: map[string][]byte{
+				SpecDataKey: renderedSpec,
+			},
+		}
+
+		_, err = clientset.CoreV1().Secrets(os.Getenv("POD_NAMESPACE")).Create(context.TODO(), secret, metav1.CreateOptions{})
+		if err != nil {
+			return errors.Wrap(err, "failed to create support bundle secret")
+		}
+
+		return nil
+	}
+
+	if existingSecret.Data == nil {
+		existingSecret.Data = map[string][]byte{}
+	}
+	existingSecret.Data[SpecDataKey] = renderedSpec
+
+	_, err = clientset.CoreV1().Secrets(os.Getenv("POD_NAMESPACE")).Update(context.TODO(), existingSecret, metav1.UpdateOptions{})
+	if err != nil {
+		return errors.Wrap(err, "failed to update support bundle secret")
+	}
+
+	return nil
+}
+
+func injectDefaults(app *apptypes.App, origin string, inCluster bool, supportBundle *troubleshootv1beta2.SupportBundle) error {
+	populateNamespaces(supportBundle)
+
+	// determine an upload URL
+	var uploadURL string
+	var redactURL string
+	randomBundleID := strings.ToLower(rand.String(32))
+	if origin != "" {
+		uploadURL = fmt.Sprintf("%s/api/v1/troubleshoot/%s/%s", origin, app.ID, randomBundleID)
+		redactURL = fmt.Sprintf("%s/api/v1/troubleshoot/supportbundle/%s/redactions", origin, randomBundleID)
+	} else if inCluster {
+		uploadURL = fmt.Sprintf("%s/api/v1/troubleshoot/%s/%s", fmt.Sprintf("http://kotsadm.%s.svc.cluster.local:3000", os.Getenv("POD_NAMESPACE")), app.ID, randomBundleID)
+		redactURL = fmt.Sprintf("%s/api/v1/troubleshoot/supportbundle/%s/redactions", fmt.Sprintf("http://kotsadm.%s.svc.cluster.local:3000", os.Getenv("POD_NAMESPACE")), randomBundleID)
+	} else {
+		uploadURL = fmt.Sprintf("%s/api/v1/troubleshoot/%s/%s", os.Getenv("API_ADVERTISE_ENDPOINT"), app.ID, randomBundleID)
+		redactURL = fmt.Sprintf("%s/api/v1/troubleshoot/supportbundle/%s/redactions", os.Getenv("API_ADVERTISE_ENDPOINT"), randomBundleID)
+	}
+
+	addDefaultTroubleshoot(supportBundle, app)
+
+	supportBundle.Spec.AfterCollection = []*troubleshootv1beta2.AfterCollection{
+		{
+			UploadResultsTo: &troubleshootv1beta2.ResultRequest{
+				URI:       uploadURL,
+				Method:    "PUT",
+				RedactURI: redactURL,
+			},
+		},
+	}
+
+	return nil
+}
+
+// if a namespace is not set for a secret/run/logs/exec/copy collector, set it to the current namespace
+func populateNamespaces(supportBundle *troubleshootv1beta2.SupportBundle) {
+	if supportBundle == nil || supportBundle.Spec.Collectors == nil {
+		return
+	}
+
+	builder := template.Builder{}
+	builder.AddCtx(template.StaticCtx{})
+
+	ns := func(ns string) string {
+		templated, err := builder.RenderTemplate("ns", ns)
+		if err != nil {
+			logger.Error(err)
+		}
+		if templated != "" {
+			return templated
+		}
+		return os.Getenv("POD_NAMESPACE")
+	}
+
+	collects := []*troubleshootv1beta2.Collect{}
+	for _, collect := range supportBundle.Spec.Collectors {
+		if collect.Secret != nil {
+			collect.Secret.Namespace = ns(collect.Secret.Namespace)
+		}
+		if collect.Run != nil {
+			collect.Run.Namespace = ns(collect.Run.Namespace)
+		}
+		if collect.Logs != nil {
+			collect.Logs.Namespace = ns(collect.Logs.Namespace)
+		}
+		if collect.Exec != nil {
+			collect.Exec.Namespace = ns(collect.Exec.Namespace)
+		}
+		if collect.Copy != nil {
+			collect.Copy.Namespace = ns(collect.Copy.Namespace)
+		}
+		collects = append(collects, collect)
+	}
+	supportBundle.Spec.Collectors = collects
+}
+
+func addDefaultTroubleshoot(supportBundle *troubleshootv1beta2.SupportBundle, app *apptypes.App) *troubleshootv1beta2.SupportBundle {
+	if supportBundle.Spec.Collectors == nil {
+		supportBundle.Spec.Collectors = make([]*troubleshootv1beta2.Collect, 0)
+	}
+
+	licenseData, err := license.GetCurrentLicenseString(app)
+	if err != nil {
+		logger.Errorf("Failed to load license data: %v", err)
+	}
+
+	if licenseData != "" {
+		supportBundle.Spec.Collectors = append(supportBundle.Spec.Collectors, &troubleshootv1beta2.Collect{
+			Data: &troubleshootv1beta2.Data{
+				CollectorMeta: troubleshootv1beta2.CollectorMeta{
+					CollectorName: "license.yaml",
+				},
+				Name: "kots/admin-console",
+				Data: licenseData,
+			},
+		})
+	}
+
+	supportBundle.Spec.Collectors = append(supportBundle.Spec.Collectors, &troubleshootv1beta2.Collect{
+		Secret: &troubleshootv1beta2.Secret{
+			CollectorMeta: troubleshootv1beta2.CollectorMeta{
+				CollectorName: "kotsadm-replicated-registry",
+			},
+			SecretName:   "kotsadm-replicated-registry",
+			Namespace:    os.Getenv("POD_NAMESPACE"),
+			Key:          ".dockerconfigjson",
+			IncludeValue: false,
+		},
+	})
+
+	supportBundle.Spec.Collectors = append(supportBundle.Spec.Collectors, makeDbCollectors()...)
+	supportBundle.Spec.Collectors = append(supportBundle.Spec.Collectors, makeKotsadmCollectors()...)
+	supportBundle.Spec.Collectors = append(supportBundle.Spec.Collectors, makeRookCollectors()...)
+	supportBundle.Spec.Collectors = append(supportBundle.Spec.Collectors, makeKurlCollectors()...)
+	supportBundle.Spec.Collectors = append(supportBundle.Spec.Collectors, makeVeleroCollectors()...)
+
+	apps := []*apptypes.App{}
+	if app != nil {
+		apps = append(apps, app)
+	} else {
+		var err error
+		apps, err = store.GetStore().ListInstalledApps()
+		if err != nil {
+			logger.Errorf("Failed to list installed apps: %v", err)
+		}
+	}
+
+	if len(apps) > 0 {
+		collectors, err := makeAppVersionArchiveCollectors(apps)
+		if err != nil {
+			logger.Errorf("Failed to make app version archive collectors: %v", err)
+		}
+		supportBundle.Spec.Collectors = append(supportBundle.Spec.Collectors, collectors...)
+	}
+
+	return supportBundle
+}
+
+func makeDbCollectors() []*troubleshootv1beta2.Collect {
+	dbCollectors := []*troubleshootv1beta2.Collect{}
+
+	pgConnectionString := os.Getenv("POSTGRES_URI")
+	parsedPg, err := url.Parse(pgConnectionString)
+	if err == nil {
+		username := "kotsadm"
+		if parsedPg.User != nil {
+			username = parsedPg.User.Username()
+		}
+		dbCollectors = append(dbCollectors, &troubleshootv1beta2.Collect{
+			Exec: &troubleshootv1beta2.Exec{
+				CollectorMeta: troubleshootv1beta2.CollectorMeta{
+					CollectorName: "kotsadm-postgres-db",
+				},
+				Name:          "kots/admin-console",
+				Selector:      []string{fmt.Sprintf("app=%s", parsedPg.Host)},
+				Namespace:     os.Getenv("POD_NAMESPACE"),
+				ContainerName: parsedPg.Host,
+				Command:       []string{"pg_dump"},
+				Args:          []string{"-U", username},
+				Timeout:       "10s",
+			},
+		})
+	}
+	return dbCollectors
+}
+
+func makeKotsadmCollectors() []*troubleshootv1beta2.Collect {
+	names := []string{
+		"kotsadm-postgres",
+		"kotsadm",
+		"kotsadm-operator",
+		"kurl-proxy-kotsadm",
+	}
+	rookCollectors := []*troubleshootv1beta2.Collect{}
+	for _, name := range names {
+		rookCollectors = append(rookCollectors, &troubleshootv1beta2.Collect{
+			Logs: &troubleshootv1beta2.Logs{
+				CollectorMeta: troubleshootv1beta2.CollectorMeta{
+					CollectorName: name,
+				},
+				Name:      "kots/admin-console",
+				Selector:  []string{fmt.Sprintf("app=%s", name)},
+				Namespace: os.Getenv("POD_NAMESPACE"),
+			},
+		})
+	}
+	return rookCollectors
+}
+
+func makeRookCollectors() []*troubleshootv1beta2.Collect {
+	names := []string{
+		"rook-ceph-agent",
+		"rook-ceph-mgr",
+		"rook-ceph-mon",
+		"rook-ceph-operator",
+		"rook-ceph-osd",
+		"rook-ceph-osd-prepare",
+		"rook-ceph-rgw",
+		"rook-discover",
+	}
+	rookCollectors := []*troubleshootv1beta2.Collect{}
+	for _, name := range names {
+		rookCollectors = append(rookCollectors, &troubleshootv1beta2.Collect{
+			Logs: &troubleshootv1beta2.Logs{
+				CollectorMeta: troubleshootv1beta2.CollectorMeta{
+					CollectorName: name,
+				},
+				Name:      "kots/rook",
+				Selector:  []string{fmt.Sprintf("app=%s", name)},
+				Namespace: "rook-ceph",
+			},
+		})
+	}
+	return rookCollectors
+}
+
+func makeKurlCollectors() []*troubleshootv1beta2.Collect {
+	names := []string{
+		"registry",
+	}
+	rookCollectors := []*troubleshootv1beta2.Collect{}
+	for _, name := range names {
+		rookCollectors = append(rookCollectors, &troubleshootv1beta2.Collect{
+			Logs: &troubleshootv1beta2.Logs{
+				CollectorMeta: troubleshootv1beta2.CollectorMeta{
+					CollectorName: name,
+				},
+				Name:      "kots/kurl",
+				Selector:  []string{fmt.Sprintf("app=%s", name)},
+				Namespace: "kurl",
+			},
+		})
+	}
+	return rookCollectors
+}
+
+func makeVeleroCollectors() []*troubleshootv1beta2.Collect {
+	collectors := []*troubleshootv1beta2.Collect{}
+
+	veleroNamespace, err := snapshot.DetectVeleroNamespace()
+	if err != nil {
+		logger.Error(err)
+		return collectors
+	}
+
+	if veleroNamespace == "" {
+		return collectors
+	}
+
+	selectors := []string{
+		"component=velero",
+		"app.kubernetes.io/name=velero",
+	}
+
+	for _, selector := range selectors {
+		collectors = append(collectors, &troubleshootv1beta2.Collect{
+			Logs: &troubleshootv1beta2.Logs{
+				CollectorMeta: troubleshootv1beta2.CollectorMeta{
+					CollectorName: "velero",
+				},
+				Name:      "velero",
+				Selector:  []string{selector},
+				Namespace: veleroNamespace,
+			},
+		})
+	}
+
+	return collectors
+}
+
+func makeAppVersionArchiveCollectors(apps []*apptypes.App) ([]*troubleshootv1beta2.Collect, error) {
+	dirPrefix, err := ioutil.TempDir("", "app-version-archive")
+	if err != nil {
+		return nil, errors.Wrap(err, "failed to create temp dir")
+	}
+	go func() {
+		<-time.After(10 * time.Minute)
+		_ = os.RemoveAll(dirPrefix)
+	}()
+
+	collectors := []*troubleshootv1beta2.Collect{}
+	for _, app := range apps {
+		collector, err := makeAppVersionArchiveCollector(app, dirPrefix)
+		if err != nil {
+			err = multierr.Append(err, errors.Wrapf(err, "failed to make collector for app %s", app.Slug))
+		} else {
+			collectors = append(collectors, collector)
+		}
+	}
+
+	return collectors, err
+}
+
+func makeAppVersionArchiveCollector(app *apptypes.App, dirPrefix string) (*troubleshootv1beta2.Collect, error) {
+	fileName := filepath.Join(dirPrefix, fmt.Sprintf("%s.tar", app.Slug))
+	archive, err := os.Create(fileName)
+	if err != nil {
+		return nil, errors.Wrapf(err, "failed to create temp file %s", fileName)
+	}
+
+	tempPath, err := store.GetStore().GetAppVersionArchive(app.ID, app.CurrentSequence)
+	if err != nil {
+		return nil, errors.Wrap(err, "failed to get app version archive")
+	}
+	defer os.RemoveAll(tempPath)
+
+	tarWriter := tar.NewWriter(archive)
+	defer tarWriter.Close()
+
+	// only upstream includes files that don't contain any secrets
+	archivePath := filepath.Join(tempPath, "upstream")
+
+	err = filepath.Walk(archivePath, func(path string, fi os.FileInfo, err error) error {
+		if err != nil {
+			return err
+		}
+		if archivePath == path {
+			// root dir itself is the first item in the walk
+			return nil
+		}
+
+		trimmedPath := strings.TrimPrefix(path, archivePath)
+
+		// do not include userdata in archive
+		if filepath.HasPrefix(trimmedPath, "/userdata") {
+			return nil
+		}
+
+		tarHeader, err := tar.FileInfoHeader(fi, "")
+		if err != nil {
+			return errors.Wrapf(err, "failed to get tar header from file info header for file %s", trimmedPath)
+		}
+		tarHeader.Name = trimmedPath
+
+		if err := tarWriter.WriteHeader(tarHeader); err != nil {
+			return errors.Wrapf(err, "failed to write tar header for file %s", trimmedPath)
+		}
+
+		if fi.IsDir() {
+			return nil
+		}
+
+		file, err := os.Open(path)
+		if err != nil {
+			return errors.Wrapf(err, "failed to open file %s", trimmedPath)
+		}
+		defer file.Close()
+
+		if _, err := io.Copy(tarWriter, file); err != nil {
+			return errors.Wrapf(err, "failed to write file %s contents", trimmedPath)
+		}
+
+		return nil
+	})
+	if err != nil {
+		return nil, errors.Wrapf(err, "failed to walk archive dir %s", archivePath)
+	}
+
+	return &troubleshootv1beta2.Collect{
+		Copy: &troubleshootv1beta2.Copy{
+			CollectorMeta: troubleshootv1beta2.CollectorMeta{
+				CollectorName: fmt.Sprintf("spec-%s", app.Slug),
+			},
+			Selector: []string{
+				"app=kotsadm", // can we assume this?
+			},
+			Namespace:     os.Getenv("POD_NAMESPACE"),
+			ContainerName: "kotsadm", // can we assume this? kotsadm-api
+			ContainerPath: fileName,
+			Name:          fmt.Sprintf("kots/admin-console/app/%s", app.Slug),
+		},
+	}, nil
+}
diff --git a/kotsadm/pkg/updatechecker/updatechecker.go b/kotsadm/pkg/updatechecker/updatechecker.go
index e12207771..37e5113bf 100644
--- a/kotsadm/pkg/updatechecker/updatechecker.go
+++ b/kotsadm/pkg/updatechecker/updatechecker.go
@@ -9,6 +9,8 @@ import (
 	"github.com/pkg/errors"
 	"github.com/replicatedhq/kots/kotsadm/pkg/app"
 	"github.com/replicatedhq/kots/kotsadm/pkg/downstream"
+	"github.com/replicatedhq/kots/kotsadm/pkg/k8s"
+	"github.com/replicatedhq/kots/kotsadm/pkg/kurl"
 	"github.com/replicatedhq/kots/kotsadm/pkg/license"
 	"github.com/replicatedhq/kots/kotsadm/pkg/logger"
 	"github.com/replicatedhq/kots/kotsadm/pkg/store"
@@ -16,6 +18,7 @@ import (
 	"github.com/replicatedhq/kots/kotsadm/pkg/version"
 	"github.com/replicatedhq/kots/pkg/kotsutil"
 	kotspull "github.com/replicatedhq/kots/pkg/pull"
+	upstreamtypes "github.com/replicatedhq/kots/pkg/upstream/types"
 	cron "github.com/robfig/cron/v3"
 	"go.uber.org/zap"
 )
@@ -199,33 +202,14 @@ func CheckForUpdates(appID string, deploy bool) (int64, error) {
 		Silent:              false,
 	}
 
-	// get info about the deployed app version for reporting purposes
-	downstreams, err := store.GetStore().ListDownstreamsForApp(a.ID)
+	// add info for reporting purposes
+	r, err := GetReportingInfo(a.ID)
 	if err != nil {
-		return 0, errors.Wrap(err, "failed to list downstreams for app")
-	}
-
-	deployedAppSequence, err := downstream.GetCurrentParentSequence(a.ID, downstreams[0].ClusterID)
-	if err != nil {
-		return 0, errors.Wrap(err, "failed to get current downstream parent sequence")
-	}
-
-	if deployedAppSequence != -1 {
-		deployedArchiveDir, err := store.GetStore().GetAppVersionArchive(a.ID, deployedAppSequence)
-		if err != nil {
-			return 0, errors.Wrap(err, "failed to get app version archive")
-		}
-
-		deployedKotsKinds, err := kotsutil.LoadKotsKindsFromPath(deployedArchiveDir)
-		if err != nil {
-			return 0, errors.Wrap(err, "failed to load kotskinds from path")
-		}
-
-		getUpdatesOptions.DownstreamCursor = deployedKotsKinds.Installation.Spec.UpdateCursor
-		getUpdatesOptions.DownstreamChannelID = deployedKotsKinds.Installation.Spec.ChannelID
-		getUpdatesOptions.DownstreamChannelName = deployedKotsKinds.Installation.Spec.ChannelName
+		logger.Error(errors.Wrap(err, "failed to get reporting info"))
 	}
+	getUpdatesOptions.ReportingInfo = r
 
+	// get updates
 	updates, err := kotspull.GetUpdates(fmt.Sprintf("replicated://%s", kotsKinds.License.Spec.AppSlug), getUpdatesOptions)
 	if err != nil {
 		return 0, errors.Wrap(err, "failed to get updates")
@@ -305,3 +289,63 @@ func CheckForUpdates(appID string, deploy bool) (int64, error) {
 
 	return availableUpdates, nil
 }
+
+func GetReportingInfo(appID string) (*upstreamtypes.ReportingInfo, error) {
+	r := upstreamtypes.ReportingInfo{
+		InstanceID: appID,
+	}
+
+	downstreams, err := store.GetStore().ListDownstreamsForApp(appID)
+	if err != nil {
+		return nil, errors.Wrap(err, "failed to list downstreams for app")
+	}
+	if len(downstreams) == 0 {
+		return nil, errors.New("no downstreams found for app")
+	}
+	r.ClusterID = downstreams[0].ClusterID
+
+	deployedAppSequence, err := downstream.GetCurrentParentSequence(appID, downstreams[0].ClusterID)
+	if err != nil {
+		return nil, errors.Wrap(err, "failed to get current downstream parent sequence")
+	}
+
+	// info about the deployed app sequence
+	if deployedAppSequence != -1 {
+		deployedArchiveDir, err := store.GetStore().GetAppVersionArchive(appID, deployedAppSequence)
+		if err != nil {
+			return nil, errors.Wrap(err, "failed to get app version archive")
+		}
+
+		deployedKotsKinds, err := kotsutil.LoadKotsKindsFromPath(deployedArchiveDir)
+		if err != nil {
+			return nil, errors.Wrap(err, "failed to load kotskinds from path")
+		}
+
+		r.DownstreamCursor = deployedKotsKinds.Installation.Spec.UpdateCursor
+		r.DownstreamChannelID = deployedKotsKinds.Installation.Spec.ChannelID
+		r.DownstreamChannelName = deployedKotsKinds.Installation.Spec.ChannelName
+	}
+
+	// get kubernetes cluster version
+	clientset, err := k8s.Clientset()
+	if err != nil {
+		return nil, errors.Wrap(err, "failed to create kubernetes clientset")
+	}
+	k8sVersion, err := clientset.Discovery().ServerVersion()
+	if err != nil {
+		return nil, errors.Wrap(err, "failed to get kubernetes server version")
+	}
+	r.K8sVersion = k8sVersion.GitVersion
+
+	// get app status
+	appStatus, err := store.GetStore().GetAppStatus(appID)
+	if err != nil {
+		return nil, errors.Wrap(err, "failed to get app status")
+	}
+	r.AppStatus = string(appStatus.State)
+
+	// check if embedded cluster
+	r.IsKurl = kurl.IsKurl()
+
+	return &r, nil
+}
diff --git a/kotsadm/pkg/version/version.go b/kotsadm/pkg/version/version.go
index 1b1f6912b..81aa473ab 100644
--- a/kotsadm/pkg/version/version.go
+++ b/kotsadm/pkg/version/version.go
@@ -1,18 +1,26 @@
 package version
 
 import (
+	"context"
 	"database/sql"
 	"fmt"
+	"os"
 	"time"
 
 	"github.com/pkg/errors"
 	"github.com/replicatedhq/kots/kotsadm/pkg/gitops"
+	"github.com/replicatedhq/kots/kotsadm/pkg/k8s"
+	"github.com/replicatedhq/kots/kotsadm/pkg/logger"
 	"github.com/replicatedhq/kots/kotsadm/pkg/persistence"
 	"github.com/replicatedhq/kots/kotsadm/pkg/secrets"
 	"github.com/replicatedhq/kots/kotsadm/pkg/store"
 	"github.com/replicatedhq/kots/kotsadm/pkg/version/types"
 	kotsv1beta1 "github.com/replicatedhq/kots/kotskinds/apis/kots/v1beta1"
 	"github.com/replicatedhq/kots/pkg/kotsutil"
+	"go.uber.org/zap"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/labels"
 	"k8s.io/client-go/kubernetes/scheme"
 	applicationv1beta1 "sigs.k8s.io/application/api/v1beta1"
 )
@@ -208,53 +216,78 @@ func GetRealizedLinksFromAppSpec(appID string, sequence int64) ([]types.Realized
 }
 
 func GetForwardedPortsFromAppSpec(appID string, sequence int64) ([]types.ForwardedPort, error) {
-	db := persistence.MustGetPGSession()
-	query := `select app_spec, kots_app_spec from app_version where app_id = $1 and sequence = $2`
-	row := db.QueryRow(query, appID, sequence)
+	appVersion, err := store.GetStore().GetAppVersion(appID, sequence)
+	if err != nil {
+		return nil, errors.Wrap(err, "failed to get app version")
+	}
 
-	var appSpecStr sql.NullString
-	var kotsAppSpecStr sql.NullString
-	if err := row.Scan(&appSpecStr, &kotsAppSpecStr); err != nil {
-		if err == sql.ErrNoRows {
-			return []types.ForwardedPort{}, nil
-		}
-		return nil, errors.Wrap(err, "failed to scan")
+	if appVersion.KOTSKinds == nil {
+		return nil, errors.Wrap(err, "failed to get kots kinds for app")
 	}
 
-	if appSpecStr.String == "" || kotsAppSpecStr.String == "" {
+	kotsAppSpec := appVersion.KOTSKinds.KotsApplication
+
+	if len(kotsAppSpec.Spec.ApplicationPorts) == 0 {
 		return []types.ForwardedPort{}, nil
 	}
 
-	decode := scheme.Codecs.UniversalDeserializer().Decode
-	obj, _, err := decode([]byte(appSpecStr.String), nil, nil)
-	if err != nil {
-		return nil, errors.Wrap(err, "failed to decode app spec yaml")
-	}
-	appSpec := obj.(*applicationv1beta1.Application)
+	ports := []types.ForwardedPort{}
 
-	obj, _, err = decode([]byte(kotsAppSpecStr.String), nil, nil)
+	clientset, err := k8s.Clientset()
 	if err != nil {
-		return nil, errors.Wrap(err, "failed to decode kots app spec yaml")
+		return nil, errors.Wrap(err, "failed to get clientset")
 	}
-	kotsAppSpec := obj.(*kotsv1beta1.Application)
 
-	if len(kotsAppSpec.Spec.ApplicationPorts) == 0 {
-		return []types.ForwardedPort{}, nil
+	appNamespace := os.Getenv("POD_NAMESPACE")
+	if os.Getenv("KOTSADM_TARGET_NAMESPACE") != "" {
+		appNamespace = os.Getenv("KOTSADM_TARGET_NAMESPACE")
 	}
 
-	ports := []types.ForwardedPort{}
-	for _, link := range appSpec.Spec.Descriptor.Links {
-		for _, port := range kotsAppSpec.Spec.ApplicationPorts {
-			if port.ApplicationURL == link.URL {
-				ports = append(ports, types.ForwardedPort{
-					ServiceName:    port.ServiceName,
-					ServicePort:    port.ServicePort,
-					LocalPort:      port.LocalPort,
-					ApplicationURL: port.ApplicationURL,
-				})
+	// To forward the ports, we need to have the port listed
+	// in the kots spec only
+	for _, port := range kotsAppSpec.Spec.ApplicationPorts {
+		// make a best effort to not return ports to services that are not yet ready
+		// this is best effort because the service could restart at any time
+		// and the RBAC persona that this api is running as does not match
+		// the users RBAC persona. Finally, this will not work in a gitops-application
+		// unless it's deployed to the same namespace as the admin console
+		// This has always been a limitation though, we need to design for this
+
+		svc, err := clientset.CoreV1().Services(appNamespace).Get(context.TODO(), port.ServiceName, metav1.GetOptions{})
+		if err != nil {
+			logger.Error(errors.Wrapf(err, "failed to get service to check status, namespace = %s", os.Getenv("POD_NAMESPACE")))
+			continue
+		}
+
+		options := metav1.ListOptions{LabelSelector: labels.SelectorFromSet(svc.Spec.Selector).String()}
+		podList, err := clientset.CoreV1().Pods(appNamespace).List(context.TODO(), options)
+		if err != nil {
+			logger.Error(errors.Wrap(err, "failed to list pods in service"))
+			continue
+		}
+
+		hasReadyPod := false
+		for _, pod := range podList.Items {
+			if pod.Status.Phase == corev1.PodRunning {
+				for _, status := range pod.Status.ContainerStatuses {
+					if status.Ready {
+						hasReadyPod = true
+					}
+				}
 			}
+		}
 
+		if !hasReadyPod {
+			logger.Info("not forwarding to service because no pods are ready", zap.String("serviceName", port.ServiceName), zap.String("namespace", os.Getenv("POD_NAMESPACE")))
+			continue
 		}
+
+		ports = append(ports, types.ForwardedPort{
+			ServiceName:    port.ServiceName,
+			ServicePort:    port.ServicePort,
+			LocalPort:      port.LocalPort,
+			ApplicationURL: port.ApplicationURL,
+		})
 	}
 
 	return ports, nil
diff --git a/kotsadm/web/src/components/PreflightResultErrors.jsx b/kotsadm/web/src/components/PreflightResultErrors.jsx
index d3c0826f0..cb031c216 100644
--- a/kotsadm/web/src/components/PreflightResultErrors.jsx
+++ b/kotsadm/web/src/components/PreflightResultErrors.jsx
@@ -58,10 +58,14 @@ class PreflightResultErrors extends Component {
 
   fetchPreflightCommand = async (slug, sequence) => {
     const res = await fetch(`${window.env.API_ENDPOINT}/app/${slug}/sequence/${sequence}/preflightcommand`, {
-      method: "GET",
+      method: "POST",
       headers: {
         "Authorization": Utilities.getToken(),
-      }
+        "Content-Type": "application/json",
+      },
+      body: JSON.stringify({
+        origin: window.location.origin,
+      })
     });
     if (!res.ok) {
       throw new Error(`Unexpected status code: ${res.status}`);
@@ -80,15 +84,9 @@ class PreflightResultErrors extends Component {
       errorTitle,
       errorMsg,
       displayErrorModal,
+      command,
     } = this.state;
 
-    let command;
-    if (this.state.command) {
-      command = this.state.command.map((part) => {
-        return part.replace("API_ADDRESS", window.location.origin);
-      });
-    }
-
     return (
       <div className="flex flex1 flex-column">
         <div className="flex flex1 u-height--full u-width--full u-marginTop--5 u-marginBottom--20">
diff --git a/kotsadm/web/src/components/UploadAirgapBundle.jsx b/kotsadm/web/src/components/UploadAirgapBundle.jsx
index dbdbf246a..1b0655965 100644
--- a/kotsadm/web/src/components/UploadAirgapBundle.jsx
+++ b/kotsadm/web/src/components/UploadAirgapBundle.jsx
@@ -271,10 +271,14 @@ class UploadAirgapBundle extends React.Component {
 
   getSupportBundleCommand = async (slug) => {
     const res = await fetch(`${window.env.API_ENDPOINT}/troubleshoot/app/${slug}/supportbundlecommand`, {
-      method: "GET",
+      method: "POST",
       headers: {
         "Authorization": Utilities.getToken(),
-      }
+        "Content-Type": "application/json",
+      },
+      body: JSON.stringify({
+        origin: window.location.origin,
+      })
     });
     if (!res.ok) {
       throw new Error(`Unexpected status code: ${res.status}`);
@@ -286,7 +290,7 @@ class UploadAirgapBundle extends React.Component {
   onProgressError = async (errorMessage) => {
     const { slug } = this.props.match.params;
 
-    let supportBundleCommand = "";
+    let supportBundleCommand = [];
     try {
       supportBundleCommand = await this.getSupportBundleCommand(slug);
     } catch (err) {
@@ -377,6 +381,7 @@ class UploadAirgapBundle extends React.Component {
       preparingOnlineInstall,
       onlineInstallErrorMessage,
       viewOnlineInstallErrorMessage,
+      supportBundleCommand,
     } = this.state;
 
     const hasFile = bundleFile && !isEmpty(bundleFile);
@@ -393,13 +398,6 @@ class UploadAirgapBundle extends React.Component {
       );
     }
 
-    let supportBundleCommand;
-    if (this.state.supportBundleCommand) {
-      supportBundleCommand = this.state.supportBundleCommand.map((part) => {
-        return part.replace("API_ADDRESS", window.location.origin);
-      });
-    }
-
     let logoUri;
     let applicationName;
     if (appsListLength && appsListLength > 1) {
diff --git a/kotsadm/web/src/components/troubleshoot/GenerateSupportBundle.jsx b/kotsadm/web/src/components/troubleshoot/GenerateSupportBundle.jsx
index 9d5b291ca..28736ec74 100644
--- a/kotsadm/web/src/components/troubleshoot/GenerateSupportBundle.jsx
+++ b/kotsadm/web/src/components/troubleshoot/GenerateSupportBundle.jsx
@@ -197,6 +197,29 @@ class GenerateSupportBundle extends React.Component {
       });
   }
 
+  fetchSupportBundleCommand = async () => {
+    const { watch } = this.props;
+
+    const res = await fetch(`${window.env.API_ENDPOINT}/troubleshoot/app/${watch.slug}/supportbundlecommand`, {
+      method: "POST",
+      headers: {
+        "Authorization": Utilities.getToken(),
+        "Content-Type": "application/json",
+      },
+      body: JSON.stringify({
+        origin: window.location.origin,
+      })
+    });
+    if (!res.ok) {
+      throw new Error(`Unexpected status code: ${res.status}`);
+    }
+    const response = await res.json();
+    this.setState({
+      showRunCommand: !this.state.showRunCommand,
+      bundleCommand: response.command,
+    });
+  }
+
   toggleErrorModal = () => {
     this.setState({ displayErrorModal: !this.state.displayErrorModal });
   }
@@ -226,14 +249,8 @@ class GenerateSupportBundle extends React.Component {
     const { selectedCluster, displayUploadModal, showRunCommand, isGeneratingBundle, generateBundleErrMsg, errorMsg } = this.state;
     const { watch } = this.props;
     const watchClusters = watch.downstreams;
-    const selectedWatch = watchClusters.find(c => c.cluster.id === selectedCluster.id);
     const appTitle = watch.watchName || watch.name;
 
-    let command = selectedWatch?.bundleCommand || watch.bundleCommand;
-    if (command) {
-      command = command.replace("API_ADDRESS", window.location.origin);
-    }
-
     return (
       <div className="GenerateSupportBundle--wrapper container flex-column u-overflow--auto u-paddingTop--30 u-paddingBottom--20 alignItems--center">
         <Helmet>
@@ -273,7 +290,7 @@ class GenerateSupportBundle extends React.Component {
                       canCopy={true}
                       onCopyText={<span className="u-color--chateauGreen">Command has been copied to your clipboard</span>}
                     >
-                      {command?.split("\n")}
+                      {this.state.bundleCommand}
                     </CodeSnippet>
                   </div>
                   <div className="u-marginTop--15">
@@ -283,7 +300,7 @@ class GenerateSupportBundle extends React.Component {
                 :
                 <div>
                   <div className="u-marginTop--40">
-                    If you'd prefer, <a href="#" className="replicated-link" onClick={(e) => this.toggleShow(e, "showRunCommand")}>click here</a> to get a command to manually generate a support bundle.
+                    If you'd prefer, <a href="#" className="replicated-link" onClick={(e) => this.fetchSupportBundleCommand()}>click here</a> to get a command to manually generate a support bundle.
                   </div>
                 </div>
               }
diff --git a/pkg/docker/registry/registry.go b/pkg/docker/registry/registry.go
index 09245f475..443b4c874 100644
--- a/pkg/docker/registry/registry.go
+++ b/pkg/docker/registry/registry.go
@@ -64,7 +64,7 @@ func (r *RegistryProxyInfo) ToSlice() []string {
 	}
 }
 
-func PullSecretForRegistries(registries []string, username, password string, namespace string) (*corev1.Secret, error) {
+func PullSecretForRegistries(registries []string, username, password string, kuberneteNamespace string) (*corev1.Secret, error) {
 	dockercfgAuth := DockercfgAuth{
 		Auth: base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", username, password))),
 	}
@@ -89,7 +89,7 @@ func PullSecretForRegistries(registries []string, username, password string, nam
 		},
 		ObjectMeta: metav1.ObjectMeta{
 			Name:      "kotsadm-replicated-registry",
-			Namespace: namespace,
+			Namespace: kuberneteNamespace,
 		},
 		Type: corev1.SecretTypeDockerConfigJson,
 		Data: map[string][]byte{
diff --git a/pkg/k8sdoc/types.go b/pkg/k8sdoc/types.go
index fdd059c9e..036cab88d 100644
--- a/pkg/k8sdoc/types.go
+++ b/pkg/k8sdoc/types.go
@@ -2,6 +2,7 @@ package k8sdoc
 
 import (
 	"github.com/pkg/errors"
+	troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
 	"gopkg.in/yaml.v2"
 	corev1 "k8s.io/api/core/v1"
 )
@@ -11,6 +12,10 @@ type K8sDoc interface {
 	ListImages() []string
 }
 
+var _ K8sDoc = (*Doc)(nil)
+var _ K8sDoc = (*PodDoc)(nil)
+var _ K8sDoc = (*SupportBundleDoc)(nil)
+
 type Doc struct {
 	APIVersion string   `yaml:"apiVersion"`
 	Kind       string   `yaml:"kind"`
@@ -25,6 +30,13 @@ type PodDoc struct {
 	Spec       PodSpec  `yaml:"spec"`
 }
 
+type SupportBundleDoc struct {
+	APIVersion string                                `yaml:"apiVersion"`
+	Kind       string                                `yaml:"kind"`
+	Metadata   Metadata                              `yaml:"metadata"`
+	Spec       troubleshootv1beta2.SupportBundleSpec `yaml:"spec"`
+}
+
 type Metadata struct {
 	Name      string            `yaml:"name"`
 	Namespace string            `yaml:"namespace,omitempty"`
@@ -166,3 +178,19 @@ func (d *PodDoc) ListImages() []string {
 	}
 	return images
 }
+
+func (d *SupportBundleDoc) ListImages() []string {
+	images := make([]string, 0)
+	for _, collector := range d.Spec.Collectors {
+		if collector.Run != nil {
+			images = append(images, collector.Run.Image)
+		}
+	}
+
+	return images
+}
+
+func (d *SupportBundleDoc) PatchWithPullSecret(secret *corev1.Secret) K8sDoc {
+	// this is not a kustomize patch, so we return nil and it's ignored
+	return nil
+}
diff --git a/pkg/k8sutil/portforward.go b/pkg/k8sutil/portforward.go
index ff6159e17..3c8cb6ee4 100644
--- a/pkg/k8sutil/portforward.go
+++ b/pkg/k8sutil/portforward.go
@@ -9,8 +9,6 @@ import (
 	"net"
 	"net/http"
 	"net/url"
-	"os"
-	"path/filepath"
 	"strconv"
 	"time"
 
@@ -21,7 +19,6 @@ import (
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/labels"
-	"k8s.io/apimachinery/pkg/util/runtime"
 	"k8s.io/cli-runtime/pkg/genericclioptions"
 	"k8s.io/client-go/kubernetes"
 	"k8s.io/client-go/rest"
@@ -29,19 +26,6 @@ import (
 	"k8s.io/client-go/transport/spdy"
 )
 
-func init() {
-	// Note this changes loggers globbaly for apimachinery packages,
-	// but this is the only way to silence port forwarder.
-	f, _ := os.Create(filepath.Join(os.TempDir(), "portforward.log"))
-	logger := func(err error) {
-		if f == nil {
-			return
-		}
-		fmt.Fprintf(f, "%v\n", err)
-	}
-	runtime.ErrorHandlers = []func(error){logger}
-}
-
 func IsPortAvailable(port int) bool {
 	host := ":" + strconv.Itoa(port)
 	server, err := net.Listen("tcp", host)
@@ -194,38 +178,40 @@ func PortForward(kubernetesConfigFlags *genericclioptions.ConfigFlags, localPort
 
 				req, err := http.NewRequest("GET", uri, nil)
 				if err != nil {
-					runtime.HandleError(errors.Wrap(err, "failed to create request"))
+					log.Error(errors.Wrap(err, "failed to create request"))
 					continue
 				}
 				req.Header.Set("Accept", "application/json")
 
 				authSlug, err := auth.GetOrCreateAuthSlug(kubernetesConfigFlags, namespace)
 				if err != nil {
-					runtime.HandleError(errors.Wrap(err, "failed to get kotsadm auth slug"))
+					log.Error(errors.Wrap(err, "failed to get kotsadm auth slug"))
 					continue
 				}
 				req.Header.Add("Authorization", authSlug)
 
 				resp, err := http.DefaultClient.Do(req)
 				if err != nil {
-					runtime.HandleError(errors.Wrap(err, "failed to get ports"))
+					log.Error(errors.Wrap(err, "failed to get ports"))
 					continue
 				}
 
 				defer resp.Body.Close()
 				b, err := ioutil.ReadAll(resp.Body)
 				if err != nil {
-					runtime.HandleError(errors.Wrap(err, "failed to parse response"))
+					log.Error(errors.Wrap(err, "failed to parse response"))
 					continue
 				}
 
-				desiredAdditionalPorts := []AdditionalPort{}
-				if err := json.Unmarshal(b, &desiredAdditionalPorts); err != nil {
-					runtime.HandleError(errors.Wrap(err, "failed to decode response"))
+				response := struct {
+					DesiredAdditionalPorts []AdditionalPort `json:"ports"`
+				}{}
+				if err := json.Unmarshal(b, &response); err != nil {
+					log.Error(errors.Wrap(err, "failed to decode response"))
 					continue
 				}
 
-				for _, desiredAdditionalPort := range desiredAdditionalPorts {
+				for _, desiredAdditionalPort := range response.DesiredAdditionalPorts {
 					alreadyForwarded := false
 					for forwardedAdditionalPort := range forwardedAdditionalPorts {
 						// Avoid port conflicts by only taking the first to claim a local port
@@ -241,13 +227,14 @@ func PortForward(kubernetesConfigFlags *genericclioptions.ConfigFlags, localPort
 
 					serviceStopCh, err := ServiceForward(clientset, cfg, desiredAdditionalPort.LocalPort, desiredAdditionalPort.ServicePort, namespace, desiredAdditionalPort.ServiceName)
 					if err != nil {
-						runtime.HandleError(errors.Wrap(err, "failed to forward port"))
+						log.Error(errors.Wrap(err, fmt.Sprintf("failed to execute kubectl port-forward -n %s svc/%s %d:%d", namespace, desiredAdditionalPort.ServiceName, desiredAdditionalPort.LocalPort, desiredAdditionalPort.ServicePort)))
 						continue // try again
 					}
 					if serviceStopCh == nil {
 						// we didn't do the port forwarding, probably because the pod isn't ready.
 						// try again next loop
-						runtime.HandleError(errors.New("failed to forward port; pod not ready?"))
+						// The API doesn't return ports that aren't ready, so this is possibly rbac?
+						log.Error(errors.Errorf("failed to forward port. check that you have permission to run kubectl port-forward -n %s svc/%s %d:%d", namespace, desiredAdditionalPort.ServiceName, desiredAdditionalPort.LocalPort, desiredAdditionalPort.ServicePort))
 						continue // try again
 					}
 
diff --git a/pkg/kotsutil/kots.go b/pkg/kotsutil/kots.go
index 92152bb61..e4f7e12cc 100644
--- a/pkg/kotsutil/kots.go
+++ b/pkg/kotsutil/kots.go
@@ -393,6 +393,34 @@ func LoadInstallationFromPath(installationFilePath string) (*kotsv1beta1.Install
 	return LoadInstallationFromContents(installationData)
 }
 
+func LoadSupportBundleFromContents(data []byte) (*troubleshootv1beta2.SupportBundle, error) {
+	decode := scheme.Codecs.UniversalDeserializer().Decode
+	obj, gvk, err := decode([]byte(data), nil, nil)
+	if err != nil {
+		return nil, errors.Wrapf(err, "failed to decode support bundle data of length %d", len(data))
+	}
+
+	if gvk.Group != "troubleshoot.sh" || gvk.Version != "v1beta2" || gvk.Kind != "SupportBundle" {
+		return nil, errors.Errorf("unexpected GVK: %s", gvk.String())
+	}
+
+	return obj.(*troubleshootv1beta2.SupportBundle), nil
+}
+
+func LoadKotsAppFromContents(data []byte) (*kotsv1beta1.Application, error) {
+	decode := scheme.Codecs.UniversalDeserializer().Decode
+	obj, gvk, err := decode([]byte(data), nil, nil)
+	if err != nil {
+		return nil, errors.Wrapf(err, "failed to decode kots app data of length %d", len(data))
+	}
+
+	if gvk.Group != "kots.io" || gvk.Version != "v1beta1" || gvk.Kind != "Application" {
+		return nil, errors.Errorf("unexpected GVK: %s", gvk.String())
+	}
+
+	return obj.(*kotsv1beta1.Application), nil
+}
+
 func LoadInstallationFromContents(installationData []byte) (*kotsv1beta1.Installation, error) {
 	decode := scheme.Codecs.UniversalDeserializer().Decode
 	obj, gvk, err := decode([]byte(installationData), nil, nil)
diff --git a/pkg/midstream/write.go b/pkg/midstream/write.go
index dedb65c6c..792a38ec3 100644
--- a/pkg/midstream/write.go
+++ b/pkg/midstream/write.go
@@ -165,6 +165,9 @@ func (m *Midstream) writeObjectsWithPullSecret(options WriteOptions) error {
 
 	for _, o := range m.DocForPatches {
 		withPullSecret := o.PatchWithPullSecret(m.PullSecret)
+		if withPullSecret == nil {
+			continue
+		}
 
 		b, err := yaml.Marshal(withPullSecret)
 		if err != nil {
diff --git a/pkg/pull/peek.go b/pkg/pull/peek.go
index db4686cae..b43b925d8 100644
--- a/pkg/pull/peek.go
+++ b/pkg/pull/peek.go
@@ -5,21 +5,20 @@ import (
 	kotsv1beta1 "github.com/replicatedhq/kots/kotskinds/apis/kots/v1beta1"
 	"github.com/replicatedhq/kots/pkg/logger"
 	"github.com/replicatedhq/kots/pkg/upstream"
+	upstreamtypes "github.com/replicatedhq/kots/pkg/upstream/types"
 )
 
 type GetUpdatesOptions struct {
-	HelmRepoURI           string
-	Namespace             string
-	LocalPath             string
-	License               *kotsv1beta1.License
-	CurrentCursor         string
-	CurrentChannelID      string
-	CurrentChannelName    string
-	CurrentVersionLabel   string
-	DownstreamCursor      string
-	DownstreamChannelID   string
-	DownstreamChannelName string
-	Silent                bool
+	HelmRepoURI         string
+	Namespace           string
+	LocalPath           string
+	License             *kotsv1beta1.License
+	CurrentCursor       string
+	CurrentChannelID    string
+	CurrentChannelName  string
+	CurrentVersionLabel string
+	ReportingInfo       *upstreamtypes.ReportingInfo
+	Silent              bool
 }
 
 // GetUpdates will retrieve all later versions of the application specified in upstreamURI
@@ -33,16 +32,14 @@ func GetUpdates(upstreamURI string, getUpdatesOptions GetUpdatesOptions) ([]upst
 
 	log.Initialize()
 
-	fetchOptions := upstream.FetchOptions{}
+	fetchOptions := upstreamtypes.FetchOptions{}
 	fetchOptions.HelmRepoURI = getUpdatesOptions.HelmRepoURI
 	fetchOptions.LocalPath = getUpdatesOptions.LocalPath
 	fetchOptions.CurrentCursor = getUpdatesOptions.CurrentCursor
 	fetchOptions.CurrentChannelID = getUpdatesOptions.CurrentChannelID
 	fetchOptions.CurrentChannelName = getUpdatesOptions.CurrentChannelName
 	fetchOptions.CurrentVersionLabel = getUpdatesOptions.CurrentVersionLabel
-	fetchOptions.DownstreamCursor = getUpdatesOptions.DownstreamCursor
-	fetchOptions.DownstreamChannelID = getUpdatesOptions.DownstreamChannelID
-	fetchOptions.DownstreamChannelName = getUpdatesOptions.DownstreamChannelName
+	fetchOptions.ReportingInfo = getUpdatesOptions.ReportingInfo
 
 	if getUpdatesOptions.License != nil {
 		fetchOptions.License = getUpdatesOptions.License
diff --git a/pkg/pull/pull.go b/pkg/pull/pull.go
index f2ab96178..1a53c0170 100644
--- a/pkg/pull/pull.go
+++ b/pkg/pull/pull.go
@@ -109,14 +109,14 @@ func Pull(upstreamURI string, pullOptions PullOptions) (string, error) {
 		return "", errors.Wrap(err, "failed to parse uri")
 	}
 
-	fetchOptions := upstream.FetchOptions{
+	fetchOptions := upstreamtypes.FetchOptions{
 		HelmRepoURI:   pullOptions.HelmRepoURI,
 		RootDir:       pullOptions.RootDir,
 		UseAppDir:     pullOptions.CreateAppDir,
 		LocalPath:     pullOptions.LocalPath,
 		CurrentCursor: pullOptions.UpdateCursor,
 		AppSequence:   pullOptions.AppSequence,
-		LocalRegistry: upstream.LocalRegistry{
+		LocalRegistry: upstreamtypes.LocalRegistry{
 			Host:      pullOptions.RewriteImageOptions.Host,
 			Namespace: pullOptions.RewriteImageOptions.Namespace,
 			Username:  pullOptions.RewriteImageOptions.Username,
diff --git a/pkg/rewrite/rewrite.go b/pkg/rewrite/rewrite.go
index 7ffc8b88b..7ed0eebd2 100644
--- a/pkg/rewrite/rewrite.go
+++ b/pkg/rewrite/rewrite.go
@@ -57,14 +57,14 @@ func Rewrite(rewriteOptions RewriteOptions) error {
 		rewriteOptions.ReportWriter = ioutil.Discard
 	}
 
-	fetchOptions := &upstream.FetchOptions{
+	fetchOptions := &upstreamtypes.FetchOptions{
 		RootDir:             rewriteOptions.RootDir,
 		LocalPath:           rewriteOptions.UpstreamPath,
 		CurrentCursor:       rewriteOptions.Installation.Spec.UpdateCursor,
 		CurrentVersionLabel: rewriteOptions.Installation.Spec.VersionLabel,
 		EncryptionKey:       rewriteOptions.Installation.Spec.EncryptionKey,
 		License:             rewriteOptions.License,
-		LocalRegistry: upstream.LocalRegistry{
+		LocalRegistry: upstreamtypes.LocalRegistry{
 			Host:      rewriteOptions.RegistryEndpoint,
 			Namespace: rewriteOptions.RegistryNamespace,
 			Username:  rewriteOptions.RegistryUsername,
diff --git a/pkg/upstream/fetch.go b/pkg/upstream/fetch.go
index 43d3f4961..cd9bd696a 100644
--- a/pkg/upstream/fetch.go
+++ b/pkg/upstream/fetch.go
@@ -4,42 +4,12 @@ import (
 	"net/url"
 
 	"github.com/pkg/errors"
-	kotsv1beta1 "github.com/replicatedhq/kots/kotskinds/apis/kots/v1beta1"
 	"github.com/replicatedhq/kots/pkg/crypto"
 	"github.com/replicatedhq/kots/pkg/upstream/types"
 	"github.com/replicatedhq/kots/pkg/util"
 )
 
-type FetchOptions struct {
-	RootDir               string
-	UseAppDir             bool
-	HelmRepoName          string
-	HelmRepoURI           string
-	HelmOptions           []string
-	LocalPath             string
-	License               *kotsv1beta1.License
-	ConfigValues          *kotsv1beta1.ConfigValues
-	Airgap                *kotsv1beta1.Airgap
-	EncryptionKey         string
-	CurrentCursor         string
-	CurrentChannelID      string
-	CurrentChannelName    string
-	CurrentVersionLabel   string
-	DownstreamCursor      string
-	DownstreamChannelID   string
-	DownstreamChannelName string
-	AppSequence           int64
-	LocalRegistry         LocalRegistry
-}
-
-type LocalRegistry struct {
-	Host      string
-	Namespace string
-	Username  string
-	Password  string
-}
-
-func FetchUpstream(upstreamURI string, fetchOptions *FetchOptions) (*types.Upstream, error) {
+func FetchUpstream(upstreamURI string, fetchOptions *types.FetchOptions) (*types.Upstream, error) {
 	upstream, err := downloadUpstream(upstreamURI, fetchOptions)
 	if err != nil {
 		return nil, errors.Wrap(err, "download upstream failed")
@@ -48,7 +18,7 @@ func FetchUpstream(upstreamURI string, fetchOptions *FetchOptions) (*types.Upstr
 	return upstream, nil
 }
 
-func downloadUpstream(upstreamURI string, fetchOptions *FetchOptions) (*types.Upstream, error) {
+func downloadUpstream(upstreamURI string, fetchOptions *types.FetchOptions) (*types.Upstream, error) {
 	if !util.IsURL(upstreamURI) {
 		return readFilesFromPath(upstreamURI)
 	}
@@ -82,14 +52,14 @@ func downloadUpstream(upstreamURI string, fetchOptions *FetchOptions) (*types.Up
 	return nil, errors.Errorf("unknown protocol scheme %q", u.Scheme)
 }
 
-func pickVersionLabel(fetchOptions *FetchOptions) string {
+func pickVersionLabel(fetchOptions *types.FetchOptions) string {
 	if fetchOptions.Airgap != nil && fetchOptions.Airgap.Spec.VersionLabel != "" {
 		return fetchOptions.Airgap.Spec.VersionLabel
 	}
 	return fetchOptions.CurrentVersionLabel
 }
 
-func pickCursor(fetchOptions *FetchOptions) ReplicatedCursor {
+func pickCursor(fetchOptions *types.FetchOptions) ReplicatedCursor {
 	if fetchOptions.Airgap != nil && fetchOptions.Airgap.Spec.UpdateCursor != "" {
 		return ReplicatedCursor{
 			ChannelID:   fetchOptions.Airgap.Spec.ChannelID,
diff --git a/pkg/upstream/peek.go b/pkg/upstream/peek.go
index 6a44edc4a..a21258f87 100644
--- a/pkg/upstream/peek.go
+++ b/pkg/upstream/peek.go
@@ -4,6 +4,7 @@ import (
 	"net/url"
 
 	"github.com/pkg/errors"
+	types "github.com/replicatedhq/kots/pkg/upstream/types"
 	"github.com/replicatedhq/kots/pkg/util"
 )
 
@@ -12,7 +13,7 @@ type Update struct {
 	VersionLabel string `json:"versionLabel"`
 }
 
-func GetUpdatesUpstream(upstreamURI string, fetchOptions *FetchOptions) ([]Update, error) {
+func GetUpdatesUpstream(upstreamURI string, fetchOptions *types.FetchOptions) ([]Update, error) {
 	versions, err := getUpdatesUpstream(upstreamURI, fetchOptions)
 	if err != nil {
 		return nil, errors.Wrap(err, "download upstream failed")
@@ -21,7 +22,7 @@ func GetUpdatesUpstream(upstreamURI string, fetchOptions *FetchOptions) ([]Updat
 	return versions, nil
 }
 
-func getUpdatesUpstream(upstreamURI string, fetchOptions *FetchOptions) ([]Update, error) {
+func getUpdatesUpstream(upstreamURI string, fetchOptions *types.FetchOptions) ([]Update, error) {
 	if !util.IsURL(upstreamURI) {
 		return nil, errors.New("not implemented")
 	}
@@ -39,12 +40,7 @@ func getUpdatesUpstream(upstreamURI string, fetchOptions *FetchOptions) ([]Updat
 			ChannelName: fetchOptions.CurrentChannelName,
 			Cursor:      fetchOptions.CurrentCursor,
 		}
-		downstreamCursor := ReplicatedCursor{
-			ChannelID:   fetchOptions.DownstreamChannelID,
-			ChannelName: fetchOptions.DownstreamChannelName,
-			Cursor:      fetchOptions.DownstreamCursor,
-		}
-		return getUpdatesReplicated(u, fetchOptions.LocalPath, currentCursor, fetchOptions.CurrentVersionLabel, downstreamCursor, fetchOptions.License)
+		return getUpdatesReplicated(u, fetchOptions.LocalPath, currentCursor, fetchOptions.CurrentVersionLabel, fetchOptions.License, fetchOptions.ReportingInfo)
 	}
 	if u.Scheme == "git" {
 		// return getUpdatesGit(upstreamURI)
diff --git a/pkg/upstream/replicated.go b/pkg/upstream/replicated.go
index 790e1c2a7..b351bfa84 100644
--- a/pkg/upstream/replicated.go
+++ b/pkg/upstream/replicated.go
@@ -78,7 +78,7 @@ func (this ReplicatedCursor) Equal(other ReplicatedCursor) bool {
 	return this.ChannelName == other.ChannelName && this.Cursor == other.Cursor
 }
 
-func getUpdatesReplicated(u *url.URL, localPath string, currentCursor ReplicatedCursor, currentVersionLabel string, downstreamCursor ReplicatedCursor, license *kotsv1beta1.License) ([]Update, error) {
+func getUpdatesReplicated(u *url.URL, localPath string, currentCursor ReplicatedCursor, currentVersionLabel string, license *kotsv1beta1.License, reportingInfo *types.ReportingInfo) ([]Update, error) {
 	if localPath != "" {
 		parsedLocalRelease, err := readReplicatedAppFromLocalPath(localPath, currentCursor, currentVersionLabel)
 		if err != nil {
@@ -103,7 +103,7 @@ func getUpdatesReplicated(u *url.URL, localPath string, currentCursor Replicated
 		return nil, errors.Wrap(err, "failed to get successful head response")
 	}
 
-	pendingReleases, err := listPendingChannelReleases(replicatedUpstream, remoteLicense, currentCursor, downstreamCursor)
+	pendingReleases, err := listPendingChannelReleases(replicatedUpstream, remoteLicense, currentCursor, reportingInfo)
 	if err != nil {
 		return nil, errors.Wrap(err, "failed to list replicated app releases")
 	}
@@ -118,7 +118,7 @@ func getUpdatesReplicated(u *url.URL, localPath string, currentCursor Replicated
 	return updates, nil
 }
 
-func downloadReplicated(u *url.URL, localPath string, rootDir string, useAppDir bool, license *kotsv1beta1.License, existingConfigValues *kotsv1beta1.ConfigValues, updateCursor ReplicatedCursor, versionLabel string, cipher *crypto.AESCipher, appSequence int64, isAirgap bool, registry LocalRegistry) (*types.Upstream, error) {
+func downloadReplicated(u *url.URL, localPath string, rootDir string, useAppDir bool, license *kotsv1beta1.License, existingConfigValues *kotsv1beta1.ConfigValues, updateCursor ReplicatedCursor, versionLabel string, cipher *crypto.AESCipher, appSequence int64, isAirgap bool, registry types.LocalRegistry) (*types.Upstream, error) {
 	var release *Release
 
 	if localPath != "" {
@@ -432,7 +432,7 @@ func downloadReplicatedApp(replicatedUpstream *ReplicatedUpstream, license *kots
 	return &release, nil
 }
 
-func listPendingChannelReleases(replicatedUpstream *ReplicatedUpstream, license *kotsv1beta1.License, currentCursor ReplicatedCursor, downstreamCursor ReplicatedCursor) ([]ChannelRelease, error) {
+func listPendingChannelReleases(replicatedUpstream *ReplicatedUpstream, license *kotsv1beta1.License, currentCursor ReplicatedCursor, reportingInfo *types.ReportingInfo) ([]ChannelRelease, error) {
 	u, err := url.Parse(license.Spec.Endpoint)
 	if err != nil {
 		return nil, errors.Wrap(err, "failed to parse endpoint from license")
@@ -453,12 +453,23 @@ func listPendingChannelReleases(replicatedUpstream *ReplicatedUpstream, license
 	urlValues := url.Values{}
 	urlValues.Set("channelSequence", sequence)
 	urlValues.Add("licenseSequence", fmt.Sprintf("%d", license.Spec.LicenseSequence))
-	urlValues.Add("downstreamChannelSequence", downstreamCursor.Cursor)
 
-	if downstreamCursor.ChannelID != "" {
-		urlValues.Add("downstreamChannelID", downstreamCursor.ChannelID)
-	} else {
-		urlValues.Add("downstreamChannelName", downstreamCursor.ChannelName)
+	// reporting info
+	if reportingInfo != nil {
+		urlValues.Add("k8sVersion", reportingInfo.K8sVersion)
+		urlValues.Add("isKurl", fmt.Sprintf("%t", reportingInfo.IsKurl))
+		urlValues.Add("appStatus", reportingInfo.AppStatus)
+		urlValues.Add("clusterId", reportingInfo.ClusterID)
+		urlValues.Add("instanceId", reportingInfo.InstanceID)
+
+		if reportingInfo.DownstreamCursor != "" {
+			urlValues.Add("downstreamChannelSequence", reportingInfo.DownstreamCursor)
+		}
+		if reportingInfo.DownstreamChannelID != "" {
+			urlValues.Add("downstreamChannelId", reportingInfo.DownstreamChannelID)
+		} else if reportingInfo.DownstreamChannelName != "" {
+			urlValues.Add("downstreamChannelName", reportingInfo.DownstreamChannelName)
+		}
 	}
 
 	url := fmt.Sprintf("%s://%s/release/%s/pending?%s", u.Scheme, hostname, license.Spec.AppSlug, urlValues.Encode())
diff --git a/pkg/upstream/types/types.go b/pkg/upstream/types/types.go
index d6479dde1..15c3d9eec 100644
--- a/pkg/upstream/types/types.go
+++ b/pkg/upstream/types/types.go
@@ -3,6 +3,7 @@ package types
 import (
 	"path"
 
+	kotsv1beta1 "github.com/replicatedhq/kots/kotskinds/apis/kots/v1beta1"
 	kotsscheme "github.com/replicatedhq/kots/kotskinds/client/kotsclientset/scheme"
 	"k8s.io/client-go/kubernetes/scheme"
 )
@@ -45,6 +46,44 @@ type WriteOptions struct {
 	SharedPassword string
 }
 
+type FetchOptions struct {
+	RootDir             string
+	UseAppDir           bool
+	HelmRepoName        string
+	HelmRepoURI         string
+	HelmOptions         []string
+	LocalPath           string
+	License             *kotsv1beta1.License
+	ConfigValues        *kotsv1beta1.ConfigValues
+	Airgap              *kotsv1beta1.Airgap
+	EncryptionKey       string
+	CurrentCursor       string
+	CurrentChannelID    string
+	CurrentChannelName  string
+	CurrentVersionLabel string
+	AppSequence         int64
+	LocalRegistry       LocalRegistry
+	ReportingInfo       *ReportingInfo
+}
+
+type LocalRegistry struct {
+	Host      string
+	Namespace string
+	Username  string
+	Password  string
+}
+
+type ReportingInfo struct {
+	InstanceID            string
+	ClusterID             string
+	DownstreamCursor      string
+	DownstreamChannelID   string
+	DownstreamChannelName string
+	AppStatus             string
+	IsKurl                bool
+	K8sVersion            string
+}
+
 func (u *Upstream) GetUpstreamDir(options WriteOptions) string {
 	renderDir := options.RootDir
 	if options.CreateAppDir {


Test patch:
diff --git a/kotsadm/pkg/preflight/preflight_test.go b/kotsadm/pkg/preflight/preflight_test.go
index 60d0a146c..1a7d0ee2e 100644
--- a/kotsadm/pkg/preflight/preflight_test.go
+++ b/kotsadm/pkg/preflight/preflight_test.go
@@ -65,10 +65,11 @@ func Test_getPreflightState(t *testing.T) {
 			want:             "pass",
 		},
 	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			if got := getPreflightState(tt.preflightResults); got != tt.want {
-				t.Errorf("getPreflightState() = %v, want %v", got, tt.want)
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			if got := getPreflightState(test.preflightResults); got != test.want {
+				t.Errorf("getPreflightState() = %v, want %v", got, test.want)
 			}
 		})
 	}
diff --git a/kotsadm/pkg/registry/troubleshoot_test.go b/kotsadm/pkg/registry/troubleshoot_test.go
new file mode 100644
index 000000000..4354abc4d
--- /dev/null
+++ b/kotsadm/pkg/registry/troubleshoot_test.go
@@ -0,0 +1,189 @@
+package registry
+
+import (
+	"testing"
+
+	registrytypes "github.com/replicatedhq/kots/kotsadm/pkg/registry/types"
+	kotsv1beta1 "github.com/replicatedhq/kots/kotskinds/apis/kots/v1beta1"
+	troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func Test_UpdateCollectorSpecsWithRegistryData(t *testing.T) {
+	tests := []struct {
+		name               string
+		knownImages        []kotsv1beta1.InstallationImage
+		localRegistryInfo  *registrytypes.RegistrySettings
+		license            *kotsv1beta1.License
+		collectors         []*troubleshootv1beta2.Collect
+		expectedCollectors []*troubleshootv1beta2.Collect
+	}{
+		{
+			name:               "empty spec, no change",
+			knownImages:        nil,
+			localRegistryInfo:  nil,
+			license:            nil,
+			collectors:         []*troubleshootv1beta2.Collect{},
+			expectedCollectors: []*troubleshootv1beta2.Collect{},
+		},
+		{
+			name:              "valid spec, no change",
+			knownImages:       nil,
+			localRegistryInfo: nil,
+			license:           nil,
+			collectors: []*troubleshootv1beta2.Collect{
+				{
+					ClusterInfo: &troubleshootv1beta2.ClusterInfo{},
+				},
+			},
+			expectedCollectors: []*troubleshootv1beta2.Collect{
+				{
+					ClusterInfo: &troubleshootv1beta2.ClusterInfo{},
+				},
+			},
+		},
+		{
+			name: "run collector, public image",
+			knownImages: []kotsv1beta1.InstallationImage{
+				{
+					Image:     "docker.io/bitnami/postgres:11",
+					IsPrivate: false,
+				},
+			},
+			localRegistryInfo: nil,
+			license:           nil,
+			collectors: []*troubleshootv1beta2.Collect{
+				{
+					Run: &troubleshootv1beta2.Run{
+						Image:           "docker.io/bitnami/postgres:11",
+						ImagePullSecret: nil,
+					},
+				},
+			},
+			expectedCollectors: []*troubleshootv1beta2.Collect{
+				{
+					Run: &troubleshootv1beta2.Run{
+						Image:           "docker.io/bitnami/postgres:11",
+						ImagePullSecret: nil,
+					},
+				},
+			},
+		},
+		{
+			name:        "run collector, public image, private local registry",
+			knownImages: []kotsv1beta1.InstallationImage{},
+			localRegistryInfo: &registrytypes.RegistrySettings{
+				Hostname:  "ttl.sh",
+				Namespace: "abc",
+				Username:  "user",
+				Password:  "pass",
+			},
+			license: nil,
+			collectors: []*troubleshootv1beta2.Collect{
+				{
+					Run: &troubleshootv1beta2.Run{
+						Image:           "docker.io/bitnami/postgres:11",
+						ImagePullSecret: nil,
+					},
+				},
+			},
+			expectedCollectors: []*troubleshootv1beta2.Collect{
+				{
+					Run: &troubleshootv1beta2.Run{
+						Image: "ttl.sh/abc/postgres:11",
+						ImagePullSecret: &troubleshootv1beta2.ImagePullSecrets{
+							SecretType: "kubernetes.io/dockerconfigjson",
+							Data: map[string]string{
+								".dockerconfigjson": "eyJhdXRocyI6eyJ0dGwuc2giOnsiYXV0aCI6ImRYTmxjanB3WVhOeiJ9fX0=",
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "run collector, replicated registry image, no private local registry",
+			knownImages: []kotsv1beta1.InstallationImage{
+				{
+					Image:     "registry.replicated.com/my-app/my-image:abcdef",
+					IsPrivate: true,
+				},
+			},
+			localRegistryInfo: nil,
+			license: &kotsv1beta1.License{
+				Spec: kotsv1beta1.LicenseSpec{
+					LicenseID: "licenseid",
+					AppSlug:   "app-slug",
+				},
+			},
+			collectors: []*troubleshootv1beta2.Collect{
+				{
+					Run: &troubleshootv1beta2.Run{
+						Image: "registry.replicated.com/my-app/my-image:abcdef",
+					},
+				},
+			},
+			expectedCollectors: []*troubleshootv1beta2.Collect{
+				{
+					Run: &troubleshootv1beta2.Run{
+						Image: "registry.replicated.com/my-app/my-image:abcdef",
+						ImagePullSecret: &troubleshootv1beta2.ImagePullSecrets{
+							SecretType: "kubernetes.io/dockerconfigjson",
+							Data: map[string]string{
+								".dockerconfigjson": "eyJhdXRocyI6eyJyZWdpc3RyeS5yZXBsaWNhdGVkLmNvbSI6eyJhdXRoIjoiYkdsalpXNXpaV2xrT214cFkyVnVjMlZwWkE9PSJ9fX0=",
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "run collector, private image (not replicated), no private local registry",
+			knownImages: []kotsv1beta1.InstallationImage{
+				{
+					Image:     "quay.io/my-app/my-image:abcdef",
+					IsPrivate: true,
+				},
+			},
+			localRegistryInfo: nil,
+			license: &kotsv1beta1.License{
+				Spec: kotsv1beta1.LicenseSpec{
+					LicenseID: "licenseid",
+					AppSlug:   "app-slug",
+				},
+			},
+			collectors: []*troubleshootv1beta2.Collect{
+				{
+					Run: &troubleshootv1beta2.Run{
+						Image: "quay.io/my-app/my-image:abcdef",
+					},
+				},
+			},
+			expectedCollectors: []*troubleshootv1beta2.Collect{
+				{
+					Run: &troubleshootv1beta2.Run{
+						Image: "proxy.replicated.com/proxy/app-slug/quay.io/my-app/my-image:abcdef",
+						ImagePullSecret: &troubleshootv1beta2.ImagePullSecrets{
+							SecretType: "kubernetes.io/dockerconfigjson",
+							Data: map[string]string{
+								".dockerconfigjson": "eyJhdXRocyI6eyJwcm94eS5yZXBsaWNhdGVkLmNvbSI6eyJhdXRoIjoiYkdsalpXNXpaV2xrT214cFkyVnVjMlZwWkE9PSJ9fX0=",
+							},
+						},
+					},
+				},
+			},
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			req := require.New(t)
+
+			actualCollectors, err := UpdateCollectorSpecsWithRegistryData(test.collectors, test.localRegistryInfo, test.knownImages, test.license)
+			req.NoError(err)
+
+			assert.Equal(t, test.expectedCollectors, actualCollectors)
+		})
+	}
+}
diff --git a/pkg/upstream/fetch_test.go b/pkg/upstream/fetch_test.go
index b1fd8ef01..cfbe513e3 100644
--- a/pkg/upstream/fetch_test.go
+++ b/pkg/upstream/fetch_test.go
@@ -8,6 +8,7 @@ import (
 	"testing"
 
 	kotsv1beta1 "github.com/replicatedhq/kots/kotskinds/apis/kots/v1beta1"
+	types "github.com/replicatedhq/kots/pkg/upstream/types"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -56,7 +57,7 @@ ACgAAA==`,
 	}
 
 	for _, test := range tests {
-		fetchOptions := &FetchOptions{
+		fetchOptions := &types.FetchOptions{
 			RootDir:             workDir,
 			LocalPath:           srcDir,
 			CurrentVersionLabel: test.currentVersionLabel,