Skip to content

Commit 81ef64c

Browse files
committed
STAC-24174: add stackpack validate command
1 parent 5ee5d98 commit 81ef64c

26 files changed

Lines changed: 1171 additions & 248 deletions

cmd/stackpack.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ func StackPackCommand(cli *di.Deps) *cobra.Command {
3333
cmd.AddCommand(stackpack.StackpackScaffoldCommand(cli))
3434
cmd.AddCommand(stackpack.StackpackPackageCommand(cli))
3535
cmd.AddCommand(stackpack.StackpackTestDeployCommand(cli))
36+
cmd.AddCommand(stackpack.StackpackValidateCommand(cli))
3637
}
3738

3839
return cmd
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
package stackpack
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
"os/exec"
8+
"path/filepath"
9+
10+
"github.com/spf13/cobra"
11+
"github.com/stackvista/stackstate-cli/internal/common"
12+
"github.com/stackvista/stackstate-cli/internal/di"
13+
)
14+
15+
// ValidateArgs contains arguments for stackpack validate command
16+
type ValidateArgs struct {
17+
Name string
18+
StackpackDir string
19+
StackpackFile string
20+
DockerImage string
21+
22+
dockerRunner func([]string) error
23+
}
24+
25+
// StackpackValidateCommand creates the validate subcommand
26+
func StackpackValidateCommand(cli *di.Deps) *cobra.Command {
27+
return stackpackValidateCommandWithArgs(cli, &ValidateArgs{})
28+
}
29+
30+
// stackpackValidateCommandWithArgs creates the validate command with injected args (for testing)
31+
func stackpackValidateCommandWithArgs(cli *di.Deps, args *ValidateArgs) *cobra.Command {
32+
cmd := &cobra.Command{
33+
Use: "validate",
34+
Short: "Validate a stackpack",
35+
Long: `Validate a stackpack using either the API or Docker mode.
36+
37+
In API mode (when a configured backend context is active), this command calls POST /stackpack/{name}/validate
38+
against the live instance.
39+
40+
In Docker mode (when --image is specified), it spins up quay.io/stackstate/stackstate-server:<tag>
41+
with stack-pack-validator as the entrypoint.
42+
43+
This command is experimental and requires STS_EXPERIMENTAL_STACKPACK environment variable to be set.`,
44+
Example: `# Validate using API
45+
sts stackpack validate --name my-stackpack
46+
47+
# Validate using Docker with a directory
48+
sts stackpack validate --image quay.io/stackstate/stackstate-server:latest --stackpack-directory ./my-stackpack
49+
50+
# Validate using Docker with a file
51+
sts stackpack validate --image quay.io/stackstate/stackstate-server:latest --stackpack-file ./my-stackpack.sts`,
52+
RunE: cli.CmdRunE(RunStackpackValidateCommand(args)),
53+
}
54+
55+
cmd.Flags().StringVarP(&args.Name, "name", "n", "", "Stackpack name (required for API mode)")
56+
cmd.Flags().StringVarP(&args.StackpackDir, "stackpack-directory", "d", "", "Path to stackpack directory (Docker mode)")
57+
cmd.Flags().StringVarP(&args.StackpackFile, "stackpack-file", "f", "", "Path to .sts file (Docker mode)")
58+
cmd.Flags().StringVar(&args.DockerImage, "image", "", "Docker image reference (triggers Docker mode)")
59+
60+
// Set default docker runner if not already set
61+
if args.dockerRunner == nil {
62+
args.dockerRunner = defaultDockerRunner
63+
}
64+
65+
return cmd
66+
}
67+
68+
// RunStackpackValidateCommand executes the validate command
69+
func RunStackpackValidateCommand(args *ValidateArgs) func(cli *di.Deps, cmd *cobra.Command) common.CLIError {
70+
return func(cli *di.Deps, cmd *cobra.Command) common.CLIError {
71+
// Determine mode: use Docker if image is provided, otherwise check if context is available
72+
useDocker := args.DockerImage != ""
73+
if !useDocker {
74+
// Try to load context if not already loaded
75+
if cli.CurrentContext == nil {
76+
_ = cli.LoadContext(cmd) // Silently ignore error, context is optional
77+
}
78+
// Use docker mode if no context or no URL
79+
useDocker = cli.CurrentContext == nil || cli.CurrentContext.URL == ""
80+
}
81+
82+
if useDocker {
83+
return runDockerValidation(args)
84+
}
85+
return runAPIValidation(cli, cmd, args)
86+
}
87+
}
88+
89+
// runAPIValidation validates stackpack via API
90+
func runAPIValidation(cli *di.Deps, cmd *cobra.Command, args *ValidateArgs) common.CLIError {
91+
if args.Name == "" {
92+
return common.NewCLIArgParseError(fmt.Errorf("stackpack name is required (use --name)"))
93+
}
94+
95+
// Ensure client is loaded
96+
if cli.Client == nil {
97+
err := cli.LoadClient(cmd, cli.CurrentContext)
98+
if err != nil {
99+
return err
100+
}
101+
}
102+
103+
// Connect to API
104+
api, _, connectErr := cli.Client.Connect()
105+
if connectErr != nil {
106+
return common.NewRuntimeError(fmt.Errorf("failed to connect to API: %w", connectErr))
107+
}
108+
109+
// Call validate endpoint
110+
result, resp, validateErr := api.StackpackApi.ValidateStackPack(cli.Context, args.Name).Execute()
111+
if validateErr != nil {
112+
return common.NewResponseError(validateErr, resp)
113+
}
114+
115+
if cli.IsJson() {
116+
cli.Printer.PrintJson(map[string]interface{}{
117+
"success": true,
118+
"node_count": result.NodeCount,
119+
})
120+
} else {
121+
cli.Printer.Success("Stackpack validation successful!")
122+
cli.Printer.PrintLn("")
123+
cli.Printer.PrintLn(fmt.Sprintf("Node Count: %d", result.NodeCount))
124+
}
125+
126+
return nil
127+
}
128+
129+
// runDockerValidation validates stackpack via Docker
130+
func runDockerValidation(args *ValidateArgs) common.CLIError {
131+
// Validate required flags
132+
if args.DockerImage == "" {
133+
return common.NewCLIArgParseError(fmt.Errorf("--image is required for Docker mode"))
134+
}
135+
136+
// Validate exactly one of directory or file is set
137+
if (args.StackpackDir == "" && args.StackpackFile == "") ||
138+
(args.StackpackDir != "" && args.StackpackFile != "") {
139+
return common.NewCLIArgParseError(fmt.Errorf("exactly one of --stackpack-directory or --stackpack-file must be specified"))
140+
}
141+
142+
// Check docker is available
143+
if _, err := exec.LookPath("docker"); err != nil {
144+
return common.NewRuntimeError(fmt.Errorf("docker is not available: %w", err))
145+
}
146+
147+
// Build docker command arguments
148+
dockerArgs := []string{"run", "--rm", "--entrypoint", "/opt/docker/bin/stack-pack-validator"}
149+
150+
if args.StackpackDir != "" {
151+
// Convert to absolute path
152+
absDir, err := filepath.Abs(args.StackpackDir)
153+
if err != nil {
154+
return common.NewRuntimeError(fmt.Errorf("failed to resolve stackpack directory: %w", err))
155+
}
156+
dockerArgs = append(dockerArgs, "-v", fmt.Sprintf("%s:/stackpack", absDir), args.DockerImage, "-directory", "/stackpack")
157+
} else {
158+
// Convert to absolute path
159+
absFile, err := filepath.Abs(args.StackpackFile)
160+
if err != nil {
161+
return common.NewRuntimeError(fmt.Errorf("failed to resolve stackpack file: %w", err))
162+
}
163+
dockerArgs = append(dockerArgs, "-v", fmt.Sprintf("%s:/stackpack.sts", absFile), args.DockerImage, "-file", "/stackpack.sts")
164+
}
165+
166+
// Execute docker command
167+
if err := args.dockerRunner(dockerArgs); err != nil {
168+
return common.NewRuntimeError(fmt.Errorf("docker validation failed: %w", err))
169+
}
170+
171+
return nil
172+
}
173+
174+
// defaultDockerRunner executes docker command with streaming output
175+
func defaultDockerRunner(dockerArgs []string) error {
176+
cmd := exec.CommandContext(context.Background(), "docker", dockerArgs...)
177+
cmd.Stdout = os.Stdout
178+
cmd.Stderr = os.Stderr
179+
return cmd.Run()
180+
}

0 commit comments

Comments
 (0)