Skip to content

Commit

Permalink
Ability to download the application archive of any version including …
Browse files Browse the repository at this point in the history
…the currently deployed one (#4998)

* Ability to download the archive of any version including the currently deployed one
  • Loading branch information
sgalsaleh authored Nov 11, 2024
1 parent af1c3c7 commit 48decd7
Show file tree
Hide file tree
Showing 7 changed files with 142 additions and 41 deletions.
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

0 comments on commit 48decd7

Please sign in to comment.