Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ability to download the application archive of any version including the currently deployed one #4998

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/actions/cmx-versions/action.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name: 'Get CMX Versions'
description: 'Retrieves a list of the CMX versions to test against'
runs:
using: 'node16'
using: 'node20'
main: 'dist/index.js'

inputs:
Expand Down
2 changes: 1 addition & 1 deletion .github/actions/kurl-addon-kots-publisher/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,5 @@ inputs:
required: true
description: 'GitHub token.'
runs:
using: 'node16'
using: 'node20'
main: 'dist/index.js'
2 changes: 1 addition & 1 deletion .github/actions/version-tag/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ outputs:
GIT_TAG:
description: 'Git tag if this is a tagged revision'
runs:
using: 'node16'
using: 'node20'
main: 'dist/index.js'
47 changes: 47 additions & 0 deletions .github/workflows/build-test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4186,6 +4186,53 @@ jobs:
exit 1
fi

# ---- validate archives ---- #

function validate_configmap_in_archive {
local expected_value="$1"
if ! grep -q "$expected_value" get-set-config/base/configmap.yaml; then
echo "expected base/configmap.yaml to contain $expected_value:"
cat get-set-config/base/configmap.yaml
exit 1
fi
}

# make latest different from current
./bin/kots set config "$APP_SLUG" --key=username --value=latest-username --namespace "$APP_SLUG"

# validate the archive for sequence 0
./bin/kots download --namespace "$APP_SLUG" --slug "$APP_SLUG" --sequence=0 --decrypt-password-values --overwrite

validate_configmap_in_archive "username: ''"
validate_configmap_in_archive "password: ''"
validate_configmap_in_archive "email: ''"
validate_configmap_in_archive "sequence: '0'"

# validate the archive for sequence 2
./bin/kots download --namespace "$APP_SLUG" --slug "$APP_SLUG" --sequence=2 --decrypt-password-values --overwrite

validate_configmap_in_archive "username: 'example-username'"
validate_configmap_in_archive "password: 'example-password'"
validate_configmap_in_archive "email: ''"
validate_configmap_in_archive "sequence: '2'"

# validate the archive for the currently deployed version
./bin/kots download --namespace "$APP_SLUG" --slug "$APP_SLUG" --current --decrypt-password-values --overwrite

validate_configmap_in_archive "username: 'updated-username'"
validate_configmap_in_archive "password: ''"
validate_configmap_in_archive "email: ''"
validate_configmap_in_archive "sequence: '5'"

# validate the archive for the latest version
./bin/kots download --namespace "$APP_SLUG" --slug "$APP_SLUG" --decrypt-password-values --overwrite

validate_configmap_in_archive "username: 'latest-username'"
validate_configmap_in_archive "password: ''"
validate_configmap_in_archive "email: ''"
validate_configmap_in_archive "sequence: '6'"


- name: Generate support bundle on failure
if: failure()
uses: ./.github/actions/generate-support-bundle
Expand Down
8 changes: 8 additions & 0 deletions cmd/kots/cli/download.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ func DownloadCmd() *cobra.Command {
}
}

if v.GetBool("current") && v.GetInt64("sequence") != -1 {
return errors.New("cannot use --current and --sequence together")
}

output := v.GetString("output")
if output != "json" && output != "" {
return errors.Errorf("output format %s not supported (allowed formats are: json)", output)
Expand All @@ -58,6 +62,8 @@ func DownloadCmd() *cobra.Command {
Overwrite: v.GetBool("overwrite"),
Silent: output != "",
DecryptPasswordValues: v.GetBool("decrypt-password-values"),
Current: v.GetBool("current"),
Sequence: v.GetInt64("sequence"),
}

var downloadOutput DownloadOutput
Expand Down Expand Up @@ -97,6 +103,8 @@ func DownloadCmd() *cobra.Command {
cmd.Flags().String("slug", "", "the application slug to download")
cmd.Flags().Bool("decrypt-password-values", false, "decrypt password values to plaintext")
cmd.Flags().StringP("output", "o", "", "output format (currently supported: json)")
cmd.Flags().Bool("current", false, "set to true to download the archive of the currently deployed app version")
cmd.Flags().Int64("sequence", -1, "sequence of the app version to download the archive for (defaults to the latest version unless --current flag is set)")

return cmd
}
8 changes: 8 additions & 0 deletions pkg/download/download.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ type DownloadOptions struct {
Overwrite bool
Silent bool
DecryptPasswordValues bool
Current bool
Sequence int64
}

func Download(appSlug string, path string, downloadOptions DownloadOptions) error {
Expand Down Expand Up @@ -69,6 +71,12 @@ func Download(appSlug string, path string, downloadOptions DownloadOptions) erro
if downloadOptions.DecryptPasswordValues {
url = fmt.Sprintf("%s&decryptPasswordValues=1", url)
}
if downloadOptions.Current {
url = fmt.Sprintf("%s&current=1", url)
}
if downloadOptions.Sequence != -1 {
url = fmt.Sprintf("%s&sequence=%d", url, downloadOptions.Sequence)
}

newRequest, err := util.NewRequest("GET", url, nil)
if err != nil {
Expand Down
114 changes: 76 additions & 38 deletions pkg/handlers/download.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,17 @@ import (
// NOTE: this uses special kots token authorization
func (h *Handler) DownloadApp(w http.ResponseWriter, r *http.Request) {
if err := requireValidKOTSToken(w, r); err != nil {
logger.Error(err)
logger.Error(errors.Wrap(err, "failed to validate KOTS token"))
return
}

a, err := store.GetStore().GetAppFromSlug(r.URL.Query().Get("slug"))
if err != nil {
logger.Error(err)
logger.Error(errors.Wrap(err, "failed to get app from slug"))
if store.GetStore().IsNotFound(err) {
w.WriteHeader(404)
w.WriteHeader(http.StatusNotFound)
} else {
w.WriteHeader(500)
w.WriteHeader(http.StatusInternalServerError)
}
return
}
Expand All @@ -44,59 +44,97 @@ func (h *Handler) DownloadApp(w http.ResponseWriter, r *http.Request) {
if r.URL.Query().Get("decryptPasswordValues") != "" {
decryptPasswordValues, err = strconv.ParseBool(r.URL.Query().Get("decryptPasswordValues"))
if err != nil {
logger.Error(err)
w.WriteHeader(500)
logger.Error(errors.Wrap(err, "failed to parse decrypt password values param"))
w.WriteHeader(http.StatusInternalServerError)
return
}
}

latestSequence, err := store.GetStore().GetLatestAppSequence(a.ID, true)
if err != nil {
logger.Error(err)
w.WriteHeader(500)
return
var sequence int64
if r.URL.Query().Get("sequence") != "" {
s, err := strconv.ParseInt(r.URL.Query().Get("sequence"), 10, 64)
if err != nil {
logger.Error(errors.Wrap(err, "failed to parse sequence param"))
w.WriteHeader(http.StatusInternalServerError)
return
}
sequence = s
} else if r.URL.Query().Get("current") != "" {
// use the currently deployed version as the base
downstreams, err := store.GetStore().ListDownstreamsForApp(a.ID)
if err != nil {
logger.Error(errors.Wrap(err, "failed to list downstreams for app"))
w.WriteHeader(http.StatusInternalServerError)
return
}
if len(downstreams) == 0 {
logger.Error(errors.New("no downstreams found for app"))
w.WriteHeader(http.StatusInternalServerError)
return
}
currVersion, err := store.GetStore().GetCurrentDownstreamVersion(a.ID, downstreams[0].ClusterID)
if err != nil {
logger.Error(errors.Wrap(err, "failed to get current downstream version"))
w.WriteHeader(http.StatusInternalServerError)
return
}
if currVersion == nil {
logger.Error(errors.New("no current version found"))
w.WriteHeader(http.StatusInternalServerError)
return
}
sequence = currVersion.Sequence
} else {
// no sequence was specified, fall back to the latest
latestSequence, err := store.GetStore().GetLatestAppSequence(a.ID, true)
if err != nil {
logger.Error(errors.Wrap(err, "failed to get latest app sequence"))
w.WriteHeader(http.StatusInternalServerError)
return
}
sequence = latestSequence
}

archivePath, err := ioutil.TempDir("", "kotsadm")
archivePath, err := os.MkdirTemp("", "kotsadm")
if err != nil {
logger.Error(err)
w.WriteHeader(500)
logger.Error(errors.Wrap(err, "failed to create temp dir"))
w.WriteHeader(http.StatusInternalServerError)
return
}
defer os.RemoveAll(archivePath)

err = store.GetStore().GetAppVersionArchive(a.ID, latestSequence, archivePath)
err = store.GetStore().GetAppVersionArchive(a.ID, sequence, archivePath)
if err != nil {
logger.Error(err)
w.WriteHeader(500)
logger.Error(errors.Wrap(err, "failed to get app version archive"))
w.WriteHeader(http.StatusInternalServerError)
return
}

if decryptPasswordValues {
kotsKinds, err := kotsutil.LoadKotsKinds(archivePath)
if err != nil {
logger.Error(err)
w.WriteHeader(500)
logger.Error(errors.Wrap(err, "failed to load kots kinds"))
w.WriteHeader(http.StatusInternalServerError)
return
}

if kotsKinds.ConfigValues != nil {
if err := kotsKinds.DecryptConfigValues(); err != nil {
logger.Error(err)
w.WriteHeader(500)
logger.Error(errors.Wrap(err, "failed to decrypt config values"))
w.WriteHeader(http.StatusInternalServerError)
return
}

updated, err := kotsKinds.Marshal("kots.io", "v1beta1", "ConfigValues")
if err != nil {
logger.Error(err)
w.WriteHeader(500)
logger.Error(errors.Wrap(err, "failed to marshal config values"))
w.WriteHeader(http.StatusInternalServerError)
return
}

if err := ioutil.WriteFile(filepath.Join(archivePath, "upstream", "userdata", "config.yaml"), []byte(updated), 0644); err != nil {
logger.Error(err)
w.WriteHeader(500)
if err := os.WriteFile(filepath.Join(archivePath, "upstream", "userdata", "config.yaml"), []byte(updated), 0644); err != nil {
logger.Error(errors.Wrap(err, "failed to write config values file"))
w.WriteHeader(http.StatusInternalServerError)
return
}
}
Expand Down Expand Up @@ -133,16 +171,16 @@ func (h *Handler) DownloadApp(w http.ResponseWriter, r *http.Request) {

tmpDir, err := ioutil.TempDir("", "kotsadm")
if err != nil {
logger.Error(err)
w.WriteHeader(500)
logger.Error(errors.Wrap(err, "failed to create temp dir"))
w.WriteHeader(http.StatusInternalServerError)
return
}
defer os.RemoveAll(tmpDir)
fileToSend := filepath.Join(tmpDir, "archive.tar.gz")

if err != nil {
logger.Error(err)
w.WriteHeader(500)
logger.Error(errors.Wrap(err, "failed to process temp dir"))
w.WriteHeader(http.StatusInternalServerError)
return
}

Expand All @@ -152,33 +190,33 @@ func (h *Handler) DownloadApp(w http.ResponseWriter, r *http.Request) {
},
}
if err := tarGz.Archive(paths, fileToSend); err != nil {
logger.Error(err)
w.WriteHeader(500)
logger.Error(errors.Wrap(err, "failed to create archive"))
w.WriteHeader(http.StatusInternalServerError)
return
}

fi, err := os.Stat(fileToSend)
if err != nil {
logger.Error(err)
w.WriteHeader(500)
logger.Error(errors.Wrap(err, "failed to stat archive file"))
w.WriteHeader(http.StatusInternalServerError)
return
}

f, err := os.Open(fileToSend)
if err != nil {
logger.Error(err)
w.WriteHeader(500)
logger.Error(errors.Wrap(err, "failed to open archive file"))
w.WriteHeader(http.StatusInternalServerError)
return
}

w.Header().Set("Content-Disposition", "attachment; filename=archive.tar.gz")
w.Header().Set("Content-Type", "application/gzip")
w.Header().Set("Content-Length", strconv.FormatInt(fi.Size(), 10))
w.WriteHeader(200)
w.WriteHeader(http.StatusOK)

_, err = io.Copy(w, f)
if err != nil {
logger.Error(err)
logger.Error(errors.Wrap(err, "failed to send archive file"))
}
}

Expand Down
Loading