Skip to content

Commit 40dc7a0

Browse files
committed
STAC-24174: make validate take a file to validate
1 parent 5856aa2 commit 40dc7a0

7 files changed

Lines changed: 355 additions & 390 deletions

File tree

Lines changed: 89 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,20 @@
11
package stackpack
22

33
import (
4-
"context"
54
"fmt"
65
"os"
7-
"os/exec"
86
"path/filepath"
97

108
"github.com/spf13/cobra"
9+
"github.com/stackvista/stackstate-cli/generated/stackstate_api"
1110
"github.com/stackvista/stackstate-cli/internal/common"
1211
"github.com/stackvista/stackstate-cli/internal/di"
1312
)
1413

1514
// ValidateArgs contains arguments for stackpack validate command
1615
type ValidateArgs struct {
17-
Name string
1816
StackpackDir string
1917
StackpackFile string
20-
DockerImage string
21-
22-
dockerRunner func([]string) error
2318
}
2419

2520
// StackpackValidateCommand creates the validate subcommand
@@ -32,146 +27,127 @@ func stackpackValidateCommandWithArgs(cli *di.Deps, args *ValidateArgs) *cobra.C
3227
cmd := &cobra.Command{
3328
Use: "validate",
3429
Short: "Validate a stackpack",
35-
Long: `Validate a stackpack using either the API or Docker mode.
30+
Long: `Validate a stackpack against a SUSE Observability server.
3631
37-
In API mode (when a configured backend context is active), this command calls POST /stackpack/{name}/validate
38-
against the live instance.
32+
This command validates a stackpack by uploading it to the server.
33+
- If a directory is provided, it is automatically packaged into a .sts file before uploading
34+
- If a .sts file is provided, it is uploaded directly
3935
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.
36+
Exactly one of --stackpack-directory or --stackpack-file must be specified.
4237
4338
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
39+
Example: `# Validate a stackpack directory (automatically packaged)
40+
sts stackpack validate --stackpack-directory ./my-stackpack
4941
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)),
42+
# Validate a pre-packaged .sts file
43+
sts stackpack validate --stackpack-file ./my-stackpack.sts`,
44+
RunE: cli.CmdRunEWithApi(RunStackpackValidateCommand(args)),
5345
}
5446

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-
}
47+
cmd.Flags().StringVarP(&args.StackpackDir, "stackpack-directory", "d", "", "Path to stackpack directory")
48+
cmd.Flags().StringVarP(&args.StackpackFile, "stackpack-file", "f", "", "Path to .sts file")
6449

6550
return cmd
6651
}
6752

6853
// 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)
54+
func RunStackpackValidateCommand(args *ValidateArgs) di.CmdWithApiFn {
55+
return func(
56+
cmd *cobra.Command,
57+
cli *di.Deps,
58+
api *stackstate_api.APIClient,
59+
serverInfo *stackstate_api.ServerInfo,
60+
) common.CLIError {
61+
// Validate exactly one of directory or file is set
62+
if (args.StackpackDir == "" && args.StackpackFile == "") ||
63+
(args.StackpackDir != "" && args.StackpackFile != "") {
64+
return common.NewCLIArgParseError(fmt.Errorf("exactly one of --stackpack-directory or --stackpack-file must be specified"))
8465
}
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-
}
9466

95-
// Ensure client is loaded
96-
if cli.Client == nil {
97-
err := cli.LoadClient(cmd, cli.CurrentContext)
67+
// Prepare file to validate - if directory is provided, package it first
68+
fileToValidate, cleanup, err := prepareStackpackFile(args)
9869
if err != nil {
9970
return err
10071
}
101-
}
72+
defer cleanup()
10273

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-
}
74+
// Open the file
75+
file, openErr := os.Open(fileToValidate)
76+
if openErr != nil {
77+
return common.NewRuntimeError(fmt.Errorf("failed to open stackpack file: %w", openErr))
78+
}
79+
defer file.Close()
10880

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-
}
81+
// Call validate endpoint
82+
result, resp, validateErr := api.StackpackApi.StackPackValidate(cli.Context).StackPack(file).Execute()
83+
if validateErr != nil {
84+
return common.NewResponseError(validateErr, resp)
85+
}
11486

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-
}
87+
if cli.IsJson() {
88+
cli.Printer.PrintJson(map[string]interface{}{
89+
"success": true,
90+
"result": result,
91+
})
92+
} else {
93+
cli.Printer.Success("Stackpack validation successful!")
94+
if result != "" {
95+
fmt.Println(result)
96+
}
97+
}
12298

123-
return nil
99+
return nil
100+
}
124101
}
125102

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"))
103+
// prepareStackpackFile returns the path to the stackpack file to validate.
104+
// If a directory is provided, it packages it into a temporary .sts file.
105+
// Returns the file path and a cleanup function that should be deferred.
106+
func prepareStackpackFile(args *ValidateArgs) (string, func(), common.CLIError) {
107+
if args.StackpackFile != "" {
108+
// Use provided .sts file directly
109+
if _, err := os.Stat(args.StackpackFile); err != nil {
110+
return "", func() {}, common.NewRuntimeError(fmt.Errorf("failed to access stackpack file: %w", err))
111+
}
112+
return args.StackpackFile, func() {}, nil
131113
}
132114

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"))
115+
// Package the directory
116+
absDir, err := filepath.Abs(args.StackpackDir)
117+
if err != nil {
118+
return "", func() {}, common.NewRuntimeError(fmt.Errorf("failed to resolve stackpack directory: %w", err))
137119
}
138120

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))
121+
// Validate stackpack directory
122+
if err := validateStackpackDirectory(absDir); err != nil {
123+
return "", func() {}, common.NewCLIArgParseError(err)
142124
}
143125

144-
// Build docker command arguments
145-
dockerArgs := []string{"run", "--rm", "--entrypoint", "/opt/docker/bin/stack-pack-validator"}
126+
// Parse stackpack info
127+
parser := &YamlParser{}
128+
stackpackInfo, err := parser.Parse(filepath.Join(absDir, "stackpack.yaml"))
129+
if err != nil {
130+
return "", func() {}, common.NewRuntimeError(fmt.Errorf("failed to parse stackpack.yaml: %w", err))
131+
}
146132

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")
133+
// Create temporary .sts file
134+
tmpFile, err := os.CreateTemp("", fmt.Sprintf("%s-*.sts", stackpackInfo.Name))
135+
if err != nil {
136+
return "", func() {}, common.NewRuntimeError(fmt.Errorf("failed to create temporary file: %w", err))
161137
}
138+
tmpFile.Close()
139+
tmpPath := tmpFile.Name()
162140

163-
// Execute docker command
164-
if err := args.dockerRunner(dockerArgs); err != nil {
165-
return common.NewRuntimeError(fmt.Errorf("docker validation failed: %w", err))
141+
// Package stackpack into temporary file
142+
if err := createStackpackZip(absDir, tmpPath); err != nil {
143+
os.Remove(tmpPath) // Clean up on error
144+
return "", func() {}, common.NewRuntimeError(fmt.Errorf("failed to package stackpack: %w", err))
166145
}
167146

168-
return nil
169-
}
147+
// Return cleanup function that removes the temporary file
148+
cleanup := func() {
149+
os.Remove(tmpPath)
150+
}
170151

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()
152+
return tmpPath, cleanup, nil
177153
}

0 commit comments

Comments
 (0)