Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cmd/stackpack.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ func StackPackCommand(cli *di.Deps) *cobra.Command {
cmd.AddCommand(stackpack.StackpackListParameterCommand(cli))
cmd.AddCommand(stackpack.StackpackUninstallCommand(cli))
cmd.AddCommand(stackpack.StackpackUpgradeCommand(cli))
cmd.AddCommand(stackpack.StackpackDowngradeCommand(cli))
cmd.AddCommand(stackpack.StackpackConfirmManualStepsCommand(cli))
cmd.AddCommand(stackpack.StackpackDescribeCommand(cli))
cmd.AddCommand(stackpack.StackpackListVersionsCommand(cli))
Expand Down
58 changes: 58 additions & 0 deletions cmd/stackpack/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/stackvista/stackstate-cli/generated/stackstate_api"
"github.com/stackvista/stackstate-cli/internal/common"
"github.com/stackvista/stackstate-cli/internal/di"
"github.com/stackvista/stackstate-cli/internal/printer"
)

const (
Expand Down Expand Up @@ -118,6 +119,63 @@ func (w *OperationWaiter) WaitForCompletion(options WaitOptions) error {
}
}

// waitAndDisplayResult waits for a StackPack operation to complete, then displays the final status.
// operationLabel is used in progress/success messages (e.g. "upgrade" or "downgrade").
func waitAndDisplayResult(cli *di.Deps, api *stackstate_api.APIClient, stackPackName string, timeout time.Duration, operationLabel string) common.CLIError {
Comment thread
deontaljaard marked this conversation as resolved.
if !cli.IsJson() {
cli.Printer.PrintLn("Waiting for " + operationLabel + " to complete...")
}

waiter := NewOperationWaiter(cli, api)
if waitErr := waiter.WaitForCompletion(WaitOptions{
StackPackName: stackPackName,
Timeout: timeout,
PollInterval: DefaultPollInterval,
}); waitErr != nil {
return common.NewRuntimeError(waitErr)
}

stackPackList, cliErr := fetchAllStackPacks(cli, api)
if cliErr != nil {
return cliErr
}

finalStackPack, err := findStackPackByName(stackPackList, stackPackName)
if err != nil {
return common.NewNotFoundError(err)
}

if cli.IsJson() {
cli.Printer.PrintJson(map[string]interface{}{
"stackpack": finalStackPack,
"status": "completed",
"current-version": finalStackPack.GetVersion(),
})
} else {
cli.Printer.Success("StackPack " + operationLabel + " completed successfully")

data := make([][]interface{}, 0)
for _, config := range finalStackPack.GetConfigurations() {
lastUpdateTime := time.UnixMilli(config.GetLastUpdateTimestamp())
data = append(data, []interface{}{
config.GetId(),
finalStackPack.GetName(),
config.GetStatus(),
config.GetStackPackVersion(),
lastUpdateTime,
})
}

cli.Printer.Table(printer.TableData{
Header: []string{"id", "name", "status", "version", "last updated"},
Data: data,
MissingTableDataMsg: printer.NotFoundMsg{Types: "configurations for " + stackPackName},
})
}

return nil
}

func findStackPackByName(stacks []stackstate_api.FullStackPack, name string) (stackstate_api.FullStackPack, error) {
for _, v := range stacks {
if v.GetName() == name {
Expand Down
74 changes: 74 additions & 0 deletions cmd/stackpack/stackpack_downgrade.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package stackpack

import (
"fmt"
"time"

"github.com/spf13/cobra"
"github.com/stackvista/stackstate-cli/generated/stackstate_api"
"github.com/stackvista/stackstate-cli/internal/common"
"github.com/stackvista/stackstate-cli/internal/di"
)

type DowngradeArgs struct {
TypeName string
Version string
Wait bool
Timeout time.Duration
}

func StackpackDowngradeCommand(cli *di.Deps) *cobra.Command {
args := &DowngradeArgs{}
cmd := &cobra.Command{
Use: "downgrade",
Short: "Downgrade a StackPack to an older version",
Long: "Downgrade all instances of a StackPack to an older version. " +
"Only supported for StackPacks 2.0 and the target version must be lower than the currently installed version. " +
"To move a StackPacks 1.0 instance to an older version, uninstall it and install the desired version instead.",
Example: `# downgrade a StackPack to an older version
sts stackpack downgrade --name kubernetes --stackpack-version 1.2.3

# downgrade and wait for completion
sts stackpack downgrade --name kubernetes --stackpack-version 1.2.3 --wait`,
RunE: cli.CmdRunEWithApi(RunStackpackDowngradeCommand(args)),
}
common.AddRequiredNameFlagVar(cmd, &args.TypeName, "Name of the StackPack")
cmd.Flags().StringVar(&args.Version, StackpackVersionFlag, "", "Version to downgrade to")
cmd.MarkFlagRequired(StackpackVersionFlag) //nolint:errcheck
cmd.Flags().BoolVar(&args.Wait, "wait", false, "Wait for downgrade to complete")
cmd.Flags().DurationVar(&args.Timeout, "timeout", DefaultTimeout, "Timeout for waiting")
return cmd
}

func RunStackpackDowngradeCommand(args *DowngradeArgs) di.CmdWithApiFn {
return func(
cmd *cobra.Command,
cli *di.Deps,
api *stackstate_api.APIClient,
serverInfo *stackstate_api.ServerInfo,
) common.CLIError {
_, resp, err := api.StackpackApi.DowngradeStackPack(cli.Context, args.TypeName).
Version(args.Version).
Execute()
if err != nil {
return common.NewResponseError(err, resp)
}

if args.Wait {
if cliErr := waitAndDisplayResult(cli, api, args.TypeName, args.Timeout, "downgrade"); cliErr != nil {
return cliErr
}
} else {
if cli.IsJson() {
cli.Printer.PrintJson(map[string]interface{}{
"success": true,
"target-version": args.Version,
})
} else {
cli.Printer.Success(fmt.Sprintf("Successfully triggered downgrade of %s to %s", args.TypeName, args.Version))
}
}

return nil
}
}
80 changes: 80 additions & 0 deletions cmd/stackpack/stackpack_downgrade_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package stackpack

import (
"testing"

"github.com/spf13/cobra"
"github.com/stackvista/stackstate-cli/generated/stackstate_api"
"github.com/stackvista/stackstate-cli/internal/di"
"github.com/stretchr/testify/assert"
)

var (
downgradeVersion = "1.2.3"
)

func setupStackPackDowngradeCmd(t *testing.T) (*di.MockDeps, *cobra.Command) {
cli := di.NewMockDeps(t)
cmd := StackpackDowngradeCommand(&cli.Deps)
cli.MockClient.ApiMocks.StackpackApi.DowngradeStackPackResponse.Result = successfulResponseResult
return &cli, cmd
}

func TestStackpackDowngradePrintToTable(t *testing.T) {
cli, cmd := setupStackPackDowngradeCmd(t)
di.ExecuteCommandWithContextUnsafe(&cli.Deps, cmd, "downgrade", "--name", "zabbix",
"--stackpack-version", downgradeVersion,
)

assert.Equal(t,
[]stackstate_api.DowngradeStackPackCall{{
PstackPackName: "zabbix",
Pversion: &downgradeVersion,
}},
*cli.MockClient.ApiMocks.StackpackApi.DowngradeStackPackCalls)
assert.True(t, cli.MockPrinter.HasNonJsonCalls)
assert.Equal(t, []string{"Successfully triggered downgrade of zabbix to 1.2.3"}, *cli.MockPrinter.SuccessCalls)
}

func TestStackpackDowngradePrintToJson(t *testing.T) {
cli, cmd := setupStackPackDowngradeCmd(t)
di.ExecuteCommandWithContextUnsafe(&cli.Deps, cmd, "downgrade", "--name", "zabbix",
"--stackpack-version", downgradeVersion, "-o", "json",
)

assert.Equal(t,
[]stackstate_api.DowngradeStackPackCall{{
PstackPackName: "zabbix",
Pversion: &downgradeVersion,
}},
*cli.MockClient.ApiMocks.StackpackApi.DowngradeStackPackCalls)

expectedJsonCalls := []map[string]interface{}{{
"success": true,
"target-version": downgradeVersion,
}}
assert.Equal(t, expectedJsonCalls, *cli.MockPrinter.PrintJsonCalls)
}

func TestStackpackDowngradeRequiresVersion(t *testing.T) {
cli := di.NewMockDeps(t)
cmd := StackpackDowngradeCommand(&cli.Deps)

_, err := di.ExecuteCommandWithContext(&cli.Deps, cmd, "downgrade", "--name", "zabbix")

assert.Error(t, err)
assert.Contains(t, err.Error(), "stackpack-version")
}

func TestStackpackDowngradeHasWaitFlags(t *testing.T) {
cli := di.NewMockDeps(t)
cmd := StackpackDowngradeCommand(&cli.Deps)

waitFlag := cmd.Flags().Lookup("wait")
assert.NotNil(t, waitFlag, "wait flag should exist")
assert.Equal(t, "false", waitFlag.DefValue, "wait flag default should be false")

timeoutFlag := cmd.Flags().Lookup("timeout")
assert.NotNil(t, timeoutFlag, "timeout flag should exist")
assert.Equal(t, "1m0s", timeoutFlag.DefValue, "timeout flag default should be 1m0s")
}
13 changes: 11 additions & 2 deletions cmd/stackpack/stackpack_install.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
type InstallArgs struct {
Name string
UnlockedStrategy string
Version string
Params map[string]string
Wait bool
Timeout time.Duration
Expand All @@ -33,7 +34,10 @@ func StackpackInstallCommand(cli *di.Deps) *cobra.Command {
sts stackpack install --name example -p "full_name=First Last" -p URL=https://stackstate.com

# install and wait for completion
sts stackpack install --name kubernetes -p cluster_name=production --wait`,
sts stackpack install --name kubernetes -p cluster_name=production --wait

# install a specific (older) version, e.g. to downgrade
sts stackpack install --name example --stackpack-version 1.2.3 -p "full_name=First Last"`,
RunE: cli.CmdRunEWithApi(RunStackpackInstallCommand(args)),
}
common.AddRequiredNameFlagVar(cmd, &args.Name, "Name of the StackPack")
Expand All @@ -44,6 +48,7 @@ sts stackpack install --name kubernetes -p cluster_name=production --wait`,
"Strategy use to upgrade StackPack instance"+
fmt.Sprintf(" (must be { %s })", strings.Join(UnlockedStrategyChoices, " | ")),
)
cmd.Flags().StringVar(&args.Version, StackpackVersionFlag, "", "Install a specific version of the StackPack, for example to downgrade (the StackPack must not already be installed at a different version)")
cmd.Flags().StringToStringVarP(&args.Params, ParameterFlag, "p", args.Params, "List of parameters of the form \"key=value\"")
cmd.Flags().BoolVar(&args.Wait, "wait", false, "Wait for installation to complete")
cmd.Flags().DurationVar(&args.Timeout, "timeout", DefaultTimeout, "Timeout for waiting")
Expand All @@ -57,7 +62,11 @@ func RunStackpackInstallCommand(args *InstallArgs) di.CmdWithApiFn {
api *stackstate_api.APIClient,
serverInfo *stackstate_api.ServerInfo,
) common.CLIError {
instance, resp, err := api.StackpackApi.ProvisionDetails(cli.Context, args.Name).RequestBody(args.Params).Unlocked(args.UnlockedStrategy).Execute()
apiRequest := api.StackpackApi.ProvisionDetails(cli.Context, args.Name).RequestBody(args.Params).Unlocked(args.UnlockedStrategy)
if args.Version != "" {
apiRequest = apiRequest.Version(args.Version)
}
instance, resp, err := apiRequest.Execute()
if err != nil {
return common.NewResponseError(err, resp)
}
Expand Down
28 changes: 28 additions & 0 deletions cmd/stackpack/stackpack_install_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,34 @@ func TestStackpackInstallNoParameters(t *testing.T) {
)
}

func TestStackpackInstallWithVersion(t *testing.T) {
cli, cmd := setupStackPackInstallCmd(t)

di.ExecuteCommandWithContextUnsafe(&cli.Deps, cmd, "install", "--name", "zabbix",
"--stackpack-version", "3.2.0",
)

strategyFlag := FailStrategy
versionFlag := "3.2.0"
assert.Equal(t,
stackstate_api.ProvisionDetailsCall{
PstackPackName: "zabbix",
Punlocked: &strategyFlag,
Pversion: &versionFlag,
PrequestBody: &map[string]string{},
},
(*cli.MockClient.ApiMocks.StackpackApi.ProvisionDetailsCalls)[0],
)
}

func TestStackpackInstallWithoutVersion(t *testing.T) {
cli, cmd := setupStackPackInstallCmd(t)

di.ExecuteCommandWithContextUnsafe(&cli.Deps, cmd, "install", "--name", "zabbix")

assert.Nil(t, (*cli.MockClient.ApiMocks.StackpackApi.ProvisionDetailsCalls)[0].Pversion)
}

func TestStackpackInstallPrintsToJson(t *testing.T) {
cli, cmd := setupStackPackInstallCmd(t)

Expand Down
59 changes: 1 addition & 58 deletions cmd/stackpack/stackpack_upgrade.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import (
"github.com/stackvista/stackstate-cli/generated/stackstate_api"
"github.com/stackvista/stackstate-cli/internal/common"
"github.com/stackvista/stackstate-cli/internal/di"
"github.com/stackvista/stackstate-cli/internal/printer"
)

var (
Expand Down Expand Up @@ -73,66 +72,10 @@ func RunStackpackUpgradeCommand(args *UpgradeArgs) di.CmdWithApiFn {
return common.NewResponseError(err, resp)
}

// Wait functionality: monitor upgrade until completion
if args.Wait {
if !cli.IsJson() {
cli.Printer.PrintLn("Waiting for upgrade to complete...")
}

// Use OperationWaiter to poll until all configurations are upgraded
waiter := NewOperationWaiter(cli, api)
waitErr := waiter.WaitForCompletion(WaitOptions{
StackPackName: args.TypeName,
Timeout: args.Timeout,
PollInterval: DefaultPollInterval,
})
if waitErr != nil {
// Use NewRuntimeError to avoid showing usage on operation failures
return common.NewRuntimeError(waitErr)
}

// Re-fetch final status for display after successful completion
stackPackList, cliErr := fetchAllStackPacks(cli, api)
if cliErr != nil {
if cliErr := waitAndDisplayResult(cli, api, args.TypeName, args.Timeout, "upgrade"); cliErr != nil {
return cliErr
}

finalStackPack, err := findStackPackByName(stackPackList, args.TypeName)
if err != nil {
return common.NewNotFoundError(err)
}

// Display final status
if cli.IsJson() {
cli.Printer.PrintJson(map[string]interface{}{
"stackpack": finalStackPack,
"status": "completed",
"current-version": finalStackPack.GetVersion(),
})
} else {
cli.Printer.Success("StackPack upgrade completed successfully")

// Show configurations status
data := make([][]interface{}, 0)
for _, config := range finalStackPack.GetConfigurations() {
lastUpdateTime := time.UnixMilli(config.GetLastUpdateTimestamp())
data = append(data, []interface{}{
config.GetId(),
finalStackPack.GetName(),
config.GetStatus(),
config.GetStackPackVersion(),
lastUpdateTime,
})
}

cli.Printer.Table(
printer.TableData{
Header: []string{"id", "name", "status", "version", "last updated"},
Data: data,
MissingTableDataMsg: printer.NotFoundMsg{Types: "configurations for " + args.TypeName},
},
)
}
} else {
if cli.IsJson() {
cli.Printer.PrintJson(map[string]interface{}{
Expand Down
1 change: 1 addition & 0 deletions generated/stackstate_api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,7 @@ Class | Method | HTTP request | Description
*ServiceTokenApi* | [**GetServiceTokens**](docs/ServiceTokenApi.md#getservicetokens) | **Get** /security/tokens | Get service tokens
*SnapshotApi* | [**QuerySnapshot**](docs/SnapshotApi.md#querysnapshot) | **Post** /snapshot | Query topology snapshot
*StackpackApi* | [**ConfirmManualSteps**](docs/StackpackApi.md#confirmmanualsteps) | **Post** /stackpack/{stackPackName}/confirm-manual-steps/{stackPackInstanceId} | Confirm manual steps
*StackpackApi* | [**DowngradeStackPack**](docs/StackpackApi.md#downgradestackpack) | **Post** /stackpack/{stackPackName}/downgrade | Downgrade API
*StackpackApi* | [**ProvisionDetails**](docs/StackpackApi.md#provisiondetails) | **Post** /stackpack/{stackPackName}/provision | Provision API
*StackpackApi* | [**ProvisionUninstall**](docs/StackpackApi.md#provisionuninstall) | **Post** /stackpack/{stackPackName}/deprovision/{stackPackInstanceId} | Provision API
*StackpackApi* | [**StackPackDeleteVersion**](docs/StackpackApi.md#stackpackdeleteversion) | **Delete** /stackpack/{stackPackName}/versions/{version} | Delete a StackPack version
Expand Down
Loading
Loading