diff --git a/client/client.go b/client/client.go index adc40c2..c0b194e 100644 --- a/client/client.go +++ b/client/client.go @@ -36,13 +36,13 @@ type PortainerClient interface { GetStacks(swarmId string, endpointId uint32) ([]Stack, error) // Create swarm stack - CreateSwarmStack(stackName string, environmentVariables []StackEnv, stackFileContent string, swarmClusterId string, endpointId string) error + CreateSwarmStack(stackName string, environmentVariables []StackEnv, stackFileContent string, swarmClusterId string, endpointId uint32) error // Create compose stack - CreateComposeStack(stackName string, environmentVariables []StackEnv, stackFileContent string, endpointId string) error + CreateComposeStack(stackName string, environmentVariables []StackEnv, stackFileContent string, endpointId uint32) error // Update stack - UpdateStack(stack Stack, environmentVariables []StackEnv, stackFileContent string, prune bool, endpointId string) error + UpdateStack(stack Stack, environmentVariables []StackEnv, stackFileContent string, prune bool, endpointId uint32) error // Delete stack DeleteStack(stackId uint32) error @@ -51,7 +51,7 @@ type PortainerClient interface { GetStackFileContent(stackId uint32) (content string, err error) // Get endpoint Docker info - GetEndpointDockerInfo(endpointId string) (info map[string]interface{}, err error) + GetEndpointDockerInfo(endpointId uint32) (info map[string]interface{}, err error) // Get Portainer status info GetStatus() (Status, error) @@ -231,7 +231,7 @@ func (n *portainerClientImp) GetStacks(swarmId string, endpointId uint32) (stack return } -func (n *portainerClientImp) CreateSwarmStack(stackName string, environmentVariables []StackEnv, stackFileContent string, swarmClusterId string, endpointId string) (err error) { +func (n *portainerClientImp) CreateSwarmStack(stackName string, environmentVariables []StackEnv, stackFileContent string, swarmClusterId string, endpointId uint32) (err error) { reqBody := StackCreateRequest{ Name: stackName, Env: environmentVariables, @@ -239,29 +239,29 @@ func (n *portainerClientImp) CreateSwarmStack(stackName string, environmentVaria StackFileContent: stackFileContent, } - err = n.doJSON(fmt.Sprintf("stacks?type=%v&method=%s&endpointId=%s", 1, "string", endpointId), http.MethodPost, &reqBody, nil) + err = n.doJSON(fmt.Sprintf("stacks?type=%v&method=%s&endpointId=%v", 1, "string", endpointId), http.MethodPost, &reqBody, nil) return } -func (n *portainerClientImp) CreateComposeStack(stackName string, environmentVariables []StackEnv, stackFileContent string, endpointId string) (err error) { +func (n *portainerClientImp) CreateComposeStack(stackName string, environmentVariables []StackEnv, stackFileContent string, endpointId uint32) (err error) { reqBody := StackCreateRequest{ Name: stackName, Env: environmentVariables, StackFileContent: stackFileContent, } - err = n.doJSON(fmt.Sprintf("stacks?type=%v&method=%s&endpointId=%s", 2, "string", endpointId), http.MethodPost, &reqBody, nil) + err = n.doJSON(fmt.Sprintf("stacks?type=%v&method=%s&endpointId=%v", 2, "string", endpointId), http.MethodPost, &reqBody, nil) return } -func (n *portainerClientImp) UpdateStack(stack Stack, environmentVariables []StackEnv, stackFileContent string, prune bool, endpointId string) (err error) { +func (n *portainerClientImp) UpdateStack(stack Stack, environmentVariables []StackEnv, stackFileContent string, prune bool, endpointId uint32) (err error) { reqBody := StackUpdateRequest{ Env: environmentVariables, StackFileContent: stackFileContent, Prune: prune, } - err = n.doJSON(fmt.Sprintf("stacks/%v?endpointId=%s", stack.Id, endpointId), http.MethodPut, &reqBody, nil) + err = n.doJSON(fmt.Sprintf("stacks/%v?endpointId=%v", stack.Id, endpointId), http.MethodPut, &reqBody, nil) return } @@ -283,7 +283,7 @@ func (n *portainerClientImp) GetStackFileContent(stackId uint32) (content string return } -func (n *portainerClientImp) GetEndpointDockerInfo(endpointId string) (info map[string]interface{}, err error) { +func (n *portainerClientImp) GetEndpointDockerInfo(endpointId uint32) (info map[string]interface{}, err error) { err = n.doJSON(fmt.Sprintf("endpoints/%v/docker/info", endpointId), http.MethodGet, nil, &info) return } diff --git a/cmd/stackDeploy.go b/cmd/stackDeploy.go index 44c84b8..1cbe3a8 100644 --- a/cmd/stackDeploy.go +++ b/cmd/stackDeploy.go @@ -31,10 +31,22 @@ var stackDeployCmd = &cobra.Command{ common.CheckError(clientRetrievalErr) stackName := args[0] + endpointId := viper.GetUint32("stack.deploy.endpoint") + endpointSwarmClusterId, selectionErr := common.GetEndpointSwarmClusterId(endpointId) + switch selectionErr.(type) { + case nil: + // It's a swarm cluster + case *common.StackClusterNotFoundError: + // It's not a swarm cluster + default: + // Something else happened + common.CheckError(selectionErr) + } + logrus.WithFields(logrus.Fields{ "stack": stackName, }).Debug("Getting stack") - retrievedStack, stackRetrievalErr := common.GetStackByName(stackName) + retrievedStack, stackRetrievalErr := common.GetStackByName(stackName, endpointSwarmClusterId, endpointId) switch stackRetrievalErr.(type) { case nil: // We are updating an existing stack @@ -80,7 +92,7 @@ var stackDeployCmd = &cobra.Command{ logrus.WithFields(logrus.Fields{ "stack": retrievedStack.Name, }).Info("Updating stack") - err := portainerClient.UpdateStack(retrievedStack, newEnvironmentVariables, stackFileContent, viper.GetBool("stack.deploy.prune"), viper.GetString("stack.deploy.endpoint")) + err := portainerClient.UpdateStack(retrievedStack, newEnvironmentVariables, stackFileContent, viper.GetBool("stack.deploy.prune"), endpointId) common.CheckError(err) case *common.StackNotFoundError: // We are deploying a new stack @@ -96,19 +108,16 @@ var stackDeployCmd = &cobra.Command{ stackFileContent, loadingErr := loadStackFile(viper.GetString("stack.deploy.stack-file")) common.CheckError(loadingErr) - swarmClusterId, selectionErr := getSwarmClusterId() - endpointId := viper.GetString("stack.deploy.endpoint") - switch selectionErr.(type) { - case nil: + if endpointSwarmClusterId != "" { // It's a swarm cluster logrus.WithFields(logrus.Fields{ "stack": stackName, "endpoint": endpointId, - "cluster": swarmClusterId, + "cluster": endpointSwarmClusterId, }).Info("Creating stack") - deploymentErr := portainerClient.CreateSwarmStack(stackName, loadedEnvironmentVariables, stackFileContent, swarmClusterId, endpointId) + deploymentErr := portainerClient.CreateSwarmStack(stackName, loadedEnvironmentVariables, stackFileContent, endpointSwarmClusterId, endpointId) common.CheckError(deploymentErr) - case *valueNotFoundError: + } else { // It's not a swarm cluster logrus.WithFields(logrus.Fields{ "stack": stackName, @@ -116,9 +125,6 @@ var stackDeployCmd = &cobra.Command{ }).Info("Creating stack") deploymentErr := portainerClient.CreateComposeStack(stackName, loadedEnvironmentVariables, stackFileContent, endpointId) common.CheckError(deploymentErr) - default: - // Something else happened - common.CheckError(stackRetrievalErr) } default: // Something else happened @@ -131,7 +137,7 @@ func init() { stackCmd.AddCommand(stackDeployCmd) stackDeployCmd.Flags().StringP("stack-file", "c", "", "Path to a file with the content of the stack.") - stackDeployCmd.Flags().String("endpoint", "1", "Endpoint ID.") + stackDeployCmd.Flags().Uint32("endpoint", 1, "Endpoint ID.") stackDeployCmd.Flags().StringP("env-file", "e", "", "Path to a file with environment variables used during stack deployment.") stackDeployCmd.Flags().Bool("replace-env", false, "Replace environment variables instead of merging them.") stackDeployCmd.Flags().BoolP("prune", "r", false, "Prune services that are no longer referenced (only available for Swarm stacks).") @@ -142,43 +148,6 @@ func init() { viper.BindPFlag("stack.deploy.prune", stackDeployCmd.Flags().Lookup("prune")) } -func getSwarmClusterId() (id string, err error) { - // Get docker information for endpoint - client, err := common.GetClient() - if err != nil { - return - } - - endpointId := viper.GetString("stack.deploy.endpoint") - logrus.WithFields(logrus.Fields{ - "endpoint": endpointId, - }).Debug("Getting endpoint's Docker info") - result, err := client.GetEndpointDockerInfo(endpointId) - if err != nil { - return - } - - // Get swarm (if any) information for endpoint - swarmClusterId, err := selectValue(result, []string{"Swarm", "Cluster", "ID"}) - if err != nil { - return - } - id = swarmClusterId.(string) - - return -} - -func selectValue(jsonMap map[string]interface{}, jsonPath []string) (interface{}, error) { - value := jsonMap[jsonPath[0]] - if value == nil { - return nil, &valueNotFoundError{} - } else if len(jsonPath) > 1 { - return selectValue(value.(map[string]interface{}), jsonPath[1:]) - } else { - return value, nil - } -} - func loadStackFile(path string) (string, error) { loadedStackFileContentBytes, readingErr := ioutil.ReadFile(path) if readingErr != nil { @@ -204,9 +173,3 @@ func loadEnvironmentVariablesFile(path string) ([]client.StackEnv, error) { return variables, nil } - -type valueNotFoundError struct{} - -func (e *valueNotFoundError) Error() string { - return "Value not found" -} diff --git a/cmd/stackList.go b/cmd/stackList.go index 1fd7ca0..05ffc2a 100644 --- a/cmd/stackList.go +++ b/cmd/stackList.go @@ -5,8 +5,11 @@ import ( "os" "text/template" - "github.com/greenled/portainer-stack-utils/common" + "github.com/greenled/portainer-stack-utils/client" + "github.com/sirupsen/logrus" + + "github.com/greenled/portainer-stack-utils/common" "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -18,17 +21,40 @@ var stackListCmd = &cobra.Command{ Aliases: []string{"ls"}, Example: "psu stack list --endpoint 1", Run: func(cmd *cobra.Command, args []string) { - client, err := common.GetClient() + portainerClient, err := common.GetClient() common.CheckError(err) - swarmId := viper.GetString("stack.list.swarm") endpointId := viper.GetUint32("stack.list.endpoint") - logrus.WithFields(logrus.Fields{ - "swarm": swarmId, - "endpoint": endpointId, - }).Debug("Getting stacks") - stacks, err := client.GetStacks(swarmId, endpointId) - common.CheckError(err) + var endpointSwarmClusterId string + var stacks []client.Stack + if endpointId != 0 { + var selectionErr error + endpointSwarmClusterId, selectionErr = common.GetEndpointSwarmClusterId(endpointId) + switch selectionErr.(type) { + case nil: + // It's a swarm cluster + logrus.WithFields(logrus.Fields{ + "endpoint": endpointId, + "swarm": endpointSwarmClusterId, + }).Debug("Getting stacks") + stacks, err = portainerClient.GetStacks(endpointSwarmClusterId, endpointId) + common.CheckError(err) + case *common.StackClusterNotFoundError: + // It's not a swarm cluster + logrus.WithFields(logrus.Fields{ + "endpoint": endpointId, + }).Debug("Getting stacks") + stacks, err = portainerClient.GetStacks("", endpointId) + common.CheckError(err) + default: + // Something else happened + common.CheckError(selectionErr) + } + } else { + logrus.Debug("Getting stacks") + stacks, err = portainerClient.GetStacks("", 0) + common.CheckError(err) + } if viper.GetBool("stack.list.quiet") { // Print only stack names @@ -79,11 +105,9 @@ var stackListCmd = &cobra.Command{ func init() { stackCmd.AddCommand(stackListCmd) - stackListCmd.Flags().String("swarm", "", "Filter by swarm ID.") - stackListCmd.Flags().String("endpoint", "", "Filter by endpoint ID.") + stackListCmd.Flags().Uint32("endpoint", 0, "Filter by endpoint ID.") stackListCmd.Flags().BoolP("quiet", "q", false, "Only display stack names.") stackListCmd.Flags().String("format", "", "Format output using a Go template.") - viper.BindPFlag("stack.list.swarm", stackListCmd.Flags().Lookup("swarm")) viper.BindPFlag("stack.list.endpoint", stackListCmd.Flags().Lookup("endpoint")) viper.BindPFlag("stack.list.quiet", stackListCmd.Flags().Lookup("quiet")) viper.BindPFlag("stack.list.format", stackListCmd.Flags().Lookup("format")) diff --git a/cmd/stackRemove.go b/cmd/stackRemove.go index 9689c8b..d37292e 100644 --- a/cmd/stackRemove.go +++ b/cmd/stackRemove.go @@ -16,7 +16,7 @@ var stackRemoveCmd = &cobra.Command{ Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { stackName := args[0] - stack, err := common.GetStackByName(stackName) + stack, err := common.GetStackByName(stackName, "", 0) switch err.(type) { case nil: diff --git a/common/utils.go b/common/utils.go index 856fd96..f82b6bd 100644 --- a/common/utils.go +++ b/common/utils.go @@ -3,16 +3,18 @@ package common import ( "fmt" + "github.com/sirupsen/logrus" + "github.com/greenled/portainer-stack-utils/client" ) -func GetStackByName(name string) (stack client.Stack, err error) { - client, err := GetClient() +func GetStackByName(name string, swarmId string, endpointId uint32) (stack client.Stack, err error) { + portainerClient, err := GetClient() if err != nil { return } - stacks, err := client.GetStacks("", 0) + stacks, err := portainerClient.GetStacks(swarmId, endpointId) if err != nil { return } @@ -28,6 +30,46 @@ func GetStackByName(name string) (stack client.Stack, err error) { return } +func GetEndpointSwarmClusterId(endpointId uint32) (endpointSwarmClusterId string, err error) { + // Get docker information for endpoint + portainerClient, err := GetClient() + if err != nil { + return + } + + logrus.WithFields(logrus.Fields{ + "endpoint": endpointId, + }).Debug("Getting endpoint's Docker info") + result, err := portainerClient.GetEndpointDockerInfo(endpointId) + if err != nil { + return + } + + // Get swarm (if any) information for endpoint + id, selectionErr := selectValue(result, []string{"Swarm", "Cluster", "ID"}) + switch selectionErr.(type) { + case nil: + endpointSwarmClusterId = id.(string) + case *valueNotFoundError: + err = &StackClusterNotFoundError{} + default: + err = selectionErr + } + + return +} + +func selectValue(jsonMap map[string]interface{}, jsonPath []string) (interface{}, error) { + value := jsonMap[jsonPath[0]] + if value == nil { + return nil, &valueNotFoundError{} + } else if len(jsonPath) > 1 { + return selectValue(value.(map[string]interface{}), jsonPath[1:]) + } else { + return value, nil + } +} + // Custom customerrors type StackNotFoundError struct { StackName string @@ -36,3 +78,15 @@ type StackNotFoundError struct { func (e *StackNotFoundError) Error() string { return fmt.Sprintf("Stack %s not found", e.StackName) } + +type valueNotFoundError struct{} + +func (e *valueNotFoundError) Error() string { + return "Value not found" +} + +type StackClusterNotFoundError struct{} + +func (e *StackClusterNotFoundError) Error() string { + return "Stack cluster not found" +}