Skip to content

Commit 7d34177

Browse files
authored
STAC-23287: Add --wait functionality to stackpack install and upgrade commands (#113)
* STAC-23287: stackpack install and upgrade commands optionally wait for operations to complete * STAC-23287: Address comments
1 parent 87afbb8 commit 7d34177

File tree

8 files changed

+723
-37
lines changed

8 files changed

+723
-37
lines changed

cmd/stackpack/common.go

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
package stackpack
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"sort"
7+
"strings"
8+
"time"
9+
10+
"github.com/stackvista/stackstate-cli/generated/stackstate_api"
11+
"github.com/stackvista/stackstate-cli/internal/common"
12+
"github.com/stackvista/stackstate-cli/internal/di"
13+
)
14+
15+
const (
16+
// StackPack configuration status constants for wait operations
17+
StatusInstalled = "INSTALLED" // Configuration is successfully installed
18+
StatusProvisioning = "PROVISIONING" // Configuration is still being processed
19+
StatusError = "ERROR" // Configuration failed with errors
20+
21+
// Default wait operation settings
22+
DefaultPollInterval = 5 * time.Second // How often to check status during wait
23+
DefaultTimeout = 1 * time.Minute // Default timeout for wait operations
24+
)
25+
26+
// OperationWaiter provides functionality to wait for StackPack operations to complete
27+
// by polling the API and monitoring configuration status changes
28+
type OperationWaiter struct {
29+
cli *di.Deps
30+
api *stackstate_api.APIClient
31+
}
32+
33+
// WaitOptions configures how the wait operation should behave
34+
type WaitOptions struct {
35+
StackPackName string // Name of the StackPack to monitor
36+
Timeout time.Duration // Maximum time to wait before giving up
37+
PollInterval time.Duration // How often to check the status
38+
}
39+
40+
func NewOperationWaiter(cli *di.Deps, api *stackstate_api.APIClient) *OperationWaiter {
41+
return &OperationWaiter{
42+
cli: cli,
43+
api: api,
44+
}
45+
}
46+
47+
// WaitForCompletion polls the StackPack API until all configurations are installed or an error occurs.
48+
// Returns nil on success, error on timeout or configuration failures.
49+
func (w *OperationWaiter) WaitForCompletion(options WaitOptions) error {
50+
// Set up timeout context for the entire wait operation
51+
ctx, cancel := context.WithTimeout(w.cli.Context, options.Timeout)
52+
defer cancel()
53+
54+
// Set up ticker for periodic polling
55+
ticker := time.NewTicker(options.PollInterval)
56+
defer ticker.Stop()
57+
58+
for {
59+
select {
60+
case <-ctx.Done():
61+
return fmt.Errorf("timeout waiting for stackpack '%s' operation to complete after %v", options.StackPackName, options.Timeout)
62+
case <-ticker.C:
63+
// Poll the API to check current status
64+
stackPackList, cliErr := fetchAllStackPacks(w.cli, w.api)
65+
if cliErr != nil {
66+
return fmt.Errorf("failed to check stackpack status: %v", cliErr)
67+
}
68+
69+
stackPack, err := findStackPackByName(stackPackList, options.StackPackName)
70+
if err != nil {
71+
return fmt.Errorf("stackpack '%s' not found: %v", options.StackPackName, err)
72+
}
73+
74+
// Check the status of all configurations for this StackPack
75+
allInstalled := true
76+
hasProvisioning := false
77+
var errorMessages []string
78+
79+
for _, config := range stackPack.GetConfigurations() {
80+
status := config.GetStatus()
81+
switch status {
82+
case StatusError:
83+
// Extract detailed error message from the API response
84+
errorMsg := fmt.Sprintf("Configuration %d failed", config.GetId())
85+
if config.HasError() {
86+
stackPackError := config.GetError()
87+
apiError := stackPackError.GetError()
88+
if message, ok := apiError["message"]; ok {
89+
if msgStr, ok := message.(string); ok {
90+
errorMsg = fmt.Sprintf("Configuration %d failed: %s", config.GetId(), msgStr)
91+
}
92+
}
93+
}
94+
errorMessages = append(errorMessages, errorMsg)
95+
case StatusProvisioning:
96+
hasProvisioning = true
97+
allInstalled = false
98+
case StatusInstalled:
99+
// Continue checking other configs
100+
default:
101+
// Unknown status, treat as still in progress
102+
allInstalled = false
103+
}
104+
}
105+
106+
// Return immediately if any configuration has failed
107+
if len(errorMessages) > 0 {
108+
return fmt.Errorf("stackpack '%s' installation failed:\n%s", options.StackPackName, strings.Join(errorMessages, "\n"))
109+
}
110+
111+
// Success: all configurations are installed and none are provisioning
112+
if allInstalled && !hasProvisioning {
113+
return nil
114+
}
115+
116+
// Continue polling - some configurations are still in progress
117+
}
118+
}
119+
}
120+
121+
func findStackPackByName(stacks []stackstate_api.FullStackPack, name string) (stackstate_api.FullStackPack, error) {
122+
for _, v := range stacks {
123+
if v.GetName() == name {
124+
return v, nil
125+
}
126+
}
127+
return stackstate_api.FullStackPack{}, fmt.Errorf("stackpack %s does not exist", name)
128+
}
129+
130+
// fetchAllStackPacks retrieves all StackPacks from the API and returns them sorted by name.
131+
// This function was moved from stackpack_list.go to common.go for reuse in wait operations.
132+
func fetchAllStackPacks(cli *di.Deps, api *stackstate_api.APIClient) ([]stackstate_api.FullStackPack, common.CLIError) {
133+
stackPackList, resp, err := api.StackpackApi.StackPackList(cli.Context).Execute()
134+
if err != nil {
135+
return nil, common.NewResponseError(err, resp)
136+
}
137+
138+
// Sort by name for consistent ordering
139+
sort.SliceStable(stackPackList, func(i, j int) bool {
140+
return stackPackList[i].Name < stackPackList[j].Name
141+
})
142+
return stackPackList, nil
143+
}

0 commit comments

Comments
 (0)