diff --git a/CHANGELOG.md b/CHANGELOG.md index bee2f54..4d870eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `-r, --prune` flag to remove services that are no longer referenced. - `--replace-env` flag to replace environment variables instead of merging them while updating a stack. - `-c, --stack-file` flag to set the file with the YAML definition of the stack. +- `stack inspect` command to print stack info. + - `--format` flag to select output format from "table", "json" or a custom Go template. Defaults to "table". + - `--endpoint` flag to filter stack by endpoint name. - `stack list|ls` command to print stacks. - `--format` flag to select output format from "table", "json" or a custom Go template. Defaults to "table". - `--endpoint` flag to filter stacks by endpoint name. diff --git a/Dockerfile b/Dockerfile index 518f988..9c9ec27 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,6 +14,8 @@ ENV PSU_AUTH_TOKEN="" \ PSU_STACK_DEPLOY_ENV_FILE="" \ PSU_STACK_DEPLOY_REPLACE_ENV="" \ PSU_STACK_DEPLOY_STACK_FILE="" \ + PSU_STACK_INSPECT_ENDPOINT="" \ + PSU_STACK_INSPECT_FORMAT="" \ PSU_STACK_LIST_ENDPOINT="" \ PSU_STACK_LIST_FORMAT="" \ PSU_STACK_REMOVE_ENDPOINT="" \ diff --git a/cmd/stackInspect.go b/cmd/stackInspect.go new file mode 100644 index 0000000..14f4771 --- /dev/null +++ b/cmd/stackInspect.go @@ -0,0 +1,128 @@ +package cmd + +import ( + "encoding/json" + "fmt" + "os" + "text/template" + + "github.com/greenled/portainer-stack-utils/client" + portainer "github.com/portainer/portainer/api" + + "github.com/greenled/portainer-stack-utils/common" + "github.com/sirupsen/logrus" + "github.com/spf13/viper" + + "github.com/spf13/cobra" +) + +// stackInspectCmd represents the stack inspect command +var stackInspectCmd = &cobra.Command{ + Use: "inspect", + Short: "Inspect a stack", + Example: "psu stack inspect mystack", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + stackName := args[0] + var endpointSwarmClusterId string + var stack portainer.Stack + + var endpoint portainer.Endpoint + if endpointName := viper.GetString("stack.inspect.endpoint"); endpointName == "" { + // Guess endpoint if not set + logrus.WithFields(logrus.Fields{ + "implications": "Command will fail if there is not exactly one endpoint available", + }).Warning("Endpoint not set") + var endpointRetrievalErr error + endpoint, endpointRetrievalErr = common.GetDefaultEndpoint() + common.CheckError(endpointRetrievalErr) + endpointName = endpoint.Name + logrus.WithFields(logrus.Fields{ + "endpoint": endpointName, + }).Debug("Using the only available endpoint") + } else { + // Get endpoint by name + var endpointRetrievalErr error + endpoint, endpointRetrievalErr = common.GetEndpointByName(endpointName) + common.CheckError(endpointRetrievalErr) + } + + var selectionErr, stackRetrievalErr error + endpointSwarmClusterId, selectionErr = common.GetEndpointSwarmClusterId(endpoint.ID) + if selectionErr == nil { + // It's a swarm cluster + logrus.WithFields(logrus.Fields{ + "stack": stackName, + "endpoint": endpoint.Name, + }).Debug("Getting stack") + stack, stackRetrievalErr = common.GetStackByName(stackName, endpointSwarmClusterId, endpoint.ID) + } else if selectionErr == common.ErrStackClusterNotFound { + // It's not a swarm cluster + logrus.WithFields(logrus.Fields{ + "stack": stackName, + "endpoint": endpoint.Name, + }).Debug("Getting stack") + stack, stackRetrievalErr = common.GetStackByName(stackName, "", endpoint.ID) + } else { + // Something else happened + common.CheckError(selectionErr) + } + + if stackRetrievalErr == nil { + // The stack exists + switch viper.GetString("stack.inspect.format") { + case "table": + // Print stack in a table format + writer, err := common.NewTabWriter([]string{ + "ID", + "NAME", + "TYPE", + "ENDPOINT", + }) + common.CheckError(err) + _, err = fmt.Fprintln(writer, fmt.Sprintf( + "%v\t%s\t%v\t%s", + stack.ID, + stack.Name, + client.GetTranslatedStackType(stack), + endpoint.Name, + )) + common.CheckError(err) + flushErr := writer.Flush() + common.CheckError(flushErr) + case "json": + // Print stack in a json format + stackJsonBytes, err := json.Marshal(stack) + common.CheckError(err) + fmt.Println(string(stackJsonBytes)) + default: + // Print stack in a custom format + template, templateParsingErr := template.New("stackTpl").Parse(viper.GetString("stack.inspect.format")) + common.CheckError(templateParsingErr) + templateExecutionErr := template.Execute(os.Stdout, stack) + common.CheckError(templateExecutionErr) + fmt.Println() + } + } else if stackRetrievalErr == common.ErrStackNotFound { + // The stack does not exist + logrus.WithFields(logrus.Fields{ + "stack": stackName, + "endpoint": endpoint.Name, + }).Fatal("Stack not found") + } else { + // Something else happened + common.CheckError(stackRetrievalErr) + } + }, +} + +func init() { + stackCmd.AddCommand(stackInspectCmd) + + stackInspectCmd.Flags().String("endpoint", "", "Filter by endpoint name.") + stackInspectCmd.Flags().String("format", "table", `Output format. Can be "table", "json" or a Go template.`) + viper.BindPFlag("stack.inspect.endpoint", stackInspectCmd.Flags().Lookup("endpoint")) + viper.BindPFlag("stack.inspect.format", stackInspectCmd.Flags().Lookup("format")) + + stackInspectCmd.SetUsageTemplate(stackInspectCmd.UsageTemplate() + common.GetFormatHelp(portainer.Stack{})) +}