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

add current and available release version to upstream upgrade cmd #2380

Merged
merged 7 commits into from
Dec 7, 2021
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
54 changes: 35 additions & 19 deletions cmd/kots/cli/upstream-upgrade.go
Original file line number Diff line number Diff line change
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(res, output)
if err != nil {
return err
}

return nil
Expand All @@ -141,3 +130,30 @@ func UpstreamUpgradeCmd() *cobra.Command {

return cmd
}

func logUpstreamUpgrade(res *upstream.UpgradeResponse, output string) error {
morningvera marked this conversation as resolved.
Show resolved Hide resolved
log := logger.NewCLILogger()
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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 // todo: this results in weird json output
morningvera marked this conversation as resolved.
Show resolved Hide resolved
}

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
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

having to do this because new versions aren't immediately assigned sequence numbers. open to feedback if you can think of something better.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"make a new command that just returns the available, parsed versions" is what I see as better here :)

though this is better than nothing for the command we have

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
Original file line number Diff line number Diff line change
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