|
| 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