Skip to content

Commit

Permalink
add current and available versions to upstream upgrade cmd (#2380)
Browse files Browse the repository at this point in the history
  • Loading branch information
morningvera committed Dec 7, 2021
1 parent b84794f commit c21c5ca
Show file tree
Hide file tree
Showing 4 changed files with 142 additions and 67 deletions.
53 changes: 34 additions & 19 deletions cmd/kots/cli/upstream-upgrade.go
Expand Up @@ -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]",
Expand Down Expand Up @@ -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
Expand All @@ -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
}
37 changes: 30 additions & 7 deletions pkg/handlers/update.go
Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
79 changes: 58 additions & 21 deletions pkg/updatechecker/updatechecker.go
Expand Up @@ -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))
}
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -269,7 +306,7 @@ func CheckForUpdates(opts CheckForUpdatesOpts) (int64, error) {
}
}()

return availableUpdates, nil
return &ucr, nil
}

func ensureDesiredVersionIsDeployed(opts CheckForUpdatesOpts, clusterID string) error {
Expand Down
40 changes: 20 additions & 20 deletions pkg/upstream/upgrade.go
Expand Up @@ -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 {
Expand Down Expand Up @@ -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")
}

Expand All @@ -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")
Expand All @@ -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))
}
}

Expand All @@ -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 {
Expand Down

0 comments on commit c21c5ca

Please sign in to comment.