diff --git a/cmd/kots/cli/upstream-upgrade.go b/cmd/kots/cli/upstream-upgrade.go index e9b19b0c07..356b9fe435 100644 --- a/cmd/kots/cli/upstream-upgrade.go +++ b/cmd/kots/cli/upstream-upgrade.go @@ -15,12 +15,6 @@ import ( "github.com/spf13/viper" ) -type UpstreamUpgradeOutput struct { - Success bool `json:"success"` - AvailableUpdates int64 `json:"availableUpdates,omitempty"` - Error string `json:"error,omitempty"` -} - func UpstreamUpgradeCmd() *cobra.Command { cmd := &cobra.Command{ Use: "upgrade [appSlug]", @@ -101,23 +95,18 @@ func UpstreamUpgradeCmd() *cobra.Command { } }() - var upgradeOutput UpstreamUpgradeOutput res, err := upstream.Upgrade(appSlug, upgradeOptions) - if err != nil && output == "" { - return err - } else if err != nil { - upgradeOutput.Error = fmt.Sprint(err) + if err != nil { + res = &upstream.UpgradeResponse{ + Error: fmt.Sprint(err), + } } else { - upgradeOutput.Success = true - upgradeOutput.AvailableUpdates = res.AvailableUpdates + res.Success = true } - if output == "json" { - outputJSON, err := json.Marshal(upgradeOutput) - if err != nil { - return errors.Wrap(err, "error marshaling JSON") - } - log.Info(string(outputJSON)) + err = logUpstreamUpgrade(log, res, output) + if err != nil { + return err } return nil @@ -141,3 +130,29 @@ func UpstreamUpgradeCmd() *cobra.Command { return cmd } + +func logUpstreamUpgrade(log *logger.CLILogger, res *upstream.UpgradeResponse, output string) error { + if output == "json" { + outputJSON, err := json.Marshal(res) + if err != nil { + return errors.Wrap(err, "error marshaling JSON") + } + log.Info(string(outputJSON)) + return nil + } + + // text output + if res.Error != "" { + log.ActionWithoutSpinner(res.Error) + } else { + if res.CurrentRelease != nil { + log.ActionWithoutSpinner(fmt.Sprintf("Currently deployed release: sequence %v, version %v", res.CurrentRelease.Sequence, res.CurrentRelease.Version)) + } + + for _, r := range res.AvailableReleases { + log.ActionWithoutSpinner(fmt.Sprintf("Downloading available release: sequence %v, version %v", r.Sequence, r.Version)) + } + } + + return nil +} diff --git a/pkg/handlers/update.go b/pkg/handlers/update.go index f1d266dd75..f4780bd82b 100644 --- a/pkg/handlers/update.go +++ b/pkg/handlers/update.go @@ -22,8 +22,15 @@ type AppUpdateCheckRequest struct { } type AppUpdateCheckResponse struct { - AvailableUpdates int64 `json:"availableUpdates"` - CurrentAppSequence int64 `json:"currentAppSequence"` + AvailableUpdates int64 `json:"availableUpdates"` + CurrentAppSequence int64 `json:"currentAppSequence"` + CurrentRelease AppUpdateRelease `json:"currentRelease"` + AvailableReleases []AppUpdateRelease `json:"availableReleases"` +} + +type AppUpdateRelease struct { + Sequence int64 `json:"sequence"` + Version string `json:"version"` } func (h *Handler) AppUpdateCheck(w http.ResponseWriter, r *http.Request) { @@ -52,7 +59,7 @@ func (h *Handler) AppUpdateCheck(w http.ResponseWriter, r *http.Request) { SkipPreflights: skipPreflights, IsCLI: isCLI, } - availableUpdates, err := updatechecker.CheckForUpdates(opts) + ucr, err := updatechecker.CheckForUpdates(opts) if err != nil { logger.Error(errors.Wrap(err, "failed to check for updates")) w.WriteHeader(http.StatusInternalServerError) @@ -72,9 +79,25 @@ func (h *Handler) AppUpdateCheck(w http.ResponseWriter, r *http.Request) { return } - appUpdateCheckResponse := AppUpdateCheckResponse{ - AvailableUpdates: availableUpdates, - CurrentAppSequence: a.CurrentSequence, + var appUpdateCheckResponse AppUpdateCheckResponse + if ucr != nil { + var availableReleases []AppUpdateRelease + for _, r := range ucr.AvailableReleases { + availableReleases = append(availableReleases, AppUpdateRelease{ + Sequence: r.Sequence, + Version: r.Version, + }) + } + + appUpdateCheckResponse = AppUpdateCheckResponse{ + AvailableUpdates: ucr.AvailableUpdates, + CurrentAppSequence: a.CurrentSequence, + CurrentRelease: AppUpdateRelease{ + Sequence: ucr.CurrentRelease.Sequence, + Version: ucr.CurrentRelease.Version, + }, + AvailableReleases: availableReleases, + } } JSON(w, http.StatusOK, appUpdateCheckResponse) @@ -143,7 +166,7 @@ func (h *Handler) AppUpdateCheck(w http.ResponseWriter, r *http.Request) { if err != nil { finishedChan <- err - logger.Error(errors.Wrap(err, "failed to upgrde app")) + logger.Error(errors.Wrap(err, "failed to upgrade app")) w.WriteHeader(http.StatusInternalServerError) cause := errors.Cause(err) diff --git a/pkg/updatechecker/updatechecker.go b/pkg/updatechecker/updatechecker.go index ff5238a394..2ab5292a3d 100644 --- a/pkg/updatechecker/updatechecker.go +++ b/pkg/updatechecker/updatechecker.go @@ -108,16 +108,16 @@ func Configure(appID string) error { AppID: jobAppID, IsAutomatic: true, } - availableUpdates, err := CheckForUpdates(opts) + ucr, err := CheckForUpdates(opts) if err != nil { logger.Error(errors.Wrapf(err, "failed to check updates for app %s", jobAppSlug)) return } - if availableUpdates > 0 { + if ucr.AvailableUpdates > 0 { logger.Debug("updates found for app", zap.String("slug", jobAppSlug), - zap.Int64("available updates", availableUpdates)) + zap.Int64("available updates", ucr.AvailableUpdates)) } else { logger.Debug("no updates found for app", zap.String("slug", jobAppSlug)) } @@ -154,46 +154,57 @@ type CheckForUpdatesOpts struct { IsCLI bool } +type UpdateCheckResponse struct { + AvailableUpdates int64 + CurrentRelease UpdateCheckRelease + AvailableReleases []UpdateCheckRelease +} + +type UpdateCheckRelease struct { + Sequence int64 + Version string +} + // CheckForUpdates checks, downloads, and makes sure the desired version for a specific app is deployed. // if "DeployLatest" is set to true, the latest version will be deployed. // otherwise, if "DeployVersionLabel" is set to true, then the version with the corresponding version label will be deployed (if found). // otherwise, if "IsAutomatic" is set to true (which means it's an automatic update check), then the version that matches the semver auto deploy configuration (if enabled) will be deployed. // returns the number of available updates. -func CheckForUpdates(opts CheckForUpdatesOpts) (int64, error) { +func CheckForUpdates(opts CheckForUpdatesOpts) (*UpdateCheckResponse, error) { currentStatus, _, err := store.GetStore().GetTaskStatus("update-download") if err != nil { - return 0, errors.Wrap(err, "failed to get task status") + return nil, errors.Wrap(err, "failed to get task status") } if currentStatus == "running" { logger.Debug("update-download is already running, not starting a new one") - return 0, nil + return nil, nil } if err := store.GetStore().ClearTaskStatus("update-download"); err != nil { - return 0, errors.Wrap(err, "failed to clear task status") + return nil, errors.Wrap(err, "failed to clear task status") } a, err := store.GetStore().GetApp(opts.AppID) if err != nil { - return 0, errors.Wrap(err, "failed to get app") + return nil, errors.Wrap(err, "failed to get app") } // sync license, this method is only called when online latestLicense, _, err := license.Sync(a, "", false) if err != nil { - return 0, errors.Wrap(err, "failed to sync license") + return nil, errors.Wrap(err, "failed to sync license") } // reload app because license sync could have created a new release a, err = store.GetStore().GetApp(opts.AppID) if err != nil { - return 0, errors.Wrap(err, "failed to get app") + return nil, errors.Wrap(err, "failed to get app") } updateCursor, versionLabel, err := store.GetStore().GetCurrentUpdateCursor(a.ID, latestLicense.Spec.ChannelID) if err != nil { - return 0, errors.Wrap(err, "failed to get current update cursor") + return nil, errors.Wrap(err, "failed to get current update cursor") } lastUpdateCheckAt, err := time.Parse(time.RFC3339, a.LastUpdateCheckAt) @@ -216,31 +227,57 @@ func CheckForUpdates(opts CheckForUpdatesOpts) (int64, error) { // get updates updates, err := kotspull.GetUpdates(fmt.Sprintf("replicated://%s", latestLicense.Spec.AppSlug), getUpdatesOptions) if err != nil { - return 0, errors.Wrap(err, "failed to get updates") + return nil, errors.Wrap(err, "failed to get updates") } downstreams, err := store.GetStore().ListDownstreamsForApp(a.ID) if err != nil { - return 0, errors.Wrap(err, "failed to list downstreams for app") + return nil, errors.Wrap(err, "failed to list downstreams for app") } if len(downstreams) == 0 { - return 0, errors.Errorf("no downstreams found for app %q", a.Slug) + return nil, errors.Errorf("no downstreams found for app %q", a.Slug) } d := downstreams[0] + // get app version labels and sequence numbers + appVersions, err := store.GetStore().GetAppVersions(opts.AppID, d.ClusterID) + if err != nil { + return nil, errors.Wrapf(err, "failed to get app versions for app %s", opts.AppID) + } + if len(appVersions.AllVersions) == 0 { + return nil, errors.Errorf("no app versions found for app %s in downstream %s", opts.AppID, d.ClusterID) + } + + var availableReleases []UpdateCheckRelease + availableSequence := appVersions.AllVersions[0].Sequence + 1 + for _, u := range updates { + availableReleases = append(availableReleases, UpdateCheckRelease{ + Sequence: availableSequence, + Version: u.VersionLabel, + }) + availableSequence++ + } + + ucr := UpdateCheckResponse{ + AvailableUpdates: int64(len(updates)), + CurrentRelease: UpdateCheckRelease{ + Sequence: appVersions.CurrentVersion.Sequence, + Version: appVersions.CurrentVersion.VersionLabel, + }, + AvailableReleases: availableReleases, + } + if len(updates) == 0 { if err := ensureDesiredVersionIsDeployed(opts, d.ClusterID); err != nil { - return 0, errors.Wrapf(err, "failed to ensure desired version is deployed") + return nil, errors.Wrapf(err, "failed to ensure desired version is deployed") } - return 0, nil + return &ucr, nil } - availableUpdates := int64(len(updates)) - // this is to avoid a race condition where the UI polls the task status before it is set by the goroutine - status := fmt.Sprintf("%d Updates available...", availableUpdates) + status := fmt.Sprintf("%d Updates available...", ucr.AvailableUpdates) if err := store.GetStore().SetTaskStatus("update-download", status, "running"); err != nil { - return 0, errors.Wrap(err, "failed to set task status") + return nil, errors.Wrap(err, "failed to set task status") } // there are updates, go routine it @@ -269,7 +306,7 @@ func CheckForUpdates(opts CheckForUpdatesOpts) (int64, error) { } }() - return availableUpdates, nil + return &ucr, nil } func ensureDesiredVersionIsDeployed(opts CheckForUpdatesOpts, clusterID string) error { diff --git a/pkg/upstream/upgrade.go b/pkg/upstream/upgrade.go index 6e4224df8d..b7d3de7d05 100644 --- a/pkg/upstream/upgrade.go +++ b/pkg/upstream/upgrade.go @@ -24,7 +24,16 @@ import ( ) type UpgradeResponse struct { - AvailableUpdates int64 + Success bool `json:"success"` + AvailableUpdates int64 `json:"availableUpdates"` + CurrentRelease *UpgradeRelease `json:"currentRelease,omitempty"` + AvailableReleases []UpgradeRelease `json:"availableReleases,omitempty"` + Error string `json:"error,omitempty"` +} + +type UpgradeRelease struct { + Sequence int64 `json:"sequence"` + Version string `json:"version"` } type UpgradeOptions struct { @@ -206,11 +215,8 @@ func Upgrade(appSlug string, options UpgradeOptions) (*UpgradeResponse, error) { return nil, errors.Errorf("Unexpected response from the API: %d", resp.StatusCode) } - type updateCheckResponse struct { - AvailableUpdates int64 `json:"availableUpdates"` - } - ucr := updateCheckResponse{} - if err := json.Unmarshal(b, &ucr); err != nil { + ur := UpgradeResponse{} + if err := json.Unmarshal(b, &ur); err != nil { return nil, errors.Wrap(err, "failed to parse response") } @@ -220,12 +226,10 @@ func Upgrade(appSlug string, options UpgradeOptions) (*UpgradeResponse, error) { if airgapPath != "" { log.ActionWithoutSpinner("") log.ActionWithoutSpinner("Update has been uploaded and is being deployed") - return &UpgradeResponse{ - AvailableUpdates: ucr.AvailableUpdates, - }, nil + return &ur, nil } - if ucr.AvailableUpdates == 0 { + if ur.AvailableUpdates == 0 { log.ActionWithoutSpinner("") if options.Deploy { log.ActionWithoutSpinner("There are no application updates available, ensuring latest is marked as deployed") @@ -234,31 +238,29 @@ func Upgrade(appSlug string, options UpgradeOptions) (*UpgradeResponse, error) { } } else if options.Deploy { log.ActionWithoutSpinner("") - log.ActionWithoutSpinner(fmt.Sprintf("There are currently %d updates available in the Admin Console, when the latest release is downloaded, it will be deployed", ucr.AvailableUpdates)) + log.ActionWithoutSpinner(fmt.Sprintf("There are currently %d updates available in the Admin Console, when the latest release is downloaded, it will be deployed", ur.AvailableUpdates)) } else { log.ActionWithoutSpinner("") - log.ActionWithoutSpinner(fmt.Sprintf("There are currently %d updates available in the Admin Console, when the release with the %s version label is downloaded, it will be deployed", ucr.AvailableUpdates, options.DeployVersionLabel)) + log.ActionWithoutSpinner(fmt.Sprintf("There are currently %d updates available in the Admin Console, when the release with the %s version label is downloaded, it will be deployed", ur.AvailableUpdates, options.DeployVersionLabel)) } log.ActionWithoutSpinner("") log.ActionWithoutSpinner("To access the Admin Console, run kubectl kots admin-console --namespace %s", options.Namespace) log.ActionWithoutSpinner("") - return &UpgradeResponse{ - AvailableUpdates: ucr.AvailableUpdates, - }, nil + return &ur, nil } if airgapPath != "" { log.ActionWithoutSpinner("") log.ActionWithoutSpinner("Update has been uploaded") } else { - if ucr.AvailableUpdates == 0 { + if ur.AvailableUpdates == 0 { log.ActionWithoutSpinner("") log.ActionWithoutSpinner("There are no application updates available") } else { log.ActionWithoutSpinner("") - log.ActionWithoutSpinner(fmt.Sprintf("There are currently %d updates available in the Admin Console", ucr.AvailableUpdates)) + log.ActionWithoutSpinner(fmt.Sprintf("There are currently %d updates available in the Admin Console", ur.AvailableUpdates)) } } @@ -267,9 +269,7 @@ func Upgrade(appSlug string, options UpgradeOptions) (*UpgradeResponse, error) { log.ActionWithoutSpinner("") } - return &UpgradeResponse{ - AvailableUpdates: ucr.AvailableUpdates, - }, nil + return &ur, nil } func createPartFromFile(partWriter *multipart.Writer, path string, fileName string) error {