diff --git a/client/client.go b/client/client.go new file mode 100644 index 0000000..1a63bf6 --- /dev/null +++ b/client/client.go @@ -0,0 +1,295 @@ +package client + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" + "strings" + + "github.com/greenled/portainer-stack-utils/util" +) + +type StackListFilter struct { + SwarmId string `json:",omitempty"` + EndpointId uint32 `json:",omitempty"` +} + +type ClientConfig struct { + Url string + User string + Password string + Token string + DoNotUseToken bool +} + +type PortainerClient interface { + Authenticate() (token string, err error) + GetEndpoints() ([]EndpointSubset, error) + GetStacks(swarmId string, endpointId uint32) ([]Stack, error) + CreateSwarmStack(stackName string, environmentVariables []StackEnv, stackFileContent string, swarmClusterId string, endpointId string) error + CreateComposeStack(stackName string, environmentVariables []StackEnv, stackFileContent string, endpointId string) error + UpdateStack(stack Stack, environmentVariables []StackEnv, stackFileContent string, prune bool, endpointId string) error + DeleteStack(stackId uint32) error + GetStackFileContent(stackId uint32) (content string, err error) + GetEndpointDockerInfo(endpointId string) (info map[string]interface{}, err error) + GetStatus() (Status, error) +} + +type PortainerClientImp struct { + httpClient *http.Client + url *url.URL + user string + password string + token string + doNotUseToken bool +} + +// Check if an http.Response object has errors +func checkResponseForErrors(resp *http.Response) error { + if 300 <= resp.StatusCode { + // Guess it's a GenericError + respBody := GenericError{} + err := json.NewDecoder(resp.Body).Decode(&respBody) + if err != nil { + // It's not a GenericError + bodyBytes, err := ioutil.ReadAll(resp.Body) + defer resp.Body.Close() + if err != nil { + return err + } + resp.Body = ioutil.NopCloser(bytes.NewReader(bodyBytes)) + return errors.New(string(bodyBytes)) + } + return &respBody + } + return nil +} + +// Do an http request +func (n *PortainerClientImp) do(uri, method string, request io.Reader, requestType string, headers http.Header) (resp *http.Response, err error) { + requestUrl, err := n.url.Parse(uri) + if err != nil { + return + } + + req, err := http.NewRequest(method, requestUrl.String(), request) + if err != nil { + return + } + + if headers != nil { + req.Header = headers + } + + if request != nil { + req.Header.Set("Content-Type", requestType) + } + + if !n.doNotUseToken { + if n.token == "" { + n.token, err = n.Authenticate() + if err != nil { + return + } + util.PrintDebug(fmt.Sprintf("Auth token: %s", n.token)) + } + req.Header.Set("Authorization", "Bearer "+n.token) + } + + util.PrintDebugRequest("Request", req) + + resp, err = n.httpClient.Do(req) + if err != nil { + return + } + + err = checkResponseForErrors(resp) + if err != nil { + return + } + + util.PrintDebugResponse("Response", resp) + + return +} + +// Do a JSON http request +func (n *PortainerClientImp) doJSON(uri, method string, request interface{}, response interface{}) error { + var body io.Reader + + if request != nil { + reqBodyBytes, err := json.Marshal(request) + if err != nil { + return err + } + body = bytes.NewReader(reqBodyBytes) + } + + resp, err := n.do(uri, method, body, "application/json", nil) + if err != nil { + return err + } + + if response != nil { + d := json.NewDecoder(resp.Body) + err := d.Decode(response) + if err != nil { + return err + } + } + + return nil +} + +// Authenticate a user to get an auth token +func (n *PortainerClientImp) Authenticate() (token string, err error) { + util.PrintVerbose("Getting auth token...") + + reqBody := AuthenticateUserRequest{ + Username: n.user, + Password: n.password, + } + + respBody := AuthenticateUserResponse{} + + previousDoNotUseTokenValue := n.doNotUseToken + n.doNotUseToken = true + + err = n.doJSON("auth", http.MethodPost, &reqBody, &respBody) + if err != nil { + return + } + + n.doNotUseToken = previousDoNotUseTokenValue + + token = respBody.Jwt + + return +} + +// Get endpoints +func (n *PortainerClientImp) GetEndpoints() (endpoints []EndpointSubset, err error) { + util.PrintVerbose("Getting endpoints...") + err = n.doJSON("endpoints", http.MethodGet, nil, &endpoints) + return +} + +// Get stacks, optionally filtered by swarmId and endpointId +func (n *PortainerClientImp) GetStacks(swarmId string, endpointId uint32) (stacks []Stack, err error) { + util.PrintVerbose("Getting stacks...") + + filter := StackListFilter{ + SwarmId: swarmId, + EndpointId: endpointId, + } + + filterJsonBytes, _ := json.Marshal(filter) + filterJsonString := string(filterJsonBytes) + + err = n.doJSON(fmt.Sprintf("stacks?filters=%s", filterJsonString), http.MethodGet, nil, &stacks) + return +} + +// Create swarm stack +func (n *PortainerClientImp) CreateSwarmStack(stackName string, environmentVariables []StackEnv, stackFileContent string, swarmClusterId string, endpointId string) (err error) { + util.PrintVerbose("Deploying stack...") + + reqBody := StackCreateRequest{ + Name: stackName, + Env: environmentVariables, + SwarmID: swarmClusterId, + StackFileContent: stackFileContent, + } + + err = n.doJSON(fmt.Sprintf("stacks?type=%v&method=%s&endpointId=%s", 1, "string", endpointId), http.MethodPost, &reqBody, nil) + return +} + +// Create compose stack +func (n *PortainerClientImp) CreateComposeStack(stackName string, environmentVariables []StackEnv, stackFileContent string, endpointId string) (err error) { + util.PrintVerbose("Deploying stack...") + + 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) + return +} + +// Update stack +func (n *PortainerClientImp) UpdateStack(stack Stack, environmentVariables []StackEnv, stackFileContent string, prune bool, endpointId string) (err error) { + util.PrintVerbose("Updating stack...") + + reqBody := StackUpdateRequest{ + Env: environmentVariables, + StackFileContent: stackFileContent, + Prune: prune, + } + + err = n.doJSON(fmt.Sprintf("stacks/%v?endpointId=%s", stack.Id, endpointId), http.MethodPut, &reqBody, nil) + return +} + +// Delete stack +func (n *PortainerClientImp) DeleteStack(stackId uint32) (err error) { + util.PrintVerbose("Deleting stack...") + + err = n.doJSON(fmt.Sprintf("stacks/%d", stackId), http.MethodDelete, nil, nil) + return +} + +// Get stack file content +func (n *PortainerClientImp) GetStackFileContent(stackId uint32) (content string, err error) { + util.PrintVerbose("Getting stack file content...") + + var respBody StackFileInspectResponse + + err = n.doJSON(fmt.Sprintf("stacks/%v/file", stackId), http.MethodGet, nil, &respBody) + if err != nil { + return + } + + content = respBody.StackFileContent + + return +} + +// Get endpoint Docker info +func (n *PortainerClientImp) GetEndpointDockerInfo(endpointId string) (info map[string]interface{}, err error) { + util.PrintVerbose("Getting endpoint Docker info...") + + err = n.doJSON(fmt.Sprintf("endpoints/%v/docker/info", endpointId), http.MethodGet, nil, &info) + return +} + +// Get Portainer status info +func (n *PortainerClientImp) GetStatus() (status Status, err error) { + err = n.doJSON("status", http.MethodGet, nil, &status) + return +} + +// Create a new client +func NewClient(httpClient *http.Client, config ClientConfig) (c PortainerClient, err error) { + apiUrl, err := url.Parse(strings.TrimRight(config.Url, "/") + "/api/") + if err != nil { + return + } + + c = &PortainerClientImp{ + httpClient: httpClient, + url: apiUrl, + user: config.User, + password: config.Password, + token: config.Token, + } + + return +} diff --git a/common/types.go b/client/portainerTypes.go similarity index 99% rename from common/types.go rename to client/portainerTypes.go index b9daa78..03ed46c 100644 --- a/common/types.go +++ b/client/portainerTypes.go @@ -1,4 +1,4 @@ -package common +package client import "fmt" diff --git a/cmd/config.go b/cmd/config.go index 982aa6c..02c5435 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -2,12 +2,13 @@ package cmd import ( "fmt" - "github.com/greenled/portainer-stack-utils/common" + "log" + "os" + + "github.com/greenled/portainer-stack-utils/util" "github.com/mitchellh/go-homedir" "github.com/spf13/cobra" "github.com/spf13/viper" - "log" - "os" ) // configCmd represents the config command @@ -32,12 +33,12 @@ var configCmd = &cobra.Command{ if len(args) == 1 { // Get config value, configGettingErr := getConfig(args[0]) - common.CheckError(configGettingErr) + util.CheckError(configGettingErr) fmt.Println(value) } else { // Set config configSettingErr := setConfig(args[0], args[1]) - common.CheckError(configSettingErr) + util.CheckError(configSettingErr) } }, } @@ -67,7 +68,7 @@ func loadCofig() (*viper.Viper, error) { // Read config from file if configReadingErr := newViper.ReadInConfig(); configReadingErr != nil { - common.PrintVerbose(fmt.Sprintf("Could not read configuration from \"%s\". Expect all configuration values to be unset.", configFile)) + util.PrintVerbose(fmt.Sprintf("Could not read configuration from \"%s\". Expect all configuration values to be unset.", configFile)) } return newViper, nil diff --git a/cmd/endpointList.go b/cmd/endpointList.go index ac33507..b9b5601 100644 --- a/cmd/endpointList.go +++ b/cmd/endpointList.go @@ -21,6 +21,8 @@ import ( "os" "text/template" + "github.com/greenled/portainer-stack-utils/util" + "github.com/greenled/portainer-stack-utils/common" "github.com/spf13/viper" @@ -34,23 +36,23 @@ var endpointListCmd = &cobra.Command{ Aliases: []string{"ls"}, Run: func(cmd *cobra.Command, args []string) { client, err := common.GetClient() - common.CheckError(err) + util.CheckError(err) endpoints, err := client.GetEndpoints() - common.CheckError(err) + util.CheckError(err) if viper.GetString("endpoint.list.format") != "" { // Print endpoint fields formatted template, templateParsingErr := template.New("endpointTpl").Parse(viper.GetString("endpoint.list.format")) - common.CheckError(templateParsingErr) + util.CheckError(templateParsingErr) for _, e := range endpoints { templateExecutionErr := template.Execute(os.Stdout, e) - common.CheckError(templateExecutionErr) + util.CheckError(templateExecutionErr) fmt.Println() } } else { // Print all endpoint fields as a table - writer, err := common.NewTabWriter([]string{ + writer, err := util.NewTabWriter([]string{ "ENDPOINT ID", "NAME", "TYPE", @@ -58,7 +60,7 @@ var endpointListCmd = &cobra.Command{ "PUBLIC URL", "GROUP ID", }) - common.CheckError(err) + util.CheckError(err) for _, e := range endpoints { var endpointType string if e.Type == 1 { @@ -75,10 +77,10 @@ var endpointListCmd = &cobra.Command{ e.PublicURL, e.GroupID, )) - common.CheckError(err) + util.CheckError(err) } flushErr := writer.Flush() - common.CheckError(flushErr) + util.CheckError(flushErr) } }, } diff --git a/cmd/login.go b/cmd/login.go index 206135d..83ceda9 100644 --- a/cmd/login.go +++ b/cmd/login.go @@ -3,6 +3,8 @@ package cmd import ( "fmt" + "github.com/greenled/portainer-stack-utils/util" + "github.com/greenled/portainer-stack-utils/common" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -15,10 +17,10 @@ var loginCmd = &cobra.Command{ Run: func(cmd *cobra.Command, args []string) { // Get auth token client, err := common.GetDefaultClient() - common.CheckError(err) + util.CheckError(err) authToken, err := client.Authenticate() - common.CheckError(err) + util.CheckError(err) if viper.GetBool("login.print") { fmt.Println(authToken) @@ -26,7 +28,7 @@ var loginCmd = &cobra.Command{ // Save auth token configSettingErr := setConfig("auth-token", authToken) - common.CheckError(configSettingErr) + util.CheckError(configSettingErr) }, } diff --git a/cmd/root.go b/cmd/root.go index cf0d63b..3bdd872 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -2,12 +2,14 @@ package cmd import ( "fmt" + "os" + "strings" + "github.com/greenled/portainer-stack-utils/common" + "github.com/greenled/portainer-stack-utils/util" "github.com/mitchellh/go-homedir" "github.com/spf13/cobra" "github.com/spf13/viper" - "os" - "strings" ) var cfgFile string @@ -83,6 +85,6 @@ func initConfig() { // If a config file is found, read it in. if err := viper.ReadInConfig(); err == nil { - common.PrintVerbose("Using config file:", viper.ConfigFileUsed()) + util.PrintVerbose("Using config file:", viper.ConfigFileUsed()) } } diff --git a/cmd/stackDeploy.go b/cmd/stackDeploy.go index 67ea966..f48d2d7 100644 --- a/cmd/stackDeploy.go +++ b/cmd/stackDeploy.go @@ -5,6 +5,10 @@ import ( "io/ioutil" "log" + "github.com/greenled/portainer-stack-utils/util" + + "github.com/greenled/portainer-stack-utils/client" + "github.com/greenled/portainer-stack-utils/common" "github.com/joho/godotenv" "github.com/spf13/cobra" @@ -19,35 +23,35 @@ var stackDeployCmd = &cobra.Command{ Example: "psu stack deploy mystack --stack-file mystack.yml", Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { - var loadedEnvironmentVariables []common.StackEnv + var loadedEnvironmentVariables []client.StackEnv if viper.GetString("stack.deploy.env-file") != "" { var loadingErr error loadedEnvironmentVariables, loadingErr = loadEnvironmentVariablesFile(viper.GetString("stack.deploy.env-file")) - common.CheckError(loadingErr) + util.CheckError(loadingErr) } - client, clientRetrievalErr := common.GetClient() - common.CheckError(clientRetrievalErr) + portainerClient, clientRetrievalErr := common.GetClient() + util.CheckError(clientRetrievalErr) stackName := args[0] retrievedStack, stackRetrievalErr := common.GetStackByName(stackName) switch stackRetrievalErr.(type) { case nil: // We are updating an existing stack - common.PrintVerbose(fmt.Sprintf("Stack %s found. Updating...", retrievedStack.Name)) + util.PrintVerbose(fmt.Sprintf("Stack %s found. Updating...", retrievedStack.Name)) var stackFileContent string if viper.GetString("stack.deploy.stack-file") != "" { var loadingErr error stackFileContent, loadingErr = loadStackFile(viper.GetString("stack.deploy.stack-file")) - common.CheckError(loadingErr) + util.CheckError(loadingErr) } else { var stackFileContentRetrievalErr error - stackFileContent, stackFileContentRetrievalErr = client.GetStackFileContent(retrievedStack.Id) - common.CheckError(stackFileContentRetrievalErr) + stackFileContent, stackFileContentRetrievalErr = portainerClient.GetStackFileContent(retrievedStack.Id) + util.CheckError(stackFileContentRetrievalErr) } - var newEnvironmentVariables []common.StackEnv + var newEnvironmentVariables []client.StackEnv if viper.GetBool("stack.deploy.replace-env") { newEnvironmentVariables = loadedEnvironmentVariables } else { @@ -61,44 +65,44 @@ var stackDeployCmd = &cobra.Command{ continue LoadedVariablesLoop } } - newEnvironmentVariables = append(newEnvironmentVariables, common.StackEnv{ + newEnvironmentVariables = append(newEnvironmentVariables, client.StackEnv{ Name: loadedEnvironmentVariable.Name, Value: loadedEnvironmentVariable.Value, }) } } - err := client.UpdateStack(retrievedStack, newEnvironmentVariables, stackFileContent, viper.GetBool("stack.deploy.prune"), viper.GetString("stack.deploy.endpoint")) - common.CheckError(err) + err := portainerClient.UpdateStack(retrievedStack, newEnvironmentVariables, stackFileContent, viper.GetBool("stack.deploy.prune"), viper.GetString("stack.deploy.endpoint")) + util.CheckError(err) case *common.StackNotFoundError: // We are deploying a new stack - common.PrintVerbose(fmt.Sprintf("Stack %s not found. Deploying...", stackName)) + util.PrintVerbose(fmt.Sprintf("Stack %s not found. Deploying...", stackName)) if viper.GetString("stack.deploy.stack-file") == "" { log.Fatalln("Specify a docker-compose file with --stack-file") } stackFileContent, loadingErr := loadStackFile(viper.GetString("stack.deploy.stack-file")) - common.CheckError(loadingErr) + util.CheckError(loadingErr) swarmClusterId, selectionErr := getSwarmClusterId() switch selectionErr.(type) { case nil: // It's a swarm cluster - common.PrintVerbose(fmt.Sprintf("Swarm cluster found with id %s", swarmClusterId)) - deploymentErr := client.CreateSwarmStack(stackName, loadedEnvironmentVariables, stackFileContent, swarmClusterId, viper.GetString("stack.deploy.endpoint")) - common.CheckError(deploymentErr) + util.PrintVerbose(fmt.Sprintf("Swarm cluster found with id %s", swarmClusterId)) + deploymentErr := portainerClient.CreateSwarmStack(stackName, loadedEnvironmentVariables, stackFileContent, swarmClusterId, viper.GetString("stack.deploy.endpoint")) + util.CheckError(deploymentErr) case *valueNotFoundError: // It's not a swarm cluster - common.PrintVerbose("Swarm cluster not found") - deploymentErr := client.CreateComposeStack(stackName, loadedEnvironmentVariables, stackFileContent, viper.GetString("stack.deploy.endpoint")) - common.CheckError(deploymentErr) + util.PrintVerbose("Swarm cluster not found") + deploymentErr := portainerClient.CreateComposeStack(stackName, loadedEnvironmentVariables, stackFileContent, viper.GetString("stack.deploy.endpoint")) + util.CheckError(deploymentErr) default: // Something else happened - common.CheckError(stackRetrievalErr) + util.CheckError(stackRetrievalErr) } default: // Something else happened - common.CheckError(stackRetrievalErr) + util.CheckError(stackRetrievalErr) } }, } @@ -160,15 +164,15 @@ func loadStackFile(path string) (string, error) { } // Load environment variables -func loadEnvironmentVariablesFile(path string) ([]common.StackEnv, error) { - var variables []common.StackEnv +func loadEnvironmentVariablesFile(path string) ([]client.StackEnv, error) { + var variables []client.StackEnv variablesMap, readingErr := godotenv.Read(path) if readingErr != nil { - return []common.StackEnv{}, readingErr + return []client.StackEnv{}, readingErr } for key, value := range variablesMap { - variables = append(variables, common.StackEnv{ + variables = append(variables, client.StackEnv{ Name: key, Value: value, }) diff --git a/cmd/stackList.go b/cmd/stackList.go index 5304966..947e0c8 100644 --- a/cmd/stackList.go +++ b/cmd/stackList.go @@ -5,6 +5,8 @@ import ( "os" "text/template" + "github.com/greenled/portainer-stack-utils/util" + "github.com/greenled/portainer-stack-utils/common" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -18,29 +20,29 @@ var stackListCmd = &cobra.Command{ Example: "psu stack list --endpoint 1", Run: func(cmd *cobra.Command, args []string) { client, err := common.GetClient() - common.CheckError(err) + util.CheckError(err) stacks, err := client.GetStacks(viper.GetString("stack.list.swarm"), viper.GetUint32("stack.list.endpoint")) - common.CheckError(err) + util.CheckError(err) if viper.GetBool("stack.list.quiet") { // Print only stack names for _, s := range stacks { _, err := fmt.Println(s.Name) - common.CheckError(err) + util.CheckError(err) } } else if viper.GetString("stack.list.format") != "" { // Print stack fields formatted template, templateParsingErr := template.New("stackTpl").Parse(viper.GetString("stack.list.format")) - common.CheckError(templateParsingErr) + util.CheckError(templateParsingErr) for _, s := range stacks { templateExecutionErr := template.Execute(os.Stdout, s) - common.CheckError(templateExecutionErr) + util.CheckError(templateExecutionErr) fmt.Println() } } else { // Print all stack fields as a table - writer, err := common.NewTabWriter([]string{ + writer, err := util.NewTabWriter([]string{ "STACK ID", "NAME", "TYPE", @@ -49,7 +51,7 @@ var stackListCmd = &cobra.Command{ "ENDPOINT ID", "SWARM ID", }) - common.CheckError(err) + util.CheckError(err) for _, s := range stacks { _, err := fmt.Fprintln(writer, fmt.Sprintf( "%v\t%s\t%v\t%s\t%s\t%v\t%s", @@ -61,10 +63,10 @@ var stackListCmd = &cobra.Command{ s.EndpointID, s.SwarmID, )) - common.CheckError(err) + util.CheckError(err) } flushErr := writer.Flush() - common.CheckError(flushErr) + util.CheckError(flushErr) } }, } diff --git a/cmd/stackRemove.go b/cmd/stackRemove.go index 2eccae1..dab88a2 100644 --- a/cmd/stackRemove.go +++ b/cmd/stackRemove.go @@ -4,6 +4,8 @@ import ( "fmt" "log" + "github.com/greenled/portainer-stack-utils/util" + "github.com/greenled/portainer-stack-utils/common" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -23,26 +25,26 @@ var stackRemoveCmd = &cobra.Command{ switch err.(type) { case nil: // The stack exists - common.PrintVerbose(fmt.Sprintf("Stack %s exists.", stackName)) + util.PrintVerbose(fmt.Sprintf("Stack %s exists.", stackName)) stackId := stack.Id - common.PrintVerbose(fmt.Sprintf("Removing stack %s...", stackName)) + util.PrintVerbose(fmt.Sprintf("Removing stack %s...", stackName)) client, err := common.GetClient() - common.CheckError(err) + util.CheckError(err) err = client.DeleteStack(stackId) - common.CheckError(err) + util.CheckError(err) case *common.StackNotFoundError: // The stack does not exist - common.PrintVerbose(fmt.Sprintf("Stack %s does not exist.", stackName)) + util.PrintVerbose(fmt.Sprintf("Stack %s does not exist.", stackName)) if viper.GetBool("stack.remove.strict") { log.Fatalln(fmt.Sprintf("Stack %s does not exist.", stackName)) } default: // Something else happened - common.CheckError(err) + util.CheckError(err) } }, } diff --git a/cmd/status.go b/cmd/status.go index a57e11a..a252b9c 100644 --- a/cmd/status.go +++ b/cmd/status.go @@ -5,6 +5,8 @@ import ( "os" "text/template" + "github.com/greenled/portainer-stack-utils/util" + "github.com/greenled/portainer-stack-utils/common" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -16,27 +18,27 @@ var statusCmd = &cobra.Command{ Short: "Check Portainer status", Run: func(cmd *cobra.Command, args []string) { client, err := common.GetClient() - common.CheckError(err) + util.CheckError(err) respBody, err := client.GetStatus() - common.CheckError(err) + util.CheckError(err) if viper.GetString("status.format") != "" { // Print stack fields formatted template, templateParsingErr := template.New("statusTpl").Parse(viper.GetString("status.format")) - common.CheckError(templateParsingErr) + util.CheckError(templateParsingErr) templateExecutionErr := template.Execute(os.Stdout, respBody) - common.CheckError(templateExecutionErr) + util.CheckError(templateExecutionErr) fmt.Println() } else { // Print status fields as a table - writer, newTabWriterErr := common.NewTabWriter([]string{ + writer, newTabWriterErr := util.NewTabWriter([]string{ "VERSION", "AUTHENTICATION", "ENDPOINT MANAGEMENT", "ANALYTICS", }) - common.CheckError(newTabWriterErr) + util.CheckError(newTabWriterErr) _, printingErr := fmt.Fprintln(writer, fmt.Sprintf( "%s\t%v\t%v\t%v", @@ -45,10 +47,10 @@ var statusCmd = &cobra.Command{ respBody.EndpointManagement, respBody.Analytics, )) - common.CheckError(printingErr) + util.CheckError(printingErr) flushErr := writer.Flush() - common.CheckError(flushErr) + util.CheckError(flushErr) } }, } diff --git a/common/client.go b/common/client.go index 3d53dc5..2f910f5 100644 --- a/common/client.go +++ b/common/client.go @@ -1,299 +1,18 @@ package common import ( - "bytes" "crypto/tls" - "encoding/json" - "errors" - "fmt" - "io" - "io/ioutil" "net/http" - "net/url" - "strings" + + "github.com/greenled/portainer-stack-utils/client" "github.com/spf13/viper" ) -var cachedClient PortainerClient - -type ClientConfig struct { - Url string - User string - Password string - Token string - DoNotUseToken bool -} - -type PortainerClient interface { - Authenticate() (token string, err error) - GetEndpoints() ([]EndpointSubset, error) - GetStacks(swarmId string, endpointId uint32) ([]Stack, error) - CreateSwarmStack(stackName string, environmentVariables []StackEnv, stackFileContent string, swarmClusterId string, endpointId string) error - CreateComposeStack(stackName string, environmentVariables []StackEnv, stackFileContent string, endpointId string) error - UpdateStack(stack Stack, environmentVariables []StackEnv, stackFileContent string, prune bool, endpointId string) error - DeleteStack(stackId uint32) error - GetStackFileContent(stackId uint32) (content string, err error) - GetEndpointDockerInfo(endpointId string) (info map[string]interface{}, err error) - GetStatus() (Status, error) -} - -type PortainerClientImp struct { - httpClient *http.Client - url *url.URL - user string - password string - token string - doNotUseToken bool -} - -// Check if an http.Response object has errors -func checkResponseForErrors(resp *http.Response) error { - if 300 <= resp.StatusCode { - // Guess it's a GenericError - respBody := GenericError{} - err := json.NewDecoder(resp.Body).Decode(&respBody) - if err != nil { - // It's not a GenericError - bodyBytes, err := ioutil.ReadAll(resp.Body) - defer resp.Body.Close() - if err != nil { - return err - } - resp.Body = ioutil.NopCloser(bytes.NewReader(bodyBytes)) - return errors.New(string(bodyBytes)) - } - return &respBody - } - return nil -} - -// Do an http request -func (n *PortainerClientImp) do(uri, method string, request io.Reader, requestType string, headers http.Header) (resp *http.Response, err error) { - requestUrl, err := n.url.Parse(uri) - if err != nil { - return - } - - req, err := http.NewRequest(method, requestUrl.String(), request) - if err != nil { - return - } - - if headers != nil { - req.Header = headers - } - - if request != nil { - req.Header.Set("Content-Type", requestType) - } - - if !n.doNotUseToken { - if n.token == "" { - n.token, err = n.Authenticate() - if err != nil { - return - } - PrintDebug(fmt.Sprintf("Auth token: %s", n.token)) - } - req.Header.Set("Authorization", "Bearer "+n.token) - } - - PrintDebugRequest("Request", req) - - resp, err = n.httpClient.Do(req) - if err != nil { - return - } - - err = checkResponseForErrors(resp) - if err != nil { - return - } - - PrintDebugResponse("Response", resp) - - return -} - -// Do a JSON http request -func (n *PortainerClientImp) doJSON(uri, method string, request interface{}, response interface{}) error { - var body io.Reader - - if request != nil { - reqBodyBytes, err := json.Marshal(request) - if err != nil { - return err - } - body = bytes.NewReader(reqBodyBytes) - } - - resp, err := n.do(uri, method, body, "application/json", nil) - if err != nil { - return err - } - - if response != nil { - d := json.NewDecoder(resp.Body) - err := d.Decode(response) - if err != nil { - return err - } - } - - return nil -} - -// Authenticate a user to get an auth token -func (n *PortainerClientImp) Authenticate() (token string, err error) { - PrintVerbose("Getting auth token...") - - reqBody := AuthenticateUserRequest{ - Username: n.user, - Password: n.password, - } - - respBody := AuthenticateUserResponse{} - - previousDoNotUseTokenValue := n.doNotUseToken - n.doNotUseToken = true - - err = n.doJSON("auth", http.MethodPost, &reqBody, &respBody) - if err != nil { - return - } - - n.doNotUseToken = previousDoNotUseTokenValue - - token = respBody.Jwt - - return -} - -// Get endpoints -func (n *PortainerClientImp) GetEndpoints() (endpoints []EndpointSubset, err error) { - PrintVerbose("Getting endpoints...") - err = n.doJSON("endpoints", http.MethodGet, nil, &endpoints) - return -} - -// Get stacks, optionally filtered by swarmId and endpointId -func (n *PortainerClientImp) GetStacks(swarmId string, endpointId uint32) (stacks []Stack, err error) { - PrintVerbose("Getting stacks...") - - filter := StackListFilter{ - SwarmId: swarmId, - EndpointId: endpointId, - } - - filterJsonBytes, _ := json.Marshal(filter) - filterJsonString := string(filterJsonBytes) - - err = n.doJSON(fmt.Sprintf("stacks?filters=%s", filterJsonString), http.MethodGet, nil, &stacks) - return -} - -// Create swarm stack -func (n *PortainerClientImp) CreateSwarmStack(stackName string, environmentVariables []StackEnv, stackFileContent string, swarmClusterId string, endpointId string) (err error) { - PrintVerbose("Deploying stack...") - - reqBody := StackCreateRequest{ - Name: stackName, - Env: environmentVariables, - SwarmID: swarmClusterId, - StackFileContent: stackFileContent, - } - - err = n.doJSON(fmt.Sprintf("stacks?type=%v&method=%s&endpointId=%s", 1, "string", endpointId), http.MethodPost, &reqBody, nil) - return -} - -// Create compose stack -func (n *PortainerClientImp) CreateComposeStack(stackName string, environmentVariables []StackEnv, stackFileContent string, endpointId string) (err error) { - PrintVerbose("Deploying stack...") - - 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) - return -} - -// Update stack -func (n *PortainerClientImp) UpdateStack(stack Stack, environmentVariables []StackEnv, stackFileContent string, prune bool, endpointId string) (err error) { - PrintVerbose("Updating stack...") - - reqBody := StackUpdateRequest{ - Env: environmentVariables, - StackFileContent: stackFileContent, - Prune: prune, - } - - err = n.doJSON(fmt.Sprintf("stacks/%v?endpointId=%s", stack.Id, endpointId), http.MethodPut, &reqBody, nil) - return -} - -// Delete stack -func (n *PortainerClientImp) DeleteStack(stackId uint32) (err error) { - PrintVerbose("Deleting stack...") - - err = n.doJSON(fmt.Sprintf("stacks/%d", stackId), http.MethodDelete, nil, nil) - return -} - -// Get stack file content -func (n *PortainerClientImp) GetStackFileContent(stackId uint32) (content string, err error) { - PrintVerbose("Getting stack file content...") - - var respBody StackFileInspectResponse - - err = n.doJSON(fmt.Sprintf("stacks/%v/file", stackId), http.MethodGet, nil, &respBody) - if err != nil { - return - } - - content = respBody.StackFileContent - - return -} - -// Get endpoint Docker info -func (n *PortainerClientImp) GetEndpointDockerInfo(endpointId string) (info map[string]interface{}, err error) { - PrintVerbose("Getting endpoint Docker info...") - - err = n.doJSON(fmt.Sprintf("endpoints/%v/docker/info", endpointId), http.MethodGet, nil, &info) - return -} - -// Get Portainer status info -func (n *PortainerClientImp) GetStatus() (status Status, err error) { - err = n.doJSON("status", http.MethodGet, nil, &status) - return -} - -// Create a new client -func NewClient(httpClient *http.Client, config ClientConfig) (c PortainerClient, err error) { - apiUrl, err := url.Parse(strings.TrimRight(config.Url, "/") + "/api/") - if err != nil { - return - } - - c = &PortainerClientImp{ - httpClient: httpClient, - url: apiUrl, - user: config.User, - password: config.Password, - token: config.Token, - } - - return -} +var cachedClient client.PortainerClient // Get the cached client or a new one -func GetClient() (c PortainerClient, err error) { +func GetClient() (c client.PortainerClient, err error) { if cachedClient == nil { cachedClient, err = GetDefaultClient() if err != nil { @@ -304,13 +23,13 @@ func GetClient() (c PortainerClient, err error) { } // Get the default client -func GetDefaultClient() (c PortainerClient, err error) { - return NewClient(GetDefaultHttpClient(), GetDefaultClientConfig()) +func GetDefaultClient() (c client.PortainerClient, err error) { + return client.NewClient(GetDefaultHttpClient(), GetDefaultClientConfig()) } // Get the default config for a client -func GetDefaultClientConfig() ClientConfig { - return ClientConfig{ +func GetDefaultClientConfig() client.ClientConfig { + return client.ClientConfig{ Url: viper.GetString("url"), User: viper.GetString("user"), Password: viper.GetString("password"), diff --git a/common/utils.go b/common/utils.go index 378eedf..1232cf9 100644 --- a/common/utils.go +++ b/common/utils.go @@ -2,9 +2,13 @@ package common import ( "fmt" + + "github.com/greenled/portainer-stack-utils/util" + + "github.com/greenled/portainer-stack-utils/client" ) -func GetStackByName(name string) (stack Stack, err error) { +func GetStackByName(name string) (stack client.Stack, err error) { client, err := GetClient() if err != nil { return @@ -15,7 +19,7 @@ func GetStackByName(name string) (stack Stack, err error) { return } - PrintVerbose(fmt.Sprintf("Getting stack %s...", name)) + util.PrintVerbose(fmt.Sprintf("Getting stack %s...", name)) for _, stack := range stacks { if stack.Name == name { return stack, nil @@ -27,11 +31,6 @@ func GetStackByName(name string) (stack Stack, err error) { return } -type StackListFilter struct { - SwarmId string `json:",omitempty"` - EndpointId uint32 `json:",omitempty"` -} - // Custom customerrors type StackNotFoundError struct { StackName string diff --git a/common/customerrors.go b/util/errors.go similarity index 92% rename from common/customerrors.go rename to util/errors.go index 6cbc47d..b2c9d96 100644 --- a/common/customerrors.go +++ b/util/errors.go @@ -1,4 +1,4 @@ -package common +package util import ( "fmt" diff --git a/common/printing.go b/util/printing.go similarity index 99% rename from common/printing.go rename to util/printing.go index 764c831..152a1a0 100644 --- a/common/printing.go +++ b/util/printing.go @@ -1,15 +1,16 @@ -package common +package util import ( "bytes" "fmt" - "github.com/spf13/viper" "io/ioutil" "log" "net/http" "os" "strings" "text/tabwriter" + + "github.com/spf13/viper" ) func PrintVerbose(a ...interface{}) {