diff --git a/cli/cmd/channel_release_demote.go b/cli/cmd/channel_release_demote.go new file mode 100644 index 000000000..d5274d001 --- /dev/null +++ b/cli/cmd/channel_release_demote.go @@ -0,0 +1,85 @@ +package cmd + +import ( + "errors" + "fmt" + + "github.com/replicatedhq/replicated/client" + "github.com/spf13/cobra" +) + +func (r *runners) InitChannelReleaseDemote(parent *cobra.Command) { + cmd := &cobra.Command{ + Use: "demote CHANNEL_ID_OR_NAME", + Short: "Demote a release from a channel", + Long: "Demote a channel release from a channel using a channel sequence or release sequence.", + Example: ` # Demote a release from a channel by channel sequence + replicated channel release demote Beta --channel-sequence 15 + + # Demote a release from a channel by release sequence + replicated channel release demote Beta --release-sequence 12`, + Args: cobra.ExactArgs(1), + } + + cmd.Flags().Int64Var(&r.args.demoteChannelSequence, "channel-sequence", 0, "The channel sequence to demote") + cmd.Flags().Int64Var(&r.args.demoteReleaseSequence, "release-sequence", 0, "The release sequence to demote") + + parent.AddCommand(cmd) + cmd.RunE = r.channelReleaseDemote +} + +func (r *runners) channelReleaseDemote(cmd *cobra.Command, args []string) error { + if !r.hasApp() { + return errors.New("no app specified") + } + + if r.args.demoteChannelSequence == 0 && r.args.demoteReleaseSequence == 0 { + return errors.New("one of --channel-sequence or --release-sequence is required") + } + + if r.args.demoteChannelSequence != 0 && r.args.demoteReleaseSequence != 0 { + fmt.Fprintf(r.w, "Warning: Both --channel-sequence and --release-sequence provided. Using --channel-sequence %d\n", r.args.demoteChannelSequence) + } + + chanID := args[0] + + opts := client.GetOrCreateChannelOptions{ + AppID: r.appID, + AppType: r.appType, + NameOrID: chanID, + CreateIfAbsent: false, + } + foundChannel, err := r.api.GetOrCreateChannelByName(opts) + if err != nil { + return err + } + + channelSequence := r.args.demoteChannelSequence + if r.args.demoteReleaseSequence != 0 { + kotsChannel, err := r.api.KotsClient.GetKotsChannel(r.appID, foundChannel.ID) + if err != nil { + return err + } + + for _, channelRelease := range kotsChannel.Releases { + if int64(channelRelease.Sequence) == r.args.demoteReleaseSequence { + channelSequence = int64(channelRelease.ChannelSequence) + break + } + } + + if channelSequence == 0 { + return fmt.Errorf("release sequence %d not found in channel %s", r.args.demoteReleaseSequence, foundChannel.ID) + } + } + + demotedRelease, err := r.api.ChannelReleaseDemote(r.appID, r.appType, foundChannel.ID, channelSequence) + if err != nil { + return err + } + + fmt.Fprintf(r.w, "Channel sequence %d (version %s, release %d) demoted in channel %s\n", channelSequence, demotedRelease.Semver, demotedRelease.Sequence, chanID) + r.w.Flush() + + return nil +} diff --git a/cli/cmd/channel_release_undemote.go b/cli/cmd/channel_release_undemote.go new file mode 100644 index 000000000..d2ecdaf1b --- /dev/null +++ b/cli/cmd/channel_release_undemote.go @@ -0,0 +1,85 @@ +package cmd + +import ( + "errors" + "fmt" + + "github.com/replicatedhq/replicated/client" + "github.com/spf13/cobra" +) + +func (r *runners) InitChannelReleaseUnDemote(parent *cobra.Command) { + cmd := &cobra.Command{ + Use: "un-demote CHANNEL_ID_OR_NAME", + Short: "Un-demote a release from a channel", + Long: "Un-demote a channel release from a channel using a channel sequence or release sequence.", + Example: ` # Un-demote a release from a channel by channel sequence + replicated channel release un-demote Beta --channel-sequence 15 + + # Un-demote a release from a channel by release sequence + replicated channel release un-demote Beta --release-sequence 12`, + Args: cobra.ExactArgs(1), + } + + cmd.Flags().Int64Var(&r.args.unDemoteChannelSequence, "channel-sequence", 0, "The channel sequence to un-demote") + cmd.Flags().Int64Var(&r.args.unDemoteReleaseSequence, "release-sequence", 0, "The release sequence to un-demote") + + parent.AddCommand(cmd) + cmd.RunE = r.channelReleaseUnDemote +} + +func (r *runners) channelReleaseUnDemote(cmd *cobra.Command, args []string) error { + if !r.hasApp() { + return errors.New("no app specified") + } + + if r.args.unDemoteChannelSequence == 0 && r.args.unDemoteReleaseSequence == 0 { + return errors.New("one of --channel-sequence or --release-sequence is required") + } + + if r.args.unDemoteChannelSequence != 0 && r.args.unDemoteReleaseSequence != 0 { + fmt.Fprintf(r.w, "Warning: Both --channel-sequence and --release-sequence provided. Using --channel-sequence %d\n", r.args.unDemoteChannelSequence) + } + + chanID := args[0] + + opts := client.GetOrCreateChannelOptions{ + AppID: r.appID, + AppType: r.appType, + NameOrID: chanID, + CreateIfAbsent: false, + } + foundChannel, err := r.api.GetOrCreateChannelByName(opts) + if err != nil { + return err + } + + channelSequence := r.args.unDemoteChannelSequence + if r.args.unDemoteReleaseSequence != 0 { + kotsChannel, err := r.api.KotsClient.GetKotsChannel(r.appID, foundChannel.ID) + if err != nil { + return err + } + + for _, channelRelease := range kotsChannel.Releases { + if int64(channelRelease.Sequence) == r.args.unDemoteReleaseSequence { + channelSequence = int64(channelRelease.ChannelSequence) + break + } + } + + if channelSequence == 0 { + return fmt.Errorf("release sequence %d not found in channel %s", r.args.unDemoteReleaseSequence, foundChannel.ID) + } + } + + unDemotedRelease, err := r.api.ChannelReleaseUnDemote(r.appID, r.appType, foundChannel.ID, channelSequence) + if err != nil { + return err + } + + fmt.Fprintf(r.w, "Channel sequence %d (version %s, release %d) un-demoted in channel %s\n", channelSequence, unDemotedRelease.Semver, unDemotedRelease.Sequence, chanID) + r.w.Flush() + + return nil +} diff --git a/cli/cmd/root.go b/cli/cmd/root.go index 678a3c6de..e7319e554 100644 --- a/cli/cmd/root.go +++ b/cli/cmd/root.go @@ -140,6 +140,8 @@ func Execute(rootCmd *cobra.Command, stdin io.Reader, stdout io.Writer, stderr i runCmds.InitChannelRemove(channelCmd) runCmds.InitChannelEnableSemanticVersioning(channelCmd) runCmds.InitChannelDisableSemanticVersioning(channelCmd) + runCmds.InitChannelReleaseDemote(channelCmd) + runCmds.InitChannelReleaseUnDemote(channelCmd) runCmds.rootCmd.AddCommand(releaseCmd) err := runCmds.InitReleaseCreate(releaseCmd) @@ -430,10 +432,3 @@ func parseTags(tags []string) ([]types.Tag, error) { } return parsedTags, nil } - -func homeDir() string { - if h := os.Getenv("HOME"); h != "" { - return h - } - return os.Getenv("USERPROFILE") -} diff --git a/cli/cmd/runner.go b/cli/cmd/runner.go index 18776c431..90c367a6e 100644 --- a/cli/cmd/runner.go +++ b/cli/cmd/runner.go @@ -15,17 +15,15 @@ import ( // Runner holds the I/O dependencies and configurations used by individual // commands, which are defined as methods on this type. type runners struct { - appID string - appSlug string - appType string - isFoundationApp bool - api client.Client - platformAPI *platformclient.HTTPClient - kotsAPI *kotsclient.VendorV3Client - stdin io.Reader - dir string - outputFormat string - w *tabwriter.Writer + appID string + appSlug string + appType string + api client.Client + platformAPI *platformclient.HTTPClient + kotsAPI *kotsclient.VendorV3Client + stdin io.Reader + outputFormat string + w *tabwriter.Writer rootCmd *cobra.Command args runnerArgs @@ -53,13 +51,7 @@ type runnerArgs struct { createReleaseYamlFile string createReleaseYamlDir string createReleaseChart string - createReleaseConfigYaml string - createReleaseDeploymentYaml string - createReleaseServiceYaml string - createReleasePreflightYaml string - createReleaseSupportBundleYaml string createReleasePromote string - createReleasePromoteDir string createReleasePromoteRequired bool createReleasePromoteNotes string createReleasePromoteVersion string @@ -177,7 +169,6 @@ type runnerArgs struct { modelCollectionRmModelName string modelCollectionRmModelCollectionID string - lsAppVersion string lsVersionsDistribution string lsClusterShowTerminated bool @@ -254,4 +245,9 @@ type runnerArgs struct { clusterAddonCreateObjectStoreDuration time.Duration clusterAddonCreateObjectStoreDryRun bool clusterAddonCreateObjectStoreOutput string + + demoteReleaseSequence int64 + demoteChannelSequence int64 + unDemoteReleaseSequence int64 + unDemoteChannelSequence int64 } diff --git a/cli/print/channel_attributes.go b/cli/print/channel_attributes.go index 2b9b7ee43..c54868e76 100644 --- a/cli/print/channel_attributes.go +++ b/cli/print/channel_attributes.go @@ -14,6 +14,7 @@ var channelAttrsTmplSrc = `ID: {{ .Chan.ID }} NAME: {{ .Chan.Name }} DESCRIPTION: {{ .Chan.Description }} RELEASE: {{ if ge .Chan.ReleaseSequence 1 }}{{ .Chan.ReleaseSequence }}{{ else }} {{ end }} +CHANNEL SEQUENCE: {{ if ge .Chan.ChannelSequence 1 }}{{ .Chan.ChannelSequence }}{{ else }} {{ end }} VERSION: {{ .Chan.ReleaseLabel }} {{ if not .Chan.IsHelmOnly -}} {{ with .Existing -}} diff --git a/client/channel.go b/client/channel.go index 6a19957f6..90043fe3d 100644 --- a/client/channel.go +++ b/client/channel.go @@ -164,3 +164,21 @@ func (c *Client) UpdateSemanticVersioningForChannel(appType string, appID string return errors.Errorf("unknown app type %q", appType) } + +func (c *Client) ChannelReleaseDemote(appID string, appType string, channelID string, channelSequence int64) (*types.ChannelRelease, error) { + if appType == "platform" { + return nil, errors.New("This feature is not currently supported for Platform applications.") + } else if appType == "kots" { + return c.KotsClient.DemoteChannelRelease(appID, channelID, channelSequence) + } + return nil, errors.Errorf("unknown app type %q", appType) +} + +func (c *Client) ChannelReleaseUnDemote(appID string, appType string, channelID string, channelSequence int64) (*types.ChannelRelease, error) { + if appType == "platform" { + return nil, errors.New("This feature is not currently supported for Platform applications.") + } else if appType == "kots" { + return c.KotsClient.UnDemoteChannelRelease(appID, channelID, channelSequence) + } + return nil, errors.Errorf("unknown app type %q", appType) +} diff --git a/pkg/kotsclient/channel.go b/pkg/kotsclient/channel.go index 93655fe58..cf83d5b48 100644 --- a/pkg/kotsclient/channel.go +++ b/pkg/kotsclient/channel.go @@ -119,3 +119,33 @@ func (c *VendorV3Client) UpdateSemanticVersioning(appID string, channel *types.C return nil } + +func (c *VendorV3Client) DemoteChannelRelease(appID string, channelID string, channelSequence int64) (*types.ChannelRelease, error) { + url := fmt.Sprintf("/v3/app/%s/channel/%s/release/%d/demote", appID, url.QueryEscape(channelID), channelSequence) + + response := struct { + Release types.ChannelRelease `json:"release"` + }{} + + err := c.DoJSON(context.TODO(), "POST", url, http.StatusOK, nil, &response) + if err != nil { + return nil, errors.Wrap(err, "demote channel release") + } + + return &response.Release, nil +} + +func (c *VendorV3Client) UnDemoteChannelRelease(appID string, channelID string, channelSequence int64) (*types.ChannelRelease, error) { + url := fmt.Sprintf("/v3/app/%s/channel/%s/release/%d/undemote", appID, url.QueryEscape(channelID), channelSequence) + + response := struct { + Release types.ChannelRelease `json:"release"` + }{} + + err := c.DoJSON(context.TODO(), "POST", url, http.StatusOK, nil, &response) + if err != nil { + return nil, errors.Wrap(err, "un-demote channel release") + } + + return &response.Release, nil +} diff --git a/pkg/types/channel.go b/pkg/types/channel.go index 6a255c15a..e98b4c8cc 100644 --- a/pkg/types/channel.go +++ b/pkg/types/channel.go @@ -38,6 +38,7 @@ func (c *KotsChannel) ToChannel() *Channel { Description: c.Description, Slug: c.ChannelSlug, ReleaseSequence: int64(c.ReleaseSequence), + ChannelSequence: int64(c.ChannelSequence), ReleaseLabel: c.CurrentVersion, IsArchived: c.IsArchived, IsHelmOnly: c.IsHelmOnly, @@ -96,6 +97,8 @@ type Channel struct { ReleaseSequence int64 `json:"releaseSequence"` ReleaseLabel string `json:"releaseLabel"` + ChannelSequence int64 `json:"channelSequence"` + IsArchived bool `json:"isArchived"` IsHelmOnly bool `json:"isHelmOnly"` }