11package stackpack
22
33import (
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
1615type 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
4338This 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