Skip to content

Commit 5856aa2

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

23 files changed

Lines changed: 979 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: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
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+
_, 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+
})
119+
} else {
120+
cli.Printer.Success("Stackpack validation successful!")
121+
}
122+
123+
return nil
124+
}
125+
126+
// runDockerValidation validates stackpack via Docker
127+
func runDockerValidation(args *ValidateArgs) common.CLIError {
128+
// Validate required flags
129+
if args.DockerImage == "" {
130+
return common.NewCLIArgParseError(fmt.Errorf("--image is required for Docker mode"))
131+
}
132+
133+
// Validate exactly one of directory or file is set
134+
if (args.StackpackDir == "" && args.StackpackFile == "") ||
135+
(args.StackpackDir != "" && args.StackpackFile != "") {
136+
return common.NewCLIArgParseError(fmt.Errorf("exactly one of --stackpack-directory or --stackpack-file must be specified"))
137+
}
138+
139+
// Check docker is available
140+
if _, err := exec.LookPath("docker"); err != nil {
141+
return common.NewRuntimeError(fmt.Errorf("docker is not available: %w", err))
142+
}
143+
144+
// Build docker command arguments
145+
dockerArgs := []string{"run", "--rm", "--entrypoint", "/opt/docker/bin/stack-pack-validator"}
146+
147+
if args.StackpackDir != "" {
148+
// Convert to absolute path
149+
absDir, err := filepath.Abs(args.StackpackDir)
150+
if err != nil {
151+
return common.NewRuntimeError(fmt.Errorf("failed to resolve stackpack directory: %w", err))
152+
}
153+
dockerArgs = append(dockerArgs, "-v", fmt.Sprintf("%s:/stackpack", absDir), args.DockerImage, "-directory", "/stackpack")
154+
} else {
155+
// Convert to absolute path
156+
absFile, err := filepath.Abs(args.StackpackFile)
157+
if err != nil {
158+
return common.NewRuntimeError(fmt.Errorf("failed to resolve stackpack file: %w", err))
159+
}
160+
dockerArgs = append(dockerArgs, "-v", fmt.Sprintf("%s:/stackpack.sts", absFile), args.DockerImage, "-file", "/stackpack.sts")
161+
}
162+
163+
// Execute docker command
164+
if err := args.dockerRunner(dockerArgs); err != nil {
165+
return common.NewRuntimeError(fmt.Errorf("docker validation failed: %w", err))
166+
}
167+
168+
return nil
169+
}
170+
171+
// defaultDockerRunner executes docker command with streaming output
172+
func defaultDockerRunner(dockerArgs []string) error {
173+
cmd := exec.CommandContext(context.Background(), "docker", dockerArgs...)
174+
cmd.Stdout = os.Stdout
175+
cmd.Stderr = os.Stderr
176+
return cmd.Run()
177+
}

0 commit comments

Comments
 (0)