From e1b4a6f4361a763bdc7eff8dbb329a7b49ca9875 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Carlos=20Mej=C3=ADas=20Rodr=C3=ADguez?= Date: Fri, 23 Aug 2019 11:19:42 -0400 Subject: [PATCH 01/34] Move "getting endpoint info..." debug message out of common.GetEndpointSwarmClusterId() --- cmd/stackDeploy.go | 3 +++ cmd/stackInspect.go | 3 +++ cmd/stackList.go | 3 +++ cmd/stackRemove.go | 3 +++ common/utils.go | 3 --- 5 files changed, 12 insertions(+), 3 deletions(-) diff --git a/cmd/stackDeploy.go b/cmd/stackDeploy.go index b715a87..be43275 100644 --- a/cmd/stackDeploy.go +++ b/cmd/stackDeploy.go @@ -53,6 +53,9 @@ var stackDeployCmd = &cobra.Command{ common.CheckError(endpointRetrievalErr) } + logrus.WithFields(logrus.Fields{ + "endpoint": endpoint.Name, + }).Debug("Getting endpoint's Docker info") endpointSwarmClusterId, selectionErr := common.GetEndpointSwarmClusterId(endpoint.ID) if selectionErr == nil { // It's a swarm cluster diff --git a/cmd/stackInspect.go b/cmd/stackInspect.go index 1e1c5e7..ff52aa9 100644 --- a/cmd/stackInspect.go +++ b/cmd/stackInspect.go @@ -47,6 +47,9 @@ var stackInspectCmd = &cobra.Command{ common.CheckError(endpointRetrievalErr) } + logrus.WithFields(logrus.Fields{ + "endpoint": endpoint.Name, + }).Debug("Getting endpoint's Docker info") var selectionErr, stackRetrievalErr error endpointSwarmClusterId, selectionErr = common.GetEndpointSwarmClusterId(endpoint.ID) if selectionErr == nil { diff --git a/cmd/stackList.go b/cmd/stackList.go index a663c5c..06bff27 100644 --- a/cmd/stackList.go +++ b/cmd/stackList.go @@ -43,6 +43,9 @@ var stackListCmd = &cobra.Command{ endpoint, endpointRetrievalErr := common.GetEndpointFromListByName(endpoints, endpointName) common.CheckError(endpointRetrievalErr) + logrus.WithFields(logrus.Fields{ + "endpoint": endpoint.Name, + }).Debug("Getting endpoint's Docker info") var selectionErr error endpointSwarmClusterId, selectionErr = common.GetEndpointSwarmClusterId(endpoint.ID) if selectionErr == nil { diff --git a/cmd/stackRemove.go b/cmd/stackRemove.go index 0eeee59..30160cf 100644 --- a/cmd/stackRemove.go +++ b/cmd/stackRemove.go @@ -45,6 +45,9 @@ var stackRemoveCmd = &cobra.Command{ common.CheckError(endpointRetrievalErr) } + logrus.WithFields(logrus.Fields{ + "endpoint": endpoint.Name, + }).Debug("Getting endpoint's Docker info") var selectionErr, stackRetrievalErr error endpointSwarmClusterId, selectionErr = common.GetEndpointSwarmClusterId(endpoint.ID) if selectionErr == nil { diff --git a/common/utils.go b/common/utils.go index b8dbd5a..5d13171 100644 --- a/common/utils.go +++ b/common/utils.go @@ -138,9 +138,6 @@ func GetEndpointSwarmClusterId(endpointId portainer.EndpointID) (endpointSwarmCl return } - logrus.WithFields(logrus.Fields{ - "endpoint": endpointId, - }).Debug("Getting endpoint's Docker info") result, err := portainerClient.GetEndpointDockerInfo(endpointId) if err != nil { return From c6b7c2ce25cfe06414a5539fad3001021a07fdf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Carlos=20Mej=C3=ADas=20Rodr=C3=ADguez?= Date: Fri, 23 Aug 2019 11:21:21 -0400 Subject: [PATCH 02/34] Update stack deploy output example in the Readme "endpoint" field contains endpoint name now, not endpoint id. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1bdb3a5..4b6728b 100644 --- a/README.md +++ b/README.md @@ -183,7 +183,7 @@ psu stack deploy asd --endpoint primary --log-level debug The output would look like: ```text -DEBU[0000] Getting endpoint's Docker info endpoint=5 +DEBU[0000] Getting endpoint's Docker info endpoint=primary DEBU[0000] Getting stack endpoint=primary stack=asd DEBU[0000] Stack not found stack=asd INFO[0000] Creating stack endpoint=primary stack=asd From be51da44bf9de72f8a20d94a45726e2a0a0f0ceb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Carlos=20Mej=C3=ADas=20Rodr=C3=ADguez?= Date: Fri, 23 Aug 2019 12:11:03 -0400 Subject: [PATCH 03/34] Rename some identifiers to pass go-lint --- client/client.go | 64 ++++++++++++++++++------------------- client/client_test.go | 16 +++++----- cmd/configList.go | 4 +-- cmd/endpointGroupInspect.go | 4 +-- cmd/endpointGroupList.go | 4 +-- cmd/endpointInspect.go | 4 +-- cmd/endpointList.go | 4 +-- cmd/stackDeploy.go | 8 ++--- cmd/stackInspect.go | 10 +++--- cmd/stackList.go | 12 +++---- cmd/stackRemove.go | 10 +++--- cmd/status.go | 4 +-- common/client.go | 8 ++--- common/utils.go | 12 +++---- 14 files changed, 82 insertions(+), 82 deletions(-) diff --git a/client/client.go b/client/client.go index 9d16569..8d3686d 100644 --- a/client/client.go +++ b/client/client.go @@ -14,12 +14,12 @@ import ( ) type StackListFilter struct { - SwarmId string `json:",omitempty"` - EndpointId portainer.EndpointID `json:",omitempty"` + SwarmID string `json:"SwarmId,omitempty"` + EndpointID portainer.EndpointID `json:"EndpointId,omitempty"` } type Config struct { - Url *url.URL + URL *url.URL User string Password string Token string @@ -38,25 +38,25 @@ type PortainerClient interface { GetEndpointGroups() ([]portainer.EndpointGroup, error) // Get stacks, optionally filtered by swarmId and endpointId - GetStacks(swarmId string, endpointId portainer.EndpointID) ([]portainer.Stack, error) + GetStacks(swarmID string, endpointID portainer.EndpointID) ([]portainer.Stack, error) // Create swarm stack - CreateSwarmStack(stackName string, environmentVariables []portainer.Pair, stackFileContent string, swarmClusterId string, endpointId portainer.EndpointID) (stack portainer.Stack, err error) + CreateSwarmStack(stackName string, environmentVariables []portainer.Pair, stackFileContent string, swarmClusterID string, endpointID portainer.EndpointID) (stack portainer.Stack, err error) // Create compose stack - CreateComposeStack(stackName string, environmentVariables []portainer.Pair, stackFileContent string, endpointId portainer.EndpointID) (stack portainer.Stack, err error) + CreateComposeStack(stackName string, environmentVariables []portainer.Pair, stackFileContent string, endpointID portainer.EndpointID) (stack portainer.Stack, err error) // Update stack - UpdateStack(stack portainer.Stack, environmentVariables []portainer.Pair, stackFileContent string, prune bool, endpointId portainer.EndpointID) error + UpdateStack(stack portainer.Stack, environmentVariables []portainer.Pair, stackFileContent string, prune bool, endpointID portainer.EndpointID) error // Delete stack - DeleteStack(stackId portainer.StackID) error + DeleteStack(stackID portainer.StackID) error // Get stack file content - GetStackFileContent(stackId portainer.StackID) (content string, err error) + GetStackFileContent(stackID portainer.StackID) (content string, err error) // Get endpoint Docker info - GetEndpointDockerInfo(endpointId portainer.EndpointID) (info map[string]interface{}, err error) + GetEndpointDockerInfo(endpointID portainer.EndpointID) (info map[string]interface{}, err error) // Get Portainer status info GetStatus() (portainer.Status, error) @@ -103,12 +103,12 @@ func checkResponseForErrors(resp *http.Response) error { // 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) + requestURL, err := n.url.Parse(uri) if err != nil { return } - req, err := http.NewRequest(method, requestUrl.String(), request) + req, err := http.NewRequest(method, requestURL.String(), request) if err != nil { return } @@ -230,62 +230,62 @@ func (n *portainerClientImp) GetEndpointGroups() (endpointGroups []portainer.End return } -func (n *portainerClientImp) GetStacks(swarmId string, endpointId portainer.EndpointID) (stacks []portainer.Stack, err error) { +func (n *portainerClientImp) GetStacks(swarmID string, endpointID portainer.EndpointID) (stacks []portainer.Stack, err error) { filter := StackListFilter{ - SwarmId: swarmId, - EndpointId: endpointId, + SwarmID: swarmID, + EndpointID: endpointID, } - filterJsonBytes, _ := json.Marshal(filter) - filterJsonString := string(filterJsonBytes) + filterJSONBytes, _ := json.Marshal(filter) + filterJSONString := string(filterJSONBytes) - err = n.doJSON(fmt.Sprintf("stacks?filters=%s", filterJsonString), http.MethodGet, nil, &stacks) + err = n.doJSON(fmt.Sprintf("stacks?filters=%s", filterJSONString), http.MethodGet, nil, &stacks) return } -func (n *portainerClientImp) CreateSwarmStack(stackName string, environmentVariables []portainer.Pair, stackFileContent string, swarmClusterId string, endpointId portainer.EndpointID) (stack portainer.Stack, err error) { +func (n *portainerClientImp) CreateSwarmStack(stackName string, environmentVariables []portainer.Pair, stackFileContent string, swarmClusterID string, endpointID portainer.EndpointID) (stack portainer.Stack, err error) { reqBody := StackCreateRequest{ Name: stackName, Env: environmentVariables, - SwarmID: swarmClusterId, + SwarmID: swarmClusterID, StackFileContent: stackFileContent, } - err = n.doJSON(fmt.Sprintf("stacks?type=%v&method=%s&endpointId=%v", 1, "string", endpointId), http.MethodPost, &reqBody, &stack) + err = n.doJSON(fmt.Sprintf("stacks?type=%v&method=%s&endpointId=%v", 1, "string", endpointID), http.MethodPost, &reqBody, &stack) return } -func (n *portainerClientImp) CreateComposeStack(stackName string, environmentVariables []portainer.Pair, stackFileContent string, endpointId portainer.EndpointID) (stack portainer.Stack, err error) { +func (n *portainerClientImp) CreateComposeStack(stackName string, environmentVariables []portainer.Pair, stackFileContent string, endpointID portainer.EndpointID) (stack portainer.Stack, err error) { reqBody := StackCreateRequest{ Name: stackName, Env: environmentVariables, StackFileContent: stackFileContent, } - err = n.doJSON(fmt.Sprintf("stacks?type=%v&method=%s&endpointId=%v", 2, "string", endpointId), http.MethodPost, &reqBody, &stack) + err = n.doJSON(fmt.Sprintf("stacks?type=%v&method=%s&endpointId=%v", 2, "string", endpointID), http.MethodPost, &reqBody, &stack) return } -func (n *portainerClientImp) UpdateStack(stack portainer.Stack, environmentVariables []portainer.Pair, stackFileContent string, prune bool, endpointId portainer.EndpointID) (err error) { +func (n *portainerClientImp) UpdateStack(stack portainer.Stack, environmentVariables []portainer.Pair, stackFileContent string, prune bool, endpointID portainer.EndpointID) (err error) { reqBody := StackUpdateRequest{ Env: environmentVariables, StackFileContent: stackFileContent, Prune: prune, } - err = n.doJSON(fmt.Sprintf("stacks/%v?endpointId=%v", stack.ID, endpointId), http.MethodPut, &reqBody, nil) + err = n.doJSON(fmt.Sprintf("stacks/%v?endpointId=%v", stack.ID, endpointID), http.MethodPut, &reqBody, nil) return } -func (n *portainerClientImp) DeleteStack(stackId portainer.StackID) (err error) { - err = n.doJSON(fmt.Sprintf("stacks/%d", stackId), http.MethodDelete, nil, nil) +func (n *portainerClientImp) DeleteStack(stackID portainer.StackID) (err error) { + err = n.doJSON(fmt.Sprintf("stacks/%d", stackID), http.MethodDelete, nil, nil) return } -func (n *portainerClientImp) GetStackFileContent(stackId portainer.StackID) (content string, err error) { +func (n *portainerClientImp) GetStackFileContent(stackID portainer.StackID) (content string, err error) { var respBody StackFileInspectResponse - err = n.doJSON(fmt.Sprintf("stacks/%v/file", stackId), http.MethodGet, nil, &respBody) + err = n.doJSON(fmt.Sprintf("stacks/%v/file", stackID), http.MethodGet, nil, &respBody) if err != nil { return } @@ -295,8 +295,8 @@ func (n *portainerClientImp) GetStackFileContent(stackId portainer.StackID) (con return } -func (n *portainerClientImp) GetEndpointDockerInfo(endpointId portainer.EndpointID) (info map[string]interface{}, err error) { - err = n.doJSON(fmt.Sprintf("endpoints/%v/docker/info", endpointId), http.MethodGet, nil, &info) +func (n *portainerClientImp) GetEndpointDockerInfo(endpointID portainer.EndpointID) (info map[string]interface{}, err error) { + err = n.doJSON(fmt.Sprintf("endpoints/%v/docker/info", endpointID), http.MethodGet, nil, &info) return } @@ -309,7 +309,7 @@ func (n *portainerClientImp) GetStatus() (status portainer.Status, err error) { func NewClient(httpClient *http.Client, config Config) PortainerClient { return &portainerClientImp{ httpClient: httpClient, - url: config.Url, + url: config.URL, user: config.User, password: config.Password, token: config.Token, diff --git a/client/client_test.go b/client/client_test.go index d752efa..41576c6 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -12,24 +12,24 @@ import ( "github.com/stretchr/testify/assert" ) -func readRequestBodyAsJson(req *http.Request, body *map[string]interface{}) (err error) { +func readRequestBodyAsJSON(req *http.Request, body *map[string]interface{}) (err error) { bodyBytes, err := ioutil.ReadAll(req.Body) defer req.Body.Close() err = json.Unmarshal(bodyBytes, body) return } -func writeResponseBodyAsJson(w http.ResponseWriter, body map[string]interface{}) (err error) { +func writeResponseBodyAsJSON(w http.ResponseWriter, body map[string]interface{}) (err error) { bodyBytes, err := json.Marshal(body) fmt.Fprintln(w, string(bodyBytes)) return } func TestNewClient(t *testing.T) { - apiUrl, _ := url.Parse("http://validurl.com/api") + apiURL, _ := url.Parse("http://validurl.com/api") validClient := NewClient(http.DefaultClient, Config{ - Url: apiUrl, + URL: apiURL, }) assert.NotNil(t, validClient) } @@ -37,7 +37,7 @@ func TestNewClient(t *testing.T) { func TestClientAuthenticates(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { var body map[string]interface{} - err := readRequestBodyAsJson(req, &body) + err := readRequestBodyAsJSON(req, &body) assert.Equal(t, req.Method, http.MethodPost) assert.Equal(t, req.RequestURI, "/api/auth") @@ -53,16 +53,16 @@ func TestClientAuthenticates(t *testing.T) { assert.NotNil(t, body["Password"]) assert.Equal(t, body["Password"], "a") - writeResponseBodyAsJson(w, map[string]interface{}{ + writeResponseBodyAsJSON(w, map[string]interface{}{ "jwt": "somerandomtoken", }) })) defer ts.Close() - apiUrl, _ := url.Parse(ts.URL + "/api/") + apiURL, _ := url.Parse(ts.URL + "/api/") customClient := NewClient(ts.Client(), Config{ - Url: apiUrl, + URL: apiURL, User: "admin", Password: "a", UserAgent: "GE007", diff --git a/cmd/configList.go b/cmd/configList.go index 5c76e20..2942758 100644 --- a/cmd/configList.go +++ b/cmd/configList.go @@ -77,9 +77,9 @@ var configListCmd = &cobra.Command{ common.CheckError(flushErr) case "json": // Print configs in a json format - statusJsonBytes, err := json.Marshal(configs) + statusJSONBytes, err := json.Marshal(configs) common.CheckError(err) - fmt.Println(string(statusJsonBytes)) + fmt.Println(string(statusJSONBytes)) default: // Print configs in a custom format template, templateParsingErr := template.New("configTpl").Parse(viper.GetString("config.list.format")) diff --git a/cmd/endpointGroupInspect.go b/cmd/endpointGroupInspect.go index 606b47a..cc3a1ba 100644 --- a/cmd/endpointGroupInspect.go +++ b/cmd/endpointGroupInspect.go @@ -44,9 +44,9 @@ var endpointGroupInspectCmd = &cobra.Command{ common.CheckError(err) case "json": // Print endpoint group in a json format - endpointJsonBytes, err := json.Marshal(endpointGroup) + endpointJSONBytes, err := json.Marshal(endpointGroup) common.CheckError(err) - fmt.Println(string(endpointJsonBytes)) + fmt.Println(string(endpointJSONBytes)) default: // Print endpoint group in a custom format template, err := template.New("endpointGroupTpl").Parse(viper.GetString("endpoint.group.inspect.format")) diff --git a/cmd/endpointGroupList.go b/cmd/endpointGroupList.go index 5722d8f..b3f094d 100644 --- a/cmd/endpointGroupList.go +++ b/cmd/endpointGroupList.go @@ -55,9 +55,9 @@ var endpointGroupListCmd = &cobra.Command{ common.CheckError(flushErr) case "json": // Print endpoint groups in a json format - statusJsonBytes, err := json.Marshal(endpointGroups) + statusJSONBytes, err := json.Marshal(endpointGroups) common.CheckError(err) - fmt.Println(string(statusJsonBytes)) + fmt.Println(string(statusJSONBytes)) default: // Print endpoint groups in a custom format template, templateParsingErr := template.New("endpointGroupTpl").Parse(viper.GetString("endpoint.group.list.format")) diff --git a/cmd/endpointInspect.go b/cmd/endpointInspect.go index cdce6b4..1fb98eb 100644 --- a/cmd/endpointInspect.go +++ b/cmd/endpointInspect.go @@ -74,9 +74,9 @@ var endpointInspectCmd = &cobra.Command{ common.CheckError(err) case "json": // Print endpoint in a json format - endpointJsonBytes, err := json.Marshal(endpoint) + endpointJSONBytes, err := json.Marshal(endpoint) common.CheckError(err) - fmt.Println(string(endpointJsonBytes)) + fmt.Println(string(endpointJSONBytes)) default: // Print endpoint in a custom format template, err := template.New("endpointTpl").Parse(viper.GetString("endpoint.inspect.format")) diff --git a/cmd/endpointList.go b/cmd/endpointList.go index ac23979..df6f27f 100644 --- a/cmd/endpointList.go +++ b/cmd/endpointList.go @@ -71,9 +71,9 @@ var endpointListCmd = &cobra.Command{ common.CheckError(flushErr) case "json": // Print endpoints in a json format - statusJsonBytes, err := json.Marshal(endpoints) + statusJSONBytes, err := json.Marshal(endpoints) common.CheckError(err) - fmt.Println(string(statusJsonBytes)) + fmt.Println(string(statusJSONBytes)) default: // Print endpoints in a custom format template, templateParsingErr := template.New("endpointTpl").Parse(viper.GetString("endpoint.list.format")) diff --git a/cmd/stackDeploy.go b/cmd/stackDeploy.go index be43275..b8059cd 100644 --- a/cmd/stackDeploy.go +++ b/cmd/stackDeploy.go @@ -56,7 +56,7 @@ var stackDeployCmd = &cobra.Command{ logrus.WithFields(logrus.Fields{ "endpoint": endpoint.Name, }).Debug("Getting endpoint's Docker info") - endpointSwarmClusterId, selectionErr := common.GetEndpointSwarmClusterId(endpoint.ID) + endpointSwarmClusterID, selectionErr := common.GetEndpointSwarmClusterID(endpoint.ID) if selectionErr == nil { // It's a swarm cluster } else if selectionErr == common.ErrStackClusterNotFound { @@ -70,7 +70,7 @@ var stackDeployCmd = &cobra.Command{ "stack": stackName, "endpoint": endpoint.Name, }).Debug("Getting stack") - retrievedStack, stackRetrievalErr := common.GetStackByName(stackName, endpointSwarmClusterId, endpoint.ID) + retrievedStack, stackRetrievalErr := common.GetStackByName(stackName, endpointSwarmClusterID, endpoint.ID) if stackRetrievalErr == nil { // We are updating an existing stack logrus.WithFields(logrus.Fields{ @@ -129,13 +129,13 @@ var stackDeployCmd = &cobra.Command{ stackFileContent, loadingErr := loadStackFile(viper.GetString("stack.deploy.stack-file")) common.CheckError(loadingErr) - if endpointSwarmClusterId != "" { + if endpointSwarmClusterID != "" { // It's a swarm cluster logrus.WithFields(logrus.Fields{ "stack": stackName, "endpoint": endpoint.Name, }).Info("Creating stack") - stack, deploymentErr := portainerClient.CreateSwarmStack(stackName, loadedEnvironmentVariables, stackFileContent, endpointSwarmClusterId, endpoint.ID) + stack, deploymentErr := portainerClient.CreateSwarmStack(stackName, loadedEnvironmentVariables, stackFileContent, endpointSwarmClusterID, endpoint.ID) common.CheckError(deploymentErr) logrus.WithFields(logrus.Fields{ "stack": stack.Name, diff --git a/cmd/stackInspect.go b/cmd/stackInspect.go index ff52aa9..5bdd1eb 100644 --- a/cmd/stackInspect.go +++ b/cmd/stackInspect.go @@ -24,7 +24,7 @@ var stackInspectCmd = &cobra.Command{ Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { stackName := args[0] - var endpointSwarmClusterId string + var endpointSwarmClusterID string var stack portainer.Stack var endpoint portainer.Endpoint @@ -51,14 +51,14 @@ var stackInspectCmd = &cobra.Command{ "endpoint": endpoint.Name, }).Debug("Getting endpoint's Docker info") var selectionErr, stackRetrievalErr error - endpointSwarmClusterId, selectionErr = common.GetEndpointSwarmClusterId(endpoint.ID) + 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) + stack, stackRetrievalErr = common.GetStackByName(stackName, endpointSwarmClusterID, endpoint.ID) } else if selectionErr == common.ErrStackClusterNotFound { // It's not a swarm cluster logrus.WithFields(logrus.Fields{ @@ -95,9 +95,9 @@ var stackInspectCmd = &cobra.Command{ common.CheckError(flushErr) case "json": // Print stack in a json format - stackJsonBytes, err := json.Marshal(stack) + stackJSONBytes, err := json.Marshal(stack) common.CheckError(err) - fmt.Println(string(stackJsonBytes)) + fmt.Println(string(stackJSONBytes)) default: // Print stack in a custom format template, templateParsingErr := template.New("stackTpl").Parse(viper.GetString("stack.inspect.format")) diff --git a/cmd/stackList.go b/cmd/stackList.go index 06bff27..2e8b735 100644 --- a/cmd/stackList.go +++ b/cmd/stackList.go @@ -36,7 +36,7 @@ var stackListCmd = &cobra.Command{ endpoints, endpointsRetrievalErr := portainerClient.GetEndpoints() common.CheckError(endpointsRetrievalErr) - var endpointSwarmClusterId string + var endpointSwarmClusterID string var stacks []portainer.Stack if endpointName := viper.GetString("stack.list.endpoint"); endpointName != "" { // Get endpoint by name @@ -47,13 +47,13 @@ var stackListCmd = &cobra.Command{ "endpoint": endpoint.Name, }).Debug("Getting endpoint's Docker info") var selectionErr error - endpointSwarmClusterId, selectionErr = common.GetEndpointSwarmClusterId(endpoint.ID) + endpointSwarmClusterID, selectionErr = common.GetEndpointSwarmClusterID(endpoint.ID) if selectionErr == nil { // It's a swarm cluster logrus.WithFields(logrus.Fields{ "endpoint": endpoint.Name, }).Debug("Getting stacks") - stacks, err = portainerClient.GetStacks(endpointSwarmClusterId, endpoint.ID) + stacks, err = portainerClient.GetStacks(endpointSwarmClusterID, endpoint.ID) common.CheckError(err) } else if selectionErr == common.ErrStackClusterNotFound { // It's not a swarm cluster @@ -83,7 +83,7 @@ var stackListCmd = &cobra.Command{ }) common.CheckError(err) for _, s := range stacks { - stackEndpoint, err := common.GetEndpointFromListById(endpoints, s.EndpointID) + stackEndpoint, err := common.GetEndpointFromListByID(endpoints, s.EndpointID) common.CheckError(err) _, err = fmt.Fprintln(writer, fmt.Sprintf( "%v\t%s\t%v\t%s", @@ -98,9 +98,9 @@ var stackListCmd = &cobra.Command{ common.CheckError(flushErr) case "json": // Print stacks in a json format - stacksJsonBytes, err := json.Marshal(stacks) + stacksJSONBytes, err := json.Marshal(stacks) common.CheckError(err) - fmt.Println(string(stacksJsonBytes)) + fmt.Println(string(stacksJSONBytes)) default: // Print stacks in a custom format template, templateParsingErr := template.New("stackTpl").Parse(viper.GetString("stack.list.format")) diff --git a/cmd/stackRemove.go b/cmd/stackRemove.go index 30160cf..0e147d5 100644 --- a/cmd/stackRemove.go +++ b/cmd/stackRemove.go @@ -22,7 +22,7 @@ var stackRemoveCmd = &cobra.Command{ common.CheckError(clientRetrievalErr) stackName := args[0] - var endpointSwarmClusterId string + var endpointSwarmClusterID string var stack portainer.Stack var endpoint portainer.Endpoint @@ -49,14 +49,14 @@ var stackRemoveCmd = &cobra.Command{ "endpoint": endpoint.Name, }).Debug("Getting endpoint's Docker info") var selectionErr, stackRetrievalErr error - endpointSwarmClusterId, selectionErr = common.GetEndpointSwarmClusterId(endpoint.ID) + 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) + stack, stackRetrievalErr = common.GetStackByName(stackName, endpointSwarmClusterID, endpoint.ID) } else if selectionErr == common.ErrStackClusterNotFound { // It's not a swarm cluster logrus.WithFields(logrus.Fields{ @@ -71,13 +71,13 @@ var stackRemoveCmd = &cobra.Command{ if stackRetrievalErr == nil { // The stack exists - stackId := stack.ID + stackID := stack.ID logrus.WithFields(logrus.Fields{ "stack": stackName, "endpoint": endpoint.Name, }).Info("Removing stack") - err := portainerClient.DeleteStack(stackId) + err := portainerClient.DeleteStack(stackID) common.CheckError(err) logrus.WithFields(logrus.Fields{ "stack": stack.Name, diff --git a/cmd/status.go b/cmd/status.go index 3534a6c..7602a8c 100644 --- a/cmd/status.go +++ b/cmd/status.go @@ -53,9 +53,9 @@ var statusCmd = &cobra.Command{ common.CheckError(flushErr) case "json": // Print status in a json format - statusJsonBytes, err := json.Marshal(respBody) + statusJSONBytes, err := json.Marshal(respBody) common.CheckError(err) - fmt.Println(string(statusJsonBytes)) + fmt.Println(string(statusJSONBytes)) default: // Print status in a custom format template, templateParsingErr := template.New("statusTpl").Parse(viper.GetString("status.format")) diff --git a/common/client.go b/common/client.go index 742d3f9..c8114f1 100644 --- a/common/client.go +++ b/common/client.go @@ -36,7 +36,7 @@ func GetDefaultClient() (c client.PortainerClient, err error) { return } - c = client.NewClient(GetDefaultHttpClient(), config) + c = client.NewClient(GetDefaultHTTPClient(), config) c.BeforeRequest(func(req *http.Request) (err error) { var bodyString string @@ -84,13 +84,13 @@ func GetDefaultClient() (c client.PortainerClient, err error) { // Get the default config for a client func GetDefaultClientConfig() (config client.Config, err error) { - apiUrl, err := url.Parse(strings.TrimRight(viper.GetString("url"), "/") + "/api/") + apiURL, err := url.Parse(strings.TrimRight(viper.GetString("url"), "/") + "/api/") if err != nil { return } config = client.Config{ - Url: apiUrl, + URL: apiURL, User: viper.GetString("user"), Password: viper.GetString("password"), Token: viper.GetString("auth-token"), @@ -102,7 +102,7 @@ func GetDefaultClientConfig() (config client.Config, err error) { } // Get the default http client for a Portainer client -func GetDefaultHttpClient() *http.Client { +func GetDefaultHTTPClient() *http.Client { return &http.Client{ Timeout: viper.GetDuration("timeout"), Transport: &http.Transport{ diff --git a/common/utils.go b/common/utils.go index 5d13171..a4f2705 100644 --- a/common/utils.go +++ b/common/utils.go @@ -53,13 +53,13 @@ func GetDefaultEndpoint() (endpoint portainer.Endpoint, err error) { return } -func GetStackByName(name string, swarmId string, endpointId portainer.EndpointID) (stack portainer.Stack, err error) { +func GetStackByName(name string, swarmID string, endpointID portainer.EndpointID) (stack portainer.Stack, err error) { portainerClient, err := GetClient() if err != nil { return } - stacks, err := portainerClient.GetStacks(swarmId, endpointId) + stacks, err := portainerClient.GetStacks(swarmID, endpointID) if err != nil { return } @@ -113,7 +113,7 @@ func GetEndpointGroupByName(name string) (endpointGroup portainer.EndpointGroup, return } -func GetEndpointFromListById(endpoints []portainer.Endpoint, id portainer.EndpointID) (endpoint portainer.Endpoint, err error) { +func GetEndpointFromListByID(endpoints []portainer.Endpoint, id portainer.EndpointID) (endpoint portainer.Endpoint, err error) { for i := range endpoints { if endpoints[i].ID == id { return endpoints[i], err @@ -131,14 +131,14 @@ func GetEndpointFromListByName(endpoints []portainer.Endpoint, name string) (end return endpoint, ErrEndpointNotFound } -func GetEndpointSwarmClusterId(endpointId portainer.EndpointID) (endpointSwarmClusterId string, err error) { +func GetEndpointSwarmClusterID(endpointID portainer.EndpointID) (endpointSwarmClusterID string, err error) { // Get docker information for endpoint portainerClient, err := GetClient() if err != nil { return } - result, err := portainerClient.GetEndpointDockerInfo(endpointId) + result, err := portainerClient.GetEndpointDockerInfo(endpointID) if err != nil { return } @@ -146,7 +146,7 @@ func GetEndpointSwarmClusterId(endpointId portainer.EndpointID) (endpointSwarmCl // Get swarm (if any) information for endpoint id, selectionErr := selectValue(result, []string{"Swarm", "Cluster", "ID"}) if selectionErr == nil { - endpointSwarmClusterId = id.(string) + endpointSwarmClusterID = id.(string) } else if selectionErr == valueNotFoundError { err = ErrStackClusterNotFound } else { From 161fc48b5ae4760c9b3bd72c0efd57b611533e8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Carlos=20Mej=C3=ADas=20Rodr=C3=ADguez?= Date: Fri, 23 Aug 2019 13:08:08 -0400 Subject: [PATCH 04/34] Add comments to exported types to pass go-lint --- client/client.go | 5 ++++- client/portainerTypes.go | 7 +++++++ common/client.go | 8 ++++---- common/configs.go | 2 ++ common/printing.go | 1 + common/utils.go | 14 ++++++++++++++ version/version.go | 3 +++ 7 files changed, 35 insertions(+), 5 deletions(-) diff --git a/client/client.go b/client/client.go index 8d3686d..f0f81ff 100644 --- a/client/client.go +++ b/client/client.go @@ -13,11 +13,13 @@ import ( portainer "github.com/portainer/portainer/api" ) +// StackListFilter represents a filter for a stack list type StackListFilter struct { SwarmID string `json:"SwarmId,omitempty"` EndpointID portainer.EndpointID `json:"EndpointId,omitempty"` } +// Config represents a Portainer client configuration type Config struct { URL *url.URL User string @@ -27,6 +29,7 @@ type Config struct { DoNotUseToken bool } +// PortainerClient represents a Portainer API client type PortainerClient interface { // Authenticate a user to get an auth token Authenticate() (token string, err error) @@ -305,7 +308,7 @@ func (n *portainerClientImp) GetStatus() (status portainer.Status, err error) { return } -// Create a new client +// NewClient creates a new Portainer API client func NewClient(httpClient *http.Client, config Config) PortainerClient { return &portainerClientImp{ httpClient: httpClient, diff --git a/client/portainerTypes.go b/client/portainerTypes.go index 68f2f15..359a29f 100644 --- a/client/portainerTypes.go +++ b/client/portainerTypes.go @@ -6,6 +6,7 @@ import ( portainer "github.com/portainer/portainer/api" ) +// GetTranslatedStackType returns a stack's Type field (int) translated to it's human readable form (string) func GetTranslatedStackType(s portainer.Stack) string { switch s.Type { case 1: @@ -17,6 +18,7 @@ func GetTranslatedStackType(s portainer.Stack) string { } } +// StackCreateRequest represents the body of a request to POST /stacks type StackCreateRequest struct { Name string SwarmID string @@ -24,16 +26,19 @@ type StackCreateRequest struct { Env []portainer.Pair `json:",omitempty"` } +// StackUpdateRequest represents the body of a request to PUT /stacks/{id} type StackUpdateRequest struct { StackFileContent string Env []portainer.Pair `json:",omitempty"` Prune bool } +// StackFileInspectResponse represents the body of a response for a request to GET /stack/{id}/file type StackFileInspectResponse struct { StackFileContent string } +// GenericError represents the body of a generic error returned by the Portainer API type GenericError struct { Err string Details string @@ -47,11 +52,13 @@ func (e *GenericError) Error() string { } } +// AuthenticateUserRequest represents the body of a request to POST /auth type AuthenticateUserRequest struct { Username string Password string } +// AuthenticateUserResponse represents the body of a response for a request to POST /auth type AuthenticateUserResponse struct { Jwt string } diff --git a/common/client.go b/common/client.go index c8114f1..57baef3 100644 --- a/common/client.go +++ b/common/client.go @@ -18,7 +18,7 @@ import ( var cachedClient client.PortainerClient -// Get the cached client or a new one +// GetClient returns the cached Portainer API client. If none is present, creates and returns a new one). func GetClient() (c client.PortainerClient, err error) { if cachedClient == nil { cachedClient, err = GetDefaultClient() @@ -29,7 +29,7 @@ func GetClient() (c client.PortainerClient, err error) { return cachedClient, nil } -// Get the default client +// GetDefaultClient returns a new Portainer API client with the default configuration func GetDefaultClient() (c client.PortainerClient, err error) { config, err := GetDefaultClientConfig() if err != nil { @@ -82,7 +82,7 @@ func GetDefaultClient() (c client.PortainerClient, err error) { return } -// Get the default config for a client +// GetDefaultClientConfig returns the default configuration for a Portainer API client func GetDefaultClientConfig() (config client.Config, err error) { apiURL, err := url.Parse(strings.TrimRight(viper.GetString("url"), "/") + "/api/") if err != nil { @@ -101,7 +101,7 @@ func GetDefaultClientConfig() (config client.Config, err error) { return } -// Get the default http client for a Portainer client +// GetDefaultHTTPClient returns the default HTTP client for a Portainer API client func GetDefaultHTTPClient() *http.Client { return &http.Client{ Timeout: viper.GetDuration("timeout"), diff --git a/common/configs.go b/common/configs.go index 59a2348..0c18842 100644 --- a/common/configs.go +++ b/common/configs.go @@ -8,6 +8,7 @@ import ( "github.com/spf13/viper" ) +// LoadCofig loads the configuration file currently used by viper into a new viper instance func LoadCofig() (v *viper.Viper, err error) { // Set config file name var configFile string @@ -34,6 +35,7 @@ func LoadCofig() (v *viper.Viper, err error) { return } +// CheckConfigKeyExists checks a given configuration key exists in the default viper func CheckConfigKeyExists(key string) (keyExists bool) { for _, k := range viper.AllKeys() { if k == key { diff --git a/common/printing.go b/common/printing.go index 218d210..cfe163b 100644 --- a/common/printing.go +++ b/common/printing.go @@ -7,6 +7,7 @@ import ( "text/tabwriter" ) +// NewTabWriter returns a new tabwriter.Writer func NewTabWriter(headers []string) (*tabwriter.Writer, error) { writer := tabwriter.NewWriter(os.Stdout, 20, 2, 3, ' ', 0) _, err := fmt.Fprintln(writer, strings.Join(headers, "\t")) diff --git a/common/utils.go b/common/utils.go index a4f2705..f9ce174 100644 --- a/common/utils.go +++ b/common/utils.go @@ -8,6 +8,7 @@ import ( "github.com/sirupsen/logrus" ) +// Common errors const ( ErrStackNotFound = Error("Stack not found") ErrStackClusterNotFound = Error("Stack cluster not found") @@ -29,6 +30,7 @@ func (e Error) Error() string { return string(e) } +// GetDefaultEndpoint returns the default endpoint (if only one endpoint exists) func GetDefaultEndpoint() (endpoint portainer.Endpoint, err error) { portainerClient, err := GetClient() if err != nil { @@ -53,6 +55,8 @@ func GetDefaultEndpoint() (endpoint portainer.Endpoint, err error) { return } +// GetStackByName returns a stack by its name from the (endpoint filtered) list +// of all stacks func GetStackByName(name string, swarmID string, endpointID portainer.EndpointID) (stack portainer.Stack, err error) { portainerClient, err := GetClient() if err != nil { @@ -73,6 +77,8 @@ func GetStackByName(name string, swarmID string, endpointID portainer.EndpointID return } +// GetEndpointByName returns an endpoint by its name from the list of all +// endpoints func GetEndpointByName(name string) (endpoint portainer.Endpoint, err error) { portainerClient, err := GetClient() if err != nil { @@ -93,6 +99,8 @@ func GetEndpointByName(name string) (endpoint portainer.Endpoint, err error) { return } +// GetEndpointGroupByName returns an endpoint group by its name from the list +// of all endpoint groups func GetEndpointGroupByName(name string) (endpointGroup portainer.EndpointGroup, err error) { portainerClient, err := GetClient() if err != nil { @@ -113,6 +121,8 @@ func GetEndpointGroupByName(name string) (endpointGroup portainer.EndpointGroup, return } +// GetEndpointFromListByID returns an endpoint by its id from a list of +// endpoints func GetEndpointFromListByID(endpoints []portainer.Endpoint, id portainer.EndpointID) (endpoint portainer.Endpoint, err error) { for i := range endpoints { if endpoints[i].ID == id { @@ -122,6 +132,8 @@ func GetEndpointFromListByID(endpoints []portainer.Endpoint, id portainer.Endpoi return endpoint, ErrEndpointNotFound } +// GetEndpointFromListByName returns an endpoint by its name from a list of +// endpoints func GetEndpointFromListByName(endpoints []portainer.Endpoint, name string) (endpoint portainer.Endpoint, err error) { for i := range endpoints { if endpoints[i].Name == name { @@ -131,6 +143,7 @@ func GetEndpointFromListByName(endpoints []portainer.Endpoint, name string) (end return endpoint, ErrEndpointNotFound } +// GetEndpointSwarmClusterID returns an endpoint's swarm cluster id func GetEndpointSwarmClusterID(endpointID portainer.EndpointID) (endpointSwarmClusterID string, err error) { // Get docker information for endpoint portainerClient, err := GetClient() @@ -167,6 +180,7 @@ func selectValue(jsonMap map[string]interface{}, jsonPath []string) (interface{} } } +// GetFormatHelp returns the help string for --format flags func GetFormatHelp(v interface{}) (r string) { typeOfV := reflect.TypeOf(v) r = fmt.Sprintf(` diff --git a/version/version.go b/version/version.go index 734f199..3308067 100644 --- a/version/version.go +++ b/version/version.go @@ -19,6 +19,7 @@ var ( buildDate string ) +// BuildVersionString returns the tool's version func BuildVersionString() string { osArch := runtime.GOOS + "/" + runtime.GOARCH @@ -33,6 +34,8 @@ func BuildVersionString() string { return fmt.Sprintf("%s %s %s BuildDate: %s", programName, version, osArch, buildDate) } +// BuildUseAgentString returns the tool's User-Agent in requests to the +// Portainer API func BuildUseAgentString() string { var theVersion = version if theVersion == "" { From 3194260c05e9579e00f331e88b3c27ef5b299afc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Carlos=20Mej=C3=ADas=20Rodr=C3=ADguez?= Date: Fri, 23 Aug 2019 13:14:49 -0400 Subject: [PATCH 05/34] Remove else statement with single return sentence --- client/portainerTypes.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/client/portainerTypes.go b/client/portainerTypes.go index 359a29f..0e421b5 100644 --- a/client/portainerTypes.go +++ b/client/portainerTypes.go @@ -47,9 +47,8 @@ type GenericError struct { func (e *GenericError) Error() string { if e.Details != "" { return fmt.Sprintf("%s: %s", e.Err, e.Details) - } else { - return fmt.Sprintf("%s", e.Err) } + return fmt.Sprintf("%s", e.Err) } // AuthenticateUserRequest represents the body of a request to POST /auth From c0cc50c81ad9186f260409dc75b9dea5fccd7258 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Carlos=20Mej=C3=ADas=20Rodr=C3=ADguez?= Date: Fri, 23 Aug 2019 13:55:30 -0400 Subject: [PATCH 06/34] Add unit test for client/portainerTypes.go --- client/portainerTypes_test.go | 101 ++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 client/portainerTypes_test.go diff --git a/client/portainerTypes_test.go b/client/portainerTypes_test.go new file mode 100644 index 0000000..e9adf01 --- /dev/null +++ b/client/portainerTypes_test.go @@ -0,0 +1,101 @@ +package client + +import ( + "testing" + + portainer "github.com/portainer/portainer/api" + "github.com/stretchr/testify/assert" +) + +func TestGetTranslatedStackType(t *testing.T) { + type args struct { + s portainer.Stack + } + tests := []struct { + name string + args args + want string + }{ + { + name: "swarm stack type", + args: args{ + s: portainer.Stack{ + Type: 1, + }, + }, + want: "swarm", + }, + { + name: "compose stack type", + args: args{ + s: portainer.Stack{ + Type: 2, + }, + }, + want: "compose", + }, + { + name: "unknown stack type", + args: args{ + s: portainer.Stack{ + Type: 100, + }, + }, + want: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, GetTranslatedStackType(tt.args.s)) + }) + } +} + +func TestGenericError_Error(t *testing.T) { + type fields struct { + Err string + Details string + } + tests := []struct { + name string + fields fields + want string + }{ + { + name: "error with message and details", + fields: fields{ + Err: "error", + Details: "details", + }, + want: "error: details", + }, + { + name: "error with message and no details", + fields: fields{ + Err: "error", + }, + want: "error", + }, + { + name: "error with no error message and details", + fields: fields{ + Details: "details", + }, + want: ": details", + }, + { + name: "error with no error message and no details", + fields: fields{}, + want: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + e := &GenericError{ + Err: tt.fields.Err, + Details: tt.fields.Details, + } + assert.Equal(t, tt.want, e.Error()) + }) + } +} From f82f116e1bce4ca7641eb06c7be5823f47f827f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Carlos=20Mej=C3=ADas=20Rodr=C3=ADguez?= Date: Fri, 23 Aug 2019 14:07:06 -0400 Subject: [PATCH 07/34] Change GetTranslatedStackType to use StackType instead of Stack --- client/portainerTypes.go | 8 ++++---- client/portainerTypes_test.go | 16 +++++----------- cmd/stackInspect.go | 2 +- cmd/stackList.go | 2 +- 4 files changed, 11 insertions(+), 17 deletions(-) diff --git a/client/portainerTypes.go b/client/portainerTypes.go index 0e421b5..48d9d9a 100644 --- a/client/portainerTypes.go +++ b/client/portainerTypes.go @@ -7,11 +7,11 @@ import ( ) // GetTranslatedStackType returns a stack's Type field (int) translated to it's human readable form (string) -func GetTranslatedStackType(s portainer.Stack) string { - switch s.Type { - case 1: +func GetTranslatedStackType(t portainer.StackType) string { + switch t { + case portainer.DockerSwarmStack: return "swarm" - case 2: + case portainer.DockerComposeStack: return "compose" default: return "" diff --git a/client/portainerTypes_test.go b/client/portainerTypes_test.go index e9adf01..eb7050e 100644 --- a/client/portainerTypes_test.go +++ b/client/portainerTypes_test.go @@ -9,7 +9,7 @@ import ( func TestGetTranslatedStackType(t *testing.T) { type args struct { - s portainer.Stack + t portainer.StackType } tests := []struct { name string @@ -19,34 +19,28 @@ func TestGetTranslatedStackType(t *testing.T) { { name: "swarm stack type", args: args{ - s: portainer.Stack{ - Type: 1, - }, + t: portainer.DockerSwarmStack, }, want: "swarm", }, { name: "compose stack type", args: args{ - s: portainer.Stack{ - Type: 2, - }, + t: portainer.DockerComposeStack, }, want: "compose", }, { name: "unknown stack type", args: args{ - s: portainer.Stack{ - Type: 100, - }, + t: 100, }, want: "", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assert.Equal(t, tt.want, GetTranslatedStackType(tt.args.s)) + assert.Equal(t, tt.want, GetTranslatedStackType(tt.args.t)) }) } } diff --git a/cmd/stackInspect.go b/cmd/stackInspect.go index 5bdd1eb..829ea01 100644 --- a/cmd/stackInspect.go +++ b/cmd/stackInspect.go @@ -87,7 +87,7 @@ var stackInspectCmd = &cobra.Command{ "%v\t%s\t%v\t%s", stack.ID, stack.Name, - client.GetTranslatedStackType(stack), + client.GetTranslatedStackType(stack.Type), endpoint.Name, )) common.CheckError(err) diff --git a/cmd/stackList.go b/cmd/stackList.go index 2e8b735..d79d237 100644 --- a/cmd/stackList.go +++ b/cmd/stackList.go @@ -89,7 +89,7 @@ var stackListCmd = &cobra.Command{ "%v\t%s\t%v\t%s", s.ID, s.Name, - client.GetTranslatedStackType(s), + client.GetTranslatedStackType(s.Type), stackEndpoint.Name, )) common.CheckError(err) From 2cc0faa62508d807e261948bba8d7c8fa68dbf90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Carlos=20Mej=C3=ADas=20Rodr=C3=ADguez?= Date: Sat, 24 Aug 2019 00:39:43 -0400 Subject: [PATCH 08/34] Add unit tests for portainerClientImpl.do() --- client/client_test.go | 135 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) diff --git a/client/client_test.go b/client/client_test.go index 41576c6..ca7c980 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -3,6 +3,7 @@ package client import ( "encoding/json" "fmt" + "io" "io/ioutil" "net/http" "net/http/httptest" @@ -71,3 +72,137 @@ func TestClientAuthenticates(t *testing.T) { assert.Nil(t, err) assert.Equal(t, token, "somerandomtoken") } + +func Test_portainerClientImp_do(t *testing.T) { + type fields struct { + user string + password string + token string + userAgent string + doNotUseToken bool + beforeRequestHooks []func(req *http.Request) (err error) + afterResponseHooks []func(resp *http.Response) (err error) + server *httptest.Server + beforeFunctionCall func(t *testing.T, tt *fields) + } + type args struct { + uri string + method string + request io.Reader + requestType string + headers http.Header + } + tests := []struct { + name string + fields fields + args args + wantRespCheck func(resp *http.Response) bool + wantErr bool + }{ + { + name: "error on bad URI", + fields: fields{ + server: httptest.NewUnstartedServer(nil), + }, + args: args{ + uri: string(0x7f), + }, + wantErr: true, + }, + { + name: "error on bad method", + fields: fields{ + server: httptest.NewUnstartedServer(nil), + }, + args: args{ + method: "WOLOLO?", + }, + wantErr: true, + }, + { + name: "extra headers are added", + fields: fields{ + token: "somerandomtoken", + server: httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + assert.Equal(t, req.Header.Get("Some-Header"), "value") + })), + }, + args: args{ + headers: http.Header{ + "Some-Header": []string{ + "value", + }, + }, + }, + }, + { + name: "Authorization header is added when doNotUseToken is false", + fields: fields{ + token: "token", + server: httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + assert.NotEmpty(t, req.Header.Get("Authorization")) + })), + }, + }, + { + name: "Authorization header is not added when doNotUseToken is true", + fields: fields{ + doNotUseToken: true, + server: httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + assert.Equal(t, req.Header.Get("Authorization"), "") + })), + }, + }, + { + name: "returns error on http error", + fields: fields{ + token: "token", + server: httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {})), + beforeFunctionCall: func(t *testing.T, tt *fields) { + tt.server.Close() + }, + }, + wantErr: true, + }, + { + name: "returns error on response error", + fields: fields{ + token: "token", + server: httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + })), + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.fields.server.Start() + defer tt.fields.server.Close() + + apiURL, _ := url.Parse(tt.fields.server.URL + "/api/") + + n := &portainerClientImp{ + httpClient: tt.fields.server.Client(), + url: apiURL, + user: tt.fields.user, + password: tt.fields.password, + token: tt.fields.token, + userAgent: tt.fields.userAgent, + doNotUseToken: tt.fields.doNotUseToken, + beforeRequestHooks: tt.fields.beforeRequestHooks, + afterResponseHooks: tt.fields.afterResponseHooks, + } + + if tt.fields.beforeFunctionCall != nil { + tt.fields.beforeFunctionCall(t, &tt.fields) + } + gotResp, err := n.do(tt.args.uri, tt.args.method, tt.args.request, tt.args.requestType, tt.args.headers) + + assert.Equal(t, tt.wantErr, err != nil) + if tt.wantRespCheck != nil { + assert.True(t, tt.wantRespCheck(gotResp)) + } + }) + } +} From 14043a8bf8bfdb64da68a78457f938c95f42db16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Carlos=20Mej=C3=ADas=20Rodr=C3=ADguez?= Date: Sat, 24 Aug 2019 09:01:27 -0400 Subject: [PATCH 09/34] Refactor auth token setting in Portainer client --- client/client.go | 56 +++++++++++++++++++++---------------------- client/client_test.go | 22 ----------------- 2 files changed, 27 insertions(+), 51 deletions(-) diff --git a/client/client.go b/client/client.go index f0f81ff..1ee628d 100644 --- a/client/client.go +++ b/client/client.go @@ -78,7 +78,6 @@ type portainerClientImp struct { password string token string userAgent string - doNotUseToken bool beforeRequestHooks []func(req *http.Request) (err error) afterResponseHooks []func(resp *http.Response) (err error) } @@ -125,16 +124,6 @@ func (n *portainerClientImp) do(uri, method string, request io.Reader, requestTy req.Header.Set("User-Agent", n.userAgent) } - if !n.doNotUseToken { - if n.token == "" { - n.token, err = n.Authenticate() - if err != nil { - return - } - } - req.Header.Set("Authorization", "Bearer "+n.token) - } - // Run all "before request" hooks for i := 0; i < len(n.beforeRequestHooks); i++ { err = n.beforeRequestHooks[i](req) @@ -165,7 +154,7 @@ func (n *portainerClientImp) do(uri, method string, request io.Reader, requestTy } // Do a JSON http request -func (n *portainerClientImp) doJSON(uri, method string, request interface{}, response interface{}) error { +func (n *portainerClientImp) doJSON(uri, method string, headers http.Header, request interface{}, response interface{}) error { var body io.Reader if request != nil { @@ -176,7 +165,7 @@ func (n *portainerClientImp) doJSON(uri, method string, request interface{}, res body = bytes.NewReader(reqBodyBytes) } - resp, err := n.do(uri, method, body, "application/json", nil) + resp, err := n.do(uri, method, body, "application/json", headers) if err != nil { return err } @@ -192,6 +181,20 @@ func (n *portainerClientImp) doJSON(uri, method string, request interface{}, res return nil } +// Do a JSON http request with an auth token +func (n *portainerClientImp) doJSONWithToken(uri, method string, headers http.Header, request interface{}, response interface{}) (err error) { + // Ensure there is an auth token + if n.token == "" { + n.token, err = n.Authenticate() + if err != nil { + return + } + } + headers.Set("Authorization", "Bearer "+n.token) + + return n.doJSON(uri, method, headers, request, response) +} + func (n *portainerClientImp) BeforeRequest(hook func(req *http.Request) (err error)) { n.beforeRequestHooks = append(n.beforeRequestHooks, hook) } @@ -208,28 +211,23 @@ func (n *portainerClientImp) Authenticate() (token string, err error) { respBody := AuthenticateUserResponse{} - previousDoNotUseTokenValue := n.doNotUseToken - n.doNotUseToken = true - - err = n.doJSON("auth", http.MethodPost, &reqBody, &respBody) + err = n.doJSON("auth", http.MethodPost, http.Header{}, &reqBody, &respBody) if err != nil { return } - n.doNotUseToken = previousDoNotUseTokenValue - token = respBody.Jwt return } func (n *portainerClientImp) GetEndpoints() (endpoints []portainer.Endpoint, err error) { - err = n.doJSON("endpoints", http.MethodGet, nil, &endpoints) + err = n.doJSONWithToken("endpoints", http.MethodGet, http.Header{}, nil, &endpoints) return } func (n *portainerClientImp) GetEndpointGroups() (endpointGroups []portainer.EndpointGroup, err error) { - err = n.doJSON("endpoint_groups", http.MethodGet, nil, &endpointGroups) + err = n.doJSONWithToken("endpoint_groups", http.MethodGet, http.Header{}, nil, &endpointGroups) return } @@ -242,7 +240,7 @@ func (n *portainerClientImp) GetStacks(swarmID string, endpointID portainer.Endp filterJSONBytes, _ := json.Marshal(filter) filterJSONString := string(filterJSONBytes) - err = n.doJSON(fmt.Sprintf("stacks?filters=%s", filterJSONString), http.MethodGet, nil, &stacks) + err = n.doJSONWithToken(fmt.Sprintf("stacks?filters=%s", filterJSONString), http.MethodGet, http.Header{}, nil, &stacks) return } @@ -254,7 +252,7 @@ func (n *portainerClientImp) CreateSwarmStack(stackName string, environmentVaria StackFileContent: stackFileContent, } - err = n.doJSON(fmt.Sprintf("stacks?type=%v&method=%s&endpointId=%v", 1, "string", endpointID), http.MethodPost, &reqBody, &stack) + err = n.doJSONWithToken(fmt.Sprintf("stacks?type=%v&method=%s&endpointId=%v", 1, "string", endpointID), http.MethodPost, http.Header{}, &reqBody, &stack) return } @@ -265,7 +263,7 @@ func (n *portainerClientImp) CreateComposeStack(stackName string, environmentVar StackFileContent: stackFileContent, } - err = n.doJSON(fmt.Sprintf("stacks?type=%v&method=%s&endpointId=%v", 2, "string", endpointID), http.MethodPost, &reqBody, &stack) + err = n.doJSONWithToken(fmt.Sprintf("stacks?type=%v&method=%s&endpointId=%v", 2, "string", endpointID), http.MethodPost, http.Header{}, &reqBody, &stack) return } @@ -276,19 +274,19 @@ func (n *portainerClientImp) UpdateStack(stack portainer.Stack, environmentVaria Prune: prune, } - err = n.doJSON(fmt.Sprintf("stacks/%v?endpointId=%v", stack.ID, endpointID), http.MethodPut, &reqBody, nil) + err = n.doJSONWithToken(fmt.Sprintf("stacks/%v?endpointId=%v", stack.ID, endpointID), http.MethodPut, http.Header{}, &reqBody, nil) return } func (n *portainerClientImp) DeleteStack(stackID portainer.StackID) (err error) { - err = n.doJSON(fmt.Sprintf("stacks/%d", stackID), http.MethodDelete, nil, nil) + err = n.doJSONWithToken(fmt.Sprintf("stacks/%d", stackID), http.MethodDelete, http.Header{}, nil, nil) return } func (n *portainerClientImp) GetStackFileContent(stackID portainer.StackID) (content string, err error) { var respBody StackFileInspectResponse - err = n.doJSON(fmt.Sprintf("stacks/%v/file", stackID), http.MethodGet, nil, &respBody) + err = n.doJSONWithToken(fmt.Sprintf("stacks/%v/file", stackID), http.MethodGet, http.Header{}, nil, &respBody) if err != nil { return } @@ -299,12 +297,12 @@ func (n *portainerClientImp) GetStackFileContent(stackID portainer.StackID) (con } func (n *portainerClientImp) GetEndpointDockerInfo(endpointID portainer.EndpointID) (info map[string]interface{}, err error) { - err = n.doJSON(fmt.Sprintf("endpoints/%v/docker/info", endpointID), http.MethodGet, nil, &info) + err = n.doJSONWithToken(fmt.Sprintf("endpoints/%v/docker/info", endpointID), http.MethodGet, http.Header{}, nil, &info) return } func (n *portainerClientImp) GetStatus() (status portainer.Status, err error) { - err = n.doJSON("status", http.MethodGet, nil, &status) + err = n.doJSONWithToken("status", http.MethodGet, http.Header{}, nil, &status) return } diff --git a/client/client_test.go b/client/client_test.go index ca7c980..7ed88d0 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -79,7 +79,6 @@ func Test_portainerClientImp_do(t *testing.T) { password string token string userAgent string - doNotUseToken bool beforeRequestHooks []func(req *http.Request) (err error) afterResponseHooks []func(resp *http.Response) (err error) server *httptest.Server @@ -122,7 +121,6 @@ func Test_portainerClientImp_do(t *testing.T) { { name: "extra headers are added", fields: fields{ - token: "somerandomtoken", server: httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { assert.Equal(t, req.Header.Get("Some-Header"), "value") })), @@ -135,24 +133,6 @@ func Test_portainerClientImp_do(t *testing.T) { }, }, }, - { - name: "Authorization header is added when doNotUseToken is false", - fields: fields{ - token: "token", - server: httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - assert.NotEmpty(t, req.Header.Get("Authorization")) - })), - }, - }, - { - name: "Authorization header is not added when doNotUseToken is true", - fields: fields{ - doNotUseToken: true, - server: httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - assert.Equal(t, req.Header.Get("Authorization"), "") - })), - }, - }, { name: "returns error on http error", fields: fields{ @@ -167,7 +147,6 @@ func Test_portainerClientImp_do(t *testing.T) { { name: "returns error on response error", fields: fields{ - token: "token", server: httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { w.WriteHeader(http.StatusInternalServerError) })), @@ -189,7 +168,6 @@ func Test_portainerClientImp_do(t *testing.T) { password: tt.fields.password, token: tt.fields.token, userAgent: tt.fields.userAgent, - doNotUseToken: tt.fields.doNotUseToken, beforeRequestHooks: tt.fields.beforeRequestHooks, afterResponseHooks: tt.fields.afterResponseHooks, } From 19f58c4e1fef9813f6bed7ad2c13de74fc4a418b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Carlos=20Mej=C3=ADas=20Rodr=C3=ADguez?= Date: Sat, 24 Aug 2019 09:05:34 -0400 Subject: [PATCH 10/34] Move Content-Type header setting to doJSON() in Portainer client --- client/client.go | 7 ++++--- client/client_test.go | 11 +++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/client/client.go b/client/client.go index 1ee628d..36a458b 100644 --- a/client/client.go +++ b/client/client.go @@ -104,7 +104,7 @@ func checkResponseForErrors(resp *http.Response) error { } // Do an http request -func (n *portainerClientImp) do(uri, method string, request io.Reader, requestType string, headers http.Header) (resp *http.Response, err error) { +func (n *portainerClientImp) do(uri, method string, request io.Reader, headers http.Header) (resp *http.Response, err error) { requestURL, err := n.url.Parse(uri) if err != nil { return @@ -120,7 +120,6 @@ func (n *portainerClientImp) do(uri, method string, request io.Reader, requestTy } if request != nil { - req.Header.Set("Content-Type", requestType) req.Header.Set("User-Agent", n.userAgent) } @@ -165,7 +164,9 @@ func (n *portainerClientImp) doJSON(uri, method string, headers http.Header, req body = bytes.NewReader(reqBodyBytes) } - resp, err := n.do(uri, method, body, "application/json", headers) + headers.Set("Content-Type", "application/json") + + resp, err := n.do(uri, method, body, headers) if err != nil { return err } diff --git a/client/client_test.go b/client/client_test.go index 7ed88d0..e22294f 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -85,11 +85,10 @@ func Test_portainerClientImp_do(t *testing.T) { beforeFunctionCall func(t *testing.T, tt *fields) } type args struct { - uri string - method string - request io.Reader - requestType string - headers http.Header + uri string + method string + request io.Reader + headers http.Header } tests := []struct { name string @@ -175,7 +174,7 @@ func Test_portainerClientImp_do(t *testing.T) { if tt.fields.beforeFunctionCall != nil { tt.fields.beforeFunctionCall(t, &tt.fields) } - gotResp, err := n.do(tt.args.uri, tt.args.method, tt.args.request, tt.args.requestType, tt.args.headers) + gotResp, err := n.do(tt.args.uri, tt.args.method, tt.args.request, tt.args.headers) assert.Equal(t, tt.wantErr, err != nil) if tt.wantRespCheck != nil { From a8bed6a7cc4949e72ecb2800ef3a90e256cdceab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Carlos=20Mej=C3=ADas=20Rodr=C3=ADguez?= Date: Sat, 24 Aug 2019 15:06:16 -0400 Subject: [PATCH 11/34] Rename request and reponse to requestBody and responseBody --- client/client.go | 16 ++++++++-------- client/client_test.go | 10 +++++----- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/client/client.go b/client/client.go index 36a458b..9f43e6a 100644 --- a/client/client.go +++ b/client/client.go @@ -104,13 +104,13 @@ func checkResponseForErrors(resp *http.Response) error { } // Do an http request -func (n *portainerClientImp) do(uri, method string, request io.Reader, headers http.Header) (resp *http.Response, err error) { +func (n *portainerClientImp) do(uri, method string, requestBody io.Reader, 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) + req, err := http.NewRequest(method, requestURL.String(), requestBody) if err != nil { return } @@ -119,7 +119,7 @@ func (n *portainerClientImp) do(uri, method string, request io.Reader, headers h req.Header = headers } - if request != nil { + if requestBody != nil { req.Header.Set("User-Agent", n.userAgent) } @@ -153,11 +153,11 @@ func (n *portainerClientImp) do(uri, method string, request io.Reader, headers h } // Do a JSON http request -func (n *portainerClientImp) doJSON(uri, method string, headers http.Header, request interface{}, response interface{}) error { +func (n *portainerClientImp) doJSON(uri, method string, headers http.Header, requestBody interface{}, responseBody interface{}) error { var body io.Reader - if request != nil { - reqBodyBytes, err := json.Marshal(request) + if requestBody != nil { + reqBodyBytes, err := json.Marshal(requestBody) if err != nil { return err } @@ -171,9 +171,9 @@ func (n *portainerClientImp) doJSON(uri, method string, headers http.Header, req return err } - if response != nil { + if responseBody != nil { d := json.NewDecoder(resp.Body) - err := d.Decode(response) + err := d.Decode(responseBody) if err != nil { return err } diff --git a/client/client_test.go b/client/client_test.go index e22294f..65dd558 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -85,10 +85,10 @@ func Test_portainerClientImp_do(t *testing.T) { beforeFunctionCall func(t *testing.T, tt *fields) } type args struct { - uri string - method string - request io.Reader - headers http.Header + uri string + method string + requestBody io.Reader + headers http.Header } tests := []struct { name string @@ -174,7 +174,7 @@ func Test_portainerClientImp_do(t *testing.T) { if tt.fields.beforeFunctionCall != nil { tt.fields.beforeFunctionCall(t, &tt.fields) } - gotResp, err := n.do(tt.args.uri, tt.args.method, tt.args.request, tt.args.headers) + gotResp, err := n.do(tt.args.uri, tt.args.method, tt.args.requestBody, tt.args.headers) assert.Equal(t, tt.wantErr, err != nil) if tt.wantRespCheck != nil { From 7fb7710c55a0ea164f022f5524f08e777a929c80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Carlos=20Mej=C3=ADas=20Rodr=C3=ADguez?= Date: Sat, 24 Aug 2019 15:39:56 -0400 Subject: [PATCH 12/34] Remove condition of requestBody being not nil to set user agent --- client/client.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/client/client.go b/client/client.go index 9f43e6a..b89c9c7 100644 --- a/client/client.go +++ b/client/client.go @@ -119,9 +119,8 @@ func (n *portainerClientImp) do(uri, method string, requestBody io.Reader, heade req.Header = headers } - if requestBody != nil { - req.Header.Set("User-Agent", n.userAgent) - } + // Set user agent header + req.Header.Set("User-Agent", n.userAgent) // Run all "before request" hooks for i := 0; i < len(n.beforeRequestHooks); i++ { From 1c3a1c665ebc117b6eceef6d91fb39df95bd0e11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Carlos=20Mej=C3=ADas=20Rodr=C3=ADguez?= Date: Sat, 24 Aug 2019 16:45:20 -0400 Subject: [PATCH 13/34] Add unit tests for checkResponseForErrors() --- client/client_test.go | 48 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/client/client_test.go b/client/client_test.go index 65dd558..b932259 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -1,6 +1,7 @@ package client import ( + "bytes" "encoding/json" "fmt" "io" @@ -183,3 +184,50 @@ func Test_portainerClientImp_do(t *testing.T) { }) } } + +func Test_checkResponseForErrors(t *testing.T) { + type args struct { + resp *http.Response + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "generic error", + args: args{ + resp: func() (resp *http.Response) { + resp = &http.Response{ + StatusCode: http.StatusNotFound, + } + bodyBytes, _ := json.Marshal(map[string]interface{}{ + "Err": "Error", + "Details": "Not found", + }) + resp.Body = ioutil.NopCloser(bytes.NewReader(bodyBytes)) + return + }(), + }, + wantErr: true, + }, + { + name: "non generic error", + args: args{ + resp: func() (resp *http.Response) { + resp = &http.Response{ + StatusCode: http.StatusNotFound, + Body: ioutil.NopCloser(bytes.NewReader([]byte("Err"))), + } + return + }(), + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.wantErr, checkResponseForErrors(tt.args.resp) != nil) + }) + } +} From 53b13b1c76327f55be2063f6e30a2b792711d59e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Carlos=20Mej=C3=ADas=20Rodr=C3=ADguez?= Date: Mon, 26 Aug 2019 01:13:43 -0400 Subject: [PATCH 14/34] Rename PortainerClient.Authenticate() to Auth() --- client/client.go | 8 ++++---- client/client_test.go | 2 +- cmd/login.go | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/client/client.go b/client/client.go index b89c9c7..c75bfe4 100644 --- a/client/client.go +++ b/client/client.go @@ -31,8 +31,8 @@ type Config struct { // PortainerClient represents a Portainer API client type PortainerClient interface { - // Authenticate a user to get an auth token - Authenticate() (token string, err error) + // Auth a user to get an auth token + Auth() (token string, err error) // Get endpoints GetEndpoints() ([]portainer.Endpoint, error) @@ -185,7 +185,7 @@ func (n *portainerClientImp) doJSON(uri, method string, headers http.Header, req func (n *portainerClientImp) doJSONWithToken(uri, method string, headers http.Header, request interface{}, response interface{}) (err error) { // Ensure there is an auth token if n.token == "" { - n.token, err = n.Authenticate() + n.token, err = n.Auth() if err != nil { return } @@ -203,7 +203,7 @@ func (n *portainerClientImp) AfterResponse(hook func(resp *http.Response) (err e n.afterResponseHooks = append(n.afterResponseHooks, hook) } -func (n *portainerClientImp) Authenticate() (token string, err error) { +func (n *portainerClientImp) Auth() (token string, err error) { reqBody := AuthenticateUserRequest{ Username: n.user, Password: n.password, diff --git a/client/client_test.go b/client/client_test.go index b932259..88726c2 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -69,7 +69,7 @@ func TestClientAuthenticates(t *testing.T) { Password: "a", UserAgent: "GE007", }) - token, err := customClient.Authenticate() + token, err := customClient.Auth() assert.Nil(t, err) assert.Equal(t, token, "somerandomtoken") } diff --git a/cmd/login.go b/cmd/login.go index f0fad9b..a3047dc 100644 --- a/cmd/login.go +++ b/cmd/login.go @@ -22,7 +22,7 @@ var loginCmd = &cobra.Command{ logrus.WithFields(logrus.Fields{ "user": user, }).Debug("Getting auth token") - authToken, err := client.Authenticate() + authToken, err := client.Auth() common.CheckError(err) if viper.GetBool("login.print") { From 0adb18bc8ae908286d320d37af2fe01dbcd92e04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Carlos=20Mej=C3=ADas=20Rodr=C3=ADguez?= Date: Mon, 26 Aug 2019 01:15:44 -0400 Subject: [PATCH 15/34] Rename PortainerClient.GetEndpoints() to EndpointList() --- client/client.go | 4 ++-- cmd/endpointList.go | 2 +- cmd/stackList.go | 2 +- common/utils.go | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/client/client.go b/client/client.go index c75bfe4..1027ca9 100644 --- a/client/client.go +++ b/client/client.go @@ -35,7 +35,7 @@ type PortainerClient interface { Auth() (token string, err error) // Get endpoints - GetEndpoints() ([]portainer.Endpoint, error) + EndpointList() ([]portainer.Endpoint, error) // Get endpoint groups GetEndpointGroups() ([]portainer.EndpointGroup, error) @@ -221,7 +221,7 @@ func (n *portainerClientImp) Auth() (token string, err error) { return } -func (n *portainerClientImp) GetEndpoints() (endpoints []portainer.Endpoint, err error) { +func (n *portainerClientImp) EndpointList() (endpoints []portainer.Endpoint, err error) { err = n.doJSONWithToken("endpoints", http.MethodGet, http.Header{}, nil, &endpoints) return } diff --git a/cmd/endpointList.go b/cmd/endpointList.go index df6f27f..9ae8720 100644 --- a/cmd/endpointList.go +++ b/cmd/endpointList.go @@ -30,7 +30,7 @@ var endpointListCmd = &cobra.Command{ common.CheckError(err) logrus.Debug("Getting endpoints") - endpoints, err := client.GetEndpoints() + endpoints, err := client.EndpointList() common.CheckError(err) switch viper.GetString("endpoint.list.format") { diff --git a/cmd/stackList.go b/cmd/stackList.go index d79d237..172b694 100644 --- a/cmd/stackList.go +++ b/cmd/stackList.go @@ -33,7 +33,7 @@ var stackListCmd = &cobra.Command{ portainerClient, err := common.GetClient() common.CheckError(err) - endpoints, endpointsRetrievalErr := portainerClient.GetEndpoints() + endpoints, endpointsRetrievalErr := portainerClient.EndpointList() common.CheckError(endpointsRetrievalErr) var endpointSwarmClusterID string diff --git a/common/utils.go b/common/utils.go index f9ce174..b733869 100644 --- a/common/utils.go +++ b/common/utils.go @@ -38,7 +38,7 @@ func GetDefaultEndpoint() (endpoint portainer.Endpoint, err error) { } logrus.Debug("Getting endpoints") - endpoints, err := portainerClient.GetEndpoints() + endpoints, err := portainerClient.EndpointList() if err != nil { return } @@ -85,7 +85,7 @@ func GetEndpointByName(name string) (endpoint portainer.Endpoint, err error) { return } - endpoints, err := portainerClient.GetEndpoints() + endpoints, err := portainerClient.EndpointList() if err != nil { return } From 623788b6243343e79b2c8946481bb3afa2405a11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Carlos=20Mej=C3=ADas=20Rodr=C3=ADguez?= Date: Mon, 26 Aug 2019 01:16:40 -0400 Subject: [PATCH 16/34] Rename PortainerClient.GetEndpointGroups() to EndpointGroupList() --- client/client.go | 4 ++-- cmd/endpointGroupList.go | 2 +- common/utils.go | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/client/client.go b/client/client.go index 1027ca9..746a92f 100644 --- a/client/client.go +++ b/client/client.go @@ -38,7 +38,7 @@ type PortainerClient interface { EndpointList() ([]portainer.Endpoint, error) // Get endpoint groups - GetEndpointGroups() ([]portainer.EndpointGroup, error) + EndpointGroupList() ([]portainer.EndpointGroup, error) // Get stacks, optionally filtered by swarmId and endpointId GetStacks(swarmID string, endpointID portainer.EndpointID) ([]portainer.Stack, error) @@ -226,7 +226,7 @@ func (n *portainerClientImp) EndpointList() (endpoints []portainer.Endpoint, err return } -func (n *portainerClientImp) GetEndpointGroups() (endpointGroups []portainer.EndpointGroup, err error) { +func (n *portainerClientImp) EndpointGroupList() (endpointGroups []portainer.EndpointGroup, err error) { err = n.doJSONWithToken("endpoint_groups", http.MethodGet, http.Header{}, nil, &endpointGroups) return } diff --git a/cmd/endpointGroupList.go b/cmd/endpointGroupList.go index b3f094d..953c90e 100644 --- a/cmd/endpointGroupList.go +++ b/cmd/endpointGroupList.go @@ -30,7 +30,7 @@ var endpointGroupListCmd = &cobra.Command{ common.CheckError(err) logrus.Debug("Getting endpoint groups") - endpointGroups, err := client.GetEndpointGroups() + endpointGroups, err := client.EndpointGroupList() common.CheckError(err) switch viper.GetString("endpoint.group.list.format") { diff --git a/common/utils.go b/common/utils.go index b733869..81f272a 100644 --- a/common/utils.go +++ b/common/utils.go @@ -107,7 +107,7 @@ func GetEndpointGroupByName(name string) (endpointGroup portainer.EndpointGroup, return } - endpointGroups, err := portainerClient.GetEndpointGroups() + endpointGroups, err := portainerClient.EndpointGroupList() if err != nil { return } From 87ba553153766fc7b76aa16659f12f380e5e01f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Carlos=20Mej=C3=ADas=20Rodr=C3=ADguez?= Date: Mon, 26 Aug 2019 01:17:41 -0400 Subject: [PATCH 17/34] Rename PortainerClient.GetStacks() to StackList() --- client/client.go | 4 ++-- cmd/stackList.go | 6 +++--- common/utils.go | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/client/client.go b/client/client.go index 746a92f..03e9ed3 100644 --- a/client/client.go +++ b/client/client.go @@ -41,7 +41,7 @@ type PortainerClient interface { EndpointGroupList() ([]portainer.EndpointGroup, error) // Get stacks, optionally filtered by swarmId and endpointId - GetStacks(swarmID string, endpointID portainer.EndpointID) ([]portainer.Stack, error) + StackList(swarmID string, endpointID portainer.EndpointID) ([]portainer.Stack, error) // Create swarm stack CreateSwarmStack(stackName string, environmentVariables []portainer.Pair, stackFileContent string, swarmClusterID string, endpointID portainer.EndpointID) (stack portainer.Stack, err error) @@ -231,7 +231,7 @@ func (n *portainerClientImp) EndpointGroupList() (endpointGroups []portainer.End return } -func (n *portainerClientImp) GetStacks(swarmID string, endpointID portainer.EndpointID) (stacks []portainer.Stack, err error) { +func (n *portainerClientImp) StackList(swarmID string, endpointID portainer.EndpointID) (stacks []portainer.Stack, err error) { filter := StackListFilter{ SwarmID: swarmID, EndpointID: endpointID, diff --git a/cmd/stackList.go b/cmd/stackList.go index 172b694..d073427 100644 --- a/cmd/stackList.go +++ b/cmd/stackList.go @@ -53,14 +53,14 @@ var stackListCmd = &cobra.Command{ logrus.WithFields(logrus.Fields{ "endpoint": endpoint.Name, }).Debug("Getting stacks") - stacks, err = portainerClient.GetStacks(endpointSwarmClusterID, endpoint.ID) + stacks, err = portainerClient.StackList(endpointSwarmClusterID, endpoint.ID) common.CheckError(err) } else if selectionErr == common.ErrStackClusterNotFound { // It's not a swarm cluster logrus.WithFields(logrus.Fields{ "endpoint": endpoint.Name, }).Debug("Getting stacks") - stacks, err = portainerClient.GetStacks("", endpoint.ID) + stacks, err = portainerClient.StackList("", endpoint.ID) common.CheckError(err) } else { // Something else happened @@ -68,7 +68,7 @@ var stackListCmd = &cobra.Command{ } } else { logrus.Debug("Getting stacks") - stacks, err = portainerClient.GetStacks("", 0) + stacks, err = portainerClient.StackList("", 0) common.CheckError(err) } diff --git a/common/utils.go b/common/utils.go index 81f272a..0c1e026 100644 --- a/common/utils.go +++ b/common/utils.go @@ -63,7 +63,7 @@ func GetStackByName(name string, swarmID string, endpointID portainer.EndpointID return } - stacks, err := portainerClient.GetStacks(swarmID, endpointID) + stacks, err := portainerClient.StackList(swarmID, endpointID) if err != nil { return } From 00b7daae733dc260d19a899d7482b0b82b0642f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Carlos=20Mej=C3=ADas=20Rodr=C3=ADguez?= Date: Mon, 26 Aug 2019 01:18:46 -0400 Subject: [PATCH 18/34] Rename PortainerClient.CreateSwarmStack() to StackCreateSwarm() --- client/client.go | 4 ++-- cmd/stackDeploy.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/client.go b/client/client.go index 03e9ed3..6f4238f 100644 --- a/client/client.go +++ b/client/client.go @@ -44,7 +44,7 @@ type PortainerClient interface { StackList(swarmID string, endpointID portainer.EndpointID) ([]portainer.Stack, error) // Create swarm stack - CreateSwarmStack(stackName string, environmentVariables []portainer.Pair, stackFileContent string, swarmClusterID string, endpointID portainer.EndpointID) (stack portainer.Stack, err error) + StackCreateSwarm(stackName string, environmentVariables []portainer.Pair, stackFileContent string, swarmClusterID string, endpointID portainer.EndpointID) (stack portainer.Stack, err error) // Create compose stack CreateComposeStack(stackName string, environmentVariables []portainer.Pair, stackFileContent string, endpointID portainer.EndpointID) (stack portainer.Stack, err error) @@ -244,7 +244,7 @@ func (n *portainerClientImp) StackList(swarmID string, endpointID portainer.Endp return } -func (n *portainerClientImp) CreateSwarmStack(stackName string, environmentVariables []portainer.Pair, stackFileContent string, swarmClusterID string, endpointID portainer.EndpointID) (stack portainer.Stack, err error) { +func (n *portainerClientImp) StackCreateSwarm(stackName string, environmentVariables []portainer.Pair, stackFileContent string, swarmClusterID string, endpointID portainer.EndpointID) (stack portainer.Stack, err error) { reqBody := StackCreateRequest{ Name: stackName, Env: environmentVariables, diff --git a/cmd/stackDeploy.go b/cmd/stackDeploy.go index b8059cd..21dfe5f 100644 --- a/cmd/stackDeploy.go +++ b/cmd/stackDeploy.go @@ -135,7 +135,7 @@ var stackDeployCmd = &cobra.Command{ "stack": stackName, "endpoint": endpoint.Name, }).Info("Creating stack") - stack, deploymentErr := portainerClient.CreateSwarmStack(stackName, loadedEnvironmentVariables, stackFileContent, endpointSwarmClusterID, endpoint.ID) + stack, deploymentErr := portainerClient.StackCreateSwarm(stackName, loadedEnvironmentVariables, stackFileContent, endpointSwarmClusterID, endpoint.ID) common.CheckError(deploymentErr) logrus.WithFields(logrus.Fields{ "stack": stack.Name, From ea63f26ee0f4cfc29aab3cea31e771d87c360520 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Carlos=20Mej=C3=ADas=20Rodr=C3=ADguez?= Date: Mon, 26 Aug 2019 01:19:41 -0400 Subject: [PATCH 19/34] Rename PortainerClient.UpdateStack() to StackUpdate() --- client/client.go | 8 ++++---- cmd/stackDeploy.go | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/client/client.go b/client/client.go index 6f4238f..d09129b 100644 --- a/client/client.go +++ b/client/client.go @@ -47,10 +47,10 @@ type PortainerClient interface { StackCreateSwarm(stackName string, environmentVariables []portainer.Pair, stackFileContent string, swarmClusterID string, endpointID portainer.EndpointID) (stack portainer.Stack, err error) // Create compose stack - CreateComposeStack(stackName string, environmentVariables []portainer.Pair, stackFileContent string, endpointID portainer.EndpointID) (stack portainer.Stack, err error) + StackCreateCompose(stackName string, environmentVariables []portainer.Pair, stackFileContent string, endpointID portainer.EndpointID) (stack portainer.Stack, err error) // Update stack - UpdateStack(stack portainer.Stack, environmentVariables []portainer.Pair, stackFileContent string, prune bool, endpointID portainer.EndpointID) error + StackUpdate(stack portainer.Stack, environmentVariables []portainer.Pair, stackFileContent string, prune bool, endpointID portainer.EndpointID) error // Delete stack DeleteStack(stackID portainer.StackID) error @@ -256,7 +256,7 @@ func (n *portainerClientImp) StackCreateSwarm(stackName string, environmentVaria return } -func (n *portainerClientImp) CreateComposeStack(stackName string, environmentVariables []portainer.Pair, stackFileContent string, endpointID portainer.EndpointID) (stack portainer.Stack, err error) { +func (n *portainerClientImp) StackCreateCompose(stackName string, environmentVariables []portainer.Pair, stackFileContent string, endpointID portainer.EndpointID) (stack portainer.Stack, err error) { reqBody := StackCreateRequest{ Name: stackName, Env: environmentVariables, @@ -267,7 +267,7 @@ func (n *portainerClientImp) CreateComposeStack(stackName string, environmentVar return } -func (n *portainerClientImp) UpdateStack(stack portainer.Stack, environmentVariables []portainer.Pair, stackFileContent string, prune bool, endpointID portainer.EndpointID) (err error) { +func (n *portainerClientImp) StackUpdate(stack portainer.Stack, environmentVariables []portainer.Pair, stackFileContent string, prune bool, endpointID portainer.EndpointID) (err error) { reqBody := StackUpdateRequest{ Env: environmentVariables, StackFileContent: stackFileContent, diff --git a/cmd/stackDeploy.go b/cmd/stackDeploy.go index 21dfe5f..44376c6 100644 --- a/cmd/stackDeploy.go +++ b/cmd/stackDeploy.go @@ -115,7 +115,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"), endpoint.ID) + err := portainerClient.StackUpdate(retrievedStack, newEnvironmentVariables, stackFileContent, viper.GetBool("stack.deploy.prune"), endpoint.ID) common.CheckError(err) } else if stackRetrievalErr == common.ErrStackNotFound { // We are deploying a new stack @@ -148,7 +148,7 @@ var stackDeployCmd = &cobra.Command{ "stack": stackName, "endpoint": endpoint.Name, }).Info("Creating stack") - stack, deploymentErr := portainerClient.CreateComposeStack(stackName, loadedEnvironmentVariables, stackFileContent, endpoint.ID) + stack, deploymentErr := portainerClient.StackCreateCompose(stackName, loadedEnvironmentVariables, stackFileContent, endpoint.ID) common.CheckError(deploymentErr) logrus.WithFields(logrus.Fields{ "stack": stack.Name, From 0b946ac0ec8b633f360986eefffec41651a1ff30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Carlos=20Mej=C3=ADas=20Rodr=C3=ADguez?= Date: Mon, 26 Aug 2019 01:21:14 -0400 Subject: [PATCH 20/34] Rename PortainerClient.DeleteStack() to StackDelete() --- client/client.go | 4 ++-- cmd/stackRemove.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/client.go b/client/client.go index d09129b..86620a1 100644 --- a/client/client.go +++ b/client/client.go @@ -53,7 +53,7 @@ type PortainerClient interface { StackUpdate(stack portainer.Stack, environmentVariables []portainer.Pair, stackFileContent string, prune bool, endpointID portainer.EndpointID) error // Delete stack - DeleteStack(stackID portainer.StackID) error + StackDelete(stackID portainer.StackID) error // Get stack file content GetStackFileContent(stackID portainer.StackID) (content string, err error) @@ -278,7 +278,7 @@ func (n *portainerClientImp) StackUpdate(stack portainer.Stack, environmentVaria return } -func (n *portainerClientImp) DeleteStack(stackID portainer.StackID) (err error) { +func (n *portainerClientImp) StackDelete(stackID portainer.StackID) (err error) { err = n.doJSONWithToken(fmt.Sprintf("stacks/%d", stackID), http.MethodDelete, http.Header{}, nil, nil) return } diff --git a/cmd/stackRemove.go b/cmd/stackRemove.go index 0e147d5..72d9f63 100644 --- a/cmd/stackRemove.go +++ b/cmd/stackRemove.go @@ -77,7 +77,7 @@ var stackRemoveCmd = &cobra.Command{ "stack": stackName, "endpoint": endpoint.Name, }).Info("Removing stack") - err := portainerClient.DeleteStack(stackID) + err := portainerClient.StackDelete(stackID) common.CheckError(err) logrus.WithFields(logrus.Fields{ "stack": stack.Name, From 636757e28906f98db0ea0cf95a1ce4e0a5483e2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Carlos=20Mej=C3=ADas=20Rodr=C3=ADguez?= Date: Mon, 26 Aug 2019 01:24:53 -0400 Subject: [PATCH 21/34] Rename PortainerClient.GetStackFileContent() to StackFileInspect() --- client/client.go | 4 ++-- cmd/stackDeploy.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/client.go b/client/client.go index 86620a1..a91f2e5 100644 --- a/client/client.go +++ b/client/client.go @@ -56,7 +56,7 @@ type PortainerClient interface { StackDelete(stackID portainer.StackID) error // Get stack file content - GetStackFileContent(stackID portainer.StackID) (content string, err error) + StackFileInspect(stackID portainer.StackID) (content string, err error) // Get endpoint Docker info GetEndpointDockerInfo(endpointID portainer.EndpointID) (info map[string]interface{}, err error) @@ -283,7 +283,7 @@ func (n *portainerClientImp) StackDelete(stackID portainer.StackID) (err error) return } -func (n *portainerClientImp) GetStackFileContent(stackID portainer.StackID) (content string, err error) { +func (n *portainerClientImp) StackFileInspect(stackID portainer.StackID) (content string, err error) { var respBody StackFileInspectResponse err = n.doJSONWithToken(fmt.Sprintf("stacks/%v/file", stackID), http.MethodGet, http.Header{}, nil, &respBody) diff --git a/cmd/stackDeploy.go b/cmd/stackDeploy.go index 44376c6..0f5dc18 100644 --- a/cmd/stackDeploy.go +++ b/cmd/stackDeploy.go @@ -87,7 +87,7 @@ var stackDeployCmd = &cobra.Command{ logrus.WithFields(logrus.Fields{ "stack": retrievedStack.Name, }).Debug("Getting stack file content") - stackFileContent, stackFileContentRetrievalErr = portainerClient.GetStackFileContent(retrievedStack.ID) + stackFileContent, stackFileContentRetrievalErr = portainerClient.StackFileInspect(retrievedStack.ID) common.CheckError(stackFileContentRetrievalErr) } From aa606782afa55b5808388a1cd61749e5411f0ef1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Carlos=20Mej=C3=ADas=20Rodr=C3=ADguez?= Date: Mon, 26 Aug 2019 01:25:54 -0400 Subject: [PATCH 22/34] Rename PortainerClient.GetEndpointDockerInfo() to EndpointDockerInfo() --- client/client.go | 4 ++-- common/utils.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/client.go b/client/client.go index a91f2e5..45b5cc2 100644 --- a/client/client.go +++ b/client/client.go @@ -59,7 +59,7 @@ type PortainerClient interface { StackFileInspect(stackID portainer.StackID) (content string, err error) // Get endpoint Docker info - GetEndpointDockerInfo(endpointID portainer.EndpointID) (info map[string]interface{}, err error) + EndpointDockerInfo(endpointID portainer.EndpointID) (info map[string]interface{}, err error) // Get Portainer status info GetStatus() (portainer.Status, error) @@ -296,7 +296,7 @@ func (n *portainerClientImp) StackFileInspect(stackID portainer.StackID) (conten return } -func (n *portainerClientImp) GetEndpointDockerInfo(endpointID portainer.EndpointID) (info map[string]interface{}, err error) { +func (n *portainerClientImp) EndpointDockerInfo(endpointID portainer.EndpointID) (info map[string]interface{}, err error) { err = n.doJSONWithToken(fmt.Sprintf("endpoints/%v/docker/info", endpointID), http.MethodGet, http.Header{}, nil, &info) return } diff --git a/common/utils.go b/common/utils.go index 0c1e026..28a1d09 100644 --- a/common/utils.go +++ b/common/utils.go @@ -151,7 +151,7 @@ func GetEndpointSwarmClusterID(endpointID portainer.EndpointID) (endpointSwarmCl return } - result, err := portainerClient.GetEndpointDockerInfo(endpointID) + result, err := portainerClient.EndpointDockerInfo(endpointID) if err != nil { return } From f38481b0083a985153e9edab4cc0f39ce0975308 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Carlos=20Mej=C3=ADas=20Rodr=C3=ADguez?= Date: Mon, 26 Aug 2019 01:29:21 -0400 Subject: [PATCH 23/34] Rename PortainerClient.GetStatus() to Status() --- client/client.go | 4 ++-- cmd/status.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/client.go b/client/client.go index 45b5cc2..0566d4c 100644 --- a/client/client.go +++ b/client/client.go @@ -62,7 +62,7 @@ type PortainerClient interface { EndpointDockerInfo(endpointID portainer.EndpointID) (info map[string]interface{}, err error) // Get Portainer status info - GetStatus() (portainer.Status, error) + Status() (portainer.Status, error) // Run a function before sending a request to Portainer BeforeRequest(hook func(req *http.Request) (err error)) @@ -301,7 +301,7 @@ func (n *portainerClientImp) EndpointDockerInfo(endpointID portainer.EndpointID) return } -func (n *portainerClientImp) GetStatus() (status portainer.Status, err error) { +func (n *portainerClientImp) Status() (status portainer.Status, err error) { err = n.doJSONWithToken("status", http.MethodGet, http.Header{}, nil, &status) return } diff --git a/cmd/status.go b/cmd/status.go index 7602a8c..abe67dc 100644 --- a/cmd/status.go +++ b/cmd/status.go @@ -26,7 +26,7 @@ var statusCmd = &cobra.Command{ client, err := common.GetClient() common.CheckError(err) - respBody, err := client.GetStatus() + respBody, err := client.Status() common.CheckError(err) switch viper.GetString("status.format") { From 26a3efdd3a333a8f092a8d7779622ff76195613f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Carlos=20Mej=C3=ADas=20Rodr=C3=ADguez?= Date: Mon, 26 Aug 2019 01:40:15 -0400 Subject: [PATCH 24/34] Add options object for PortainerClient.StackList() --- client/client.go | 16 ++++++++-------- cmd/stackList.go | 15 ++++++++++++--- common/utils.go | 9 ++++++++- 3 files changed, 28 insertions(+), 12 deletions(-) diff --git a/client/client.go b/client/client.go index 0566d4c..c16a8ff 100644 --- a/client/client.go +++ b/client/client.go @@ -19,6 +19,11 @@ type StackListFilter struct { EndpointID portainer.EndpointID `json:"EndpointId,omitempty"` } +// StackListOptions represents options passed to PortainerClient.StackList() +type StackListOptions struct { + Filter StackListFilter +} + // Config represents a Portainer client configuration type Config struct { URL *url.URL @@ -41,7 +46,7 @@ type PortainerClient interface { EndpointGroupList() ([]portainer.EndpointGroup, error) // Get stacks, optionally filtered by swarmId and endpointId - StackList(swarmID string, endpointID portainer.EndpointID) ([]portainer.Stack, error) + StackList(options StackListOptions) ([]portainer.Stack, error) // Create swarm stack StackCreateSwarm(stackName string, environmentVariables []portainer.Pair, stackFileContent string, swarmClusterID string, endpointID portainer.EndpointID) (stack portainer.Stack, err error) @@ -231,13 +236,8 @@ func (n *portainerClientImp) EndpointGroupList() (endpointGroups []portainer.End return } -func (n *portainerClientImp) StackList(swarmID string, endpointID portainer.EndpointID) (stacks []portainer.Stack, err error) { - filter := StackListFilter{ - SwarmID: swarmID, - EndpointID: endpointID, - } - - filterJSONBytes, _ := json.Marshal(filter) +func (n *portainerClientImp) StackList(options StackListOptions) (stacks []portainer.Stack, err error) { + filterJSONBytes, _ := json.Marshal(options.Filter) filterJSONString := string(filterJSONBytes) err = n.doJSONWithToken(fmt.Sprintf("stacks?filters=%s", filterJSONString), http.MethodGet, http.Header{}, nil, &stacks) diff --git a/cmd/stackList.go b/cmd/stackList.go index d073427..88cdfa8 100644 --- a/cmd/stackList.go +++ b/cmd/stackList.go @@ -53,14 +53,23 @@ var stackListCmd = &cobra.Command{ logrus.WithFields(logrus.Fields{ "endpoint": endpoint.Name, }).Debug("Getting stacks") - stacks, err = portainerClient.StackList(endpointSwarmClusterID, endpoint.ID) + stacks, err = portainerClient.StackList(client.StackListOptions{ + Filter: client.StackListFilter{ + SwarmID: endpointSwarmClusterID, + EndpointID: endpoint.ID, + }, + }) common.CheckError(err) } else if selectionErr == common.ErrStackClusterNotFound { // It's not a swarm cluster logrus.WithFields(logrus.Fields{ "endpoint": endpoint.Name, }).Debug("Getting stacks") - stacks, err = portainerClient.StackList("", endpoint.ID) + stacks, err = portainerClient.StackList(client.StackListOptions{ + Filter: client.StackListFilter{ + EndpointID: endpoint.ID, + }, + }) common.CheckError(err) } else { // Something else happened @@ -68,7 +77,7 @@ var stackListCmd = &cobra.Command{ } } else { logrus.Debug("Getting stacks") - stacks, err = portainerClient.StackList("", 0) + stacks, err = portainerClient.StackList(client.StackListOptions{}) common.CheckError(err) } diff --git a/common/utils.go b/common/utils.go index 28a1d09..1b34702 100644 --- a/common/utils.go +++ b/common/utils.go @@ -4,6 +4,8 @@ import ( "fmt" "reflect" + "github.com/greenled/portainer-stack-utils/client" + portainer "github.com/portainer/portainer/api" "github.com/sirupsen/logrus" ) @@ -63,7 +65,12 @@ func GetStackByName(name string, swarmID string, endpointID portainer.EndpointID return } - stacks, err := portainerClient.StackList(swarmID, endpointID) + stacks, err := portainerClient.StackList(client.StackListOptions{ + Filter: client.StackListFilter{ + SwarmID: swarmID, + EndpointID: endpointID, + }, + }) if err != nil { return } From 768410ce20a7470f1cca6ce30a511aa90650fe89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Carlos=20Mej=C3=ADas=20Rodr=C3=ADguez?= Date: Mon, 26 Aug 2019 01:46:28 -0400 Subject: [PATCH 25/34] Add options object for PortainerClient.StackCreateSwarm() --- client/client.go | 23 ++++++++++++++++------- cmd/stackDeploy.go | 10 +++++++++- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/client/client.go b/client/client.go index c16a8ff..fdf975a 100644 --- a/client/client.go +++ b/client/client.go @@ -24,6 +24,15 @@ type StackListOptions struct { Filter StackListFilter } +// StackCreateSwarmOptions represents options passed to PortainerClient.StackCreateSwarm() +type StackCreateSwarmOptions struct { + StackName string + EnvironmentVariables []portainer.Pair + StackFileContent string + SwarmClusterID string + EndpointID portainer.EndpointID +} + // Config represents a Portainer client configuration type Config struct { URL *url.URL @@ -49,7 +58,7 @@ type PortainerClient interface { StackList(options StackListOptions) ([]portainer.Stack, error) // Create swarm stack - StackCreateSwarm(stackName string, environmentVariables []portainer.Pair, stackFileContent string, swarmClusterID string, endpointID portainer.EndpointID) (stack portainer.Stack, err error) + StackCreateSwarm(options StackCreateSwarmOptions) (stack portainer.Stack, err error) // Create compose stack StackCreateCompose(stackName string, environmentVariables []portainer.Pair, stackFileContent string, endpointID portainer.EndpointID) (stack portainer.Stack, err error) @@ -244,15 +253,15 @@ func (n *portainerClientImp) StackList(options StackListOptions) (stacks []porta return } -func (n *portainerClientImp) StackCreateSwarm(stackName string, environmentVariables []portainer.Pair, stackFileContent string, swarmClusterID string, endpointID portainer.EndpointID) (stack portainer.Stack, err error) { +func (n *portainerClientImp) StackCreateSwarm(options StackCreateSwarmOptions) (stack portainer.Stack, err error) { reqBody := StackCreateRequest{ - Name: stackName, - Env: environmentVariables, - SwarmID: swarmClusterID, - StackFileContent: stackFileContent, + Name: options.StackName, + Env: options.EnvironmentVariables, + SwarmID: options.SwarmClusterID, + StackFileContent: options.StackFileContent, } - err = n.doJSONWithToken(fmt.Sprintf("stacks?type=%v&method=%s&endpointId=%v", 1, "string", endpointID), http.MethodPost, http.Header{}, &reqBody, &stack) + err = n.doJSONWithToken(fmt.Sprintf("stacks?type=%v&method=%s&endpointId=%v", 1, "string", options.EndpointID), http.MethodPost, http.Header{}, &reqBody, &stack) return } diff --git a/cmd/stackDeploy.go b/cmd/stackDeploy.go index 0f5dc18..4b0b0c5 100644 --- a/cmd/stackDeploy.go +++ b/cmd/stackDeploy.go @@ -3,6 +3,8 @@ package cmd import ( "io/ioutil" + "github.com/greenled/portainer-stack-utils/client" + portainer "github.com/portainer/portainer/api" "github.com/sirupsen/logrus" @@ -135,7 +137,13 @@ var stackDeployCmd = &cobra.Command{ "stack": stackName, "endpoint": endpoint.Name, }).Info("Creating stack") - stack, deploymentErr := portainerClient.StackCreateSwarm(stackName, loadedEnvironmentVariables, stackFileContent, endpointSwarmClusterID, endpoint.ID) + stack, deploymentErr := portainerClient.StackCreateSwarm(client.StackCreateSwarmOptions{ + StackName: stackName, + EnvironmentVariables: loadedEnvironmentVariables, + StackFileContent: stackFileContent, + SwarmClusterID: endpointSwarmClusterID, + EndpointID: endpoint.ID, + }) common.CheckError(deploymentErr) logrus.WithFields(logrus.Fields{ "stack": stack.Name, From 17b941d10824b7bc492e934b25770a4eedb73aa5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Carlos=20Mej=C3=ADas=20Rodr=C3=ADguez?= Date: Mon, 26 Aug 2019 01:48:59 -0400 Subject: [PATCH 26/34] Add options object for PortainerClient.StackCreateCompose() --- client/client.go | 20 ++++++++++++++------ cmd/stackDeploy.go | 7 ++++++- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/client/client.go b/client/client.go index fdf975a..7cb1b22 100644 --- a/client/client.go +++ b/client/client.go @@ -33,6 +33,14 @@ type StackCreateSwarmOptions struct { EndpointID portainer.EndpointID } +// StackCreateComposeOptions represents options passed to PortainerClient.StackCreateCompose() +type StackCreateComposeOptions struct { + StackName string + EnvironmentVariables []portainer.Pair + StackFileContent string + EndpointID portainer.EndpointID +} + // Config represents a Portainer client configuration type Config struct { URL *url.URL @@ -61,7 +69,7 @@ type PortainerClient interface { StackCreateSwarm(options StackCreateSwarmOptions) (stack portainer.Stack, err error) // Create compose stack - StackCreateCompose(stackName string, environmentVariables []portainer.Pair, stackFileContent string, endpointID portainer.EndpointID) (stack portainer.Stack, err error) + StackCreateCompose(options StackCreateComposeOptions) (stack portainer.Stack, err error) // Update stack StackUpdate(stack portainer.Stack, environmentVariables []portainer.Pair, stackFileContent string, prune bool, endpointID portainer.EndpointID) error @@ -265,14 +273,14 @@ func (n *portainerClientImp) StackCreateSwarm(options StackCreateSwarmOptions) ( return } -func (n *portainerClientImp) StackCreateCompose(stackName string, environmentVariables []portainer.Pair, stackFileContent string, endpointID portainer.EndpointID) (stack portainer.Stack, err error) { +func (n *portainerClientImp) StackCreateCompose(options StackCreateComposeOptions) (stack portainer.Stack, err error) { reqBody := StackCreateRequest{ - Name: stackName, - Env: environmentVariables, - StackFileContent: stackFileContent, + Name: options.StackName, + Env: options.EnvironmentVariables, + StackFileContent: options.StackFileContent, } - err = n.doJSONWithToken(fmt.Sprintf("stacks?type=%v&method=%s&endpointId=%v", 2, "string", endpointID), http.MethodPost, http.Header{}, &reqBody, &stack) + err = n.doJSONWithToken(fmt.Sprintf("stacks?type=%v&method=%s&endpointId=%v", 2, "string", options.EndpointID), http.MethodPost, http.Header{}, &reqBody, &stack) return } diff --git a/cmd/stackDeploy.go b/cmd/stackDeploy.go index 4b0b0c5..2d5a293 100644 --- a/cmd/stackDeploy.go +++ b/cmd/stackDeploy.go @@ -156,7 +156,12 @@ var stackDeployCmd = &cobra.Command{ "stack": stackName, "endpoint": endpoint.Name, }).Info("Creating stack") - stack, deploymentErr := portainerClient.StackCreateCompose(stackName, loadedEnvironmentVariables, stackFileContent, endpoint.ID) + stack, deploymentErr := portainerClient.StackCreateCompose(client.StackCreateComposeOptions{ + StackName: stackName, + EnvironmentVariables: loadedEnvironmentVariables, + StackFileContent: stackFileContent, + EndpointID: endpoint.ID, + }) common.CheckError(deploymentErr) logrus.WithFields(logrus.Fields{ "stack": stack.Name, From d17b9eb9f546dce936908fab1926a8d54cf96748 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Carlos=20Mej=C3=ADas=20Rodr=C3=ADguez?= Date: Mon, 26 Aug 2019 01:52:41 -0400 Subject: [PATCH 27/34] Add options object for PortainerClient.StackUpdate() --- client/client.go | 21 +++++++++++++++------ cmd/stackDeploy.go | 8 +++++++- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/client/client.go b/client/client.go index 7cb1b22..e80be22 100644 --- a/client/client.go +++ b/client/client.go @@ -41,6 +41,15 @@ type StackCreateComposeOptions struct { EndpointID portainer.EndpointID } +// StackUpdateOptions represents options passed to PortainerClient.StackUpdate() +type StackUpdateOptions struct { + Stack portainer.Stack + EnvironmentVariables []portainer.Pair + StackFileContent string + Prune bool + EndpointID portainer.EndpointID +} + // Config represents a Portainer client configuration type Config struct { URL *url.URL @@ -72,7 +81,7 @@ type PortainerClient interface { StackCreateCompose(options StackCreateComposeOptions) (stack portainer.Stack, err error) // Update stack - StackUpdate(stack portainer.Stack, environmentVariables []portainer.Pair, stackFileContent string, prune bool, endpointID portainer.EndpointID) error + StackUpdate(options StackUpdateOptions) error // Delete stack StackDelete(stackID portainer.StackID) error @@ -284,14 +293,14 @@ func (n *portainerClientImp) StackCreateCompose(options StackCreateComposeOption return } -func (n *portainerClientImp) StackUpdate(stack portainer.Stack, environmentVariables []portainer.Pair, stackFileContent string, prune bool, endpointID portainer.EndpointID) (err error) { +func (n *portainerClientImp) StackUpdate(options StackUpdateOptions) (err error) { reqBody := StackUpdateRequest{ - Env: environmentVariables, - StackFileContent: stackFileContent, - Prune: prune, + Env: options.EnvironmentVariables, + StackFileContent: options.StackFileContent, + Prune: options.Prune, } - err = n.doJSONWithToken(fmt.Sprintf("stacks/%v?endpointId=%v", stack.ID, endpointID), http.MethodPut, http.Header{}, &reqBody, nil) + err = n.doJSONWithToken(fmt.Sprintf("stacks/%v?endpointId=%v", options.Stack.ID, options.EndpointID), http.MethodPut, http.Header{}, &reqBody, nil) return } diff --git a/cmd/stackDeploy.go b/cmd/stackDeploy.go index 2d5a293..dfdde9e 100644 --- a/cmd/stackDeploy.go +++ b/cmd/stackDeploy.go @@ -117,7 +117,13 @@ var stackDeployCmd = &cobra.Command{ logrus.WithFields(logrus.Fields{ "stack": retrievedStack.Name, }).Info("Updating stack") - err := portainerClient.StackUpdate(retrievedStack, newEnvironmentVariables, stackFileContent, viper.GetBool("stack.deploy.prune"), endpoint.ID) + err := portainerClient.StackUpdate(client.StackUpdateOptions{ + Stack: retrievedStack, + EnvironmentVariables: newEnvironmentVariables, + StackFileContent: stackFileContent, + Prune: viper.GetBool("stack.deploy.prune"), + EndpointID: endpoint.ID, + }) common.CheckError(err) } else if stackRetrievalErr == common.ErrStackNotFound { // We are deploying a new stack From f745392e413b00bb4aa1354d1a36936c6a441152 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Carlos=20Mej=C3=ADas=20Rodr=C3=ADguez?= Date: Mon, 26 Aug 2019 01:59:40 -0400 Subject: [PATCH 28/34] Rename client/portainerTypes.go to types.go --- client/{portainerTypes.go => types.go} | 0 client/{portainerTypes_test.go => types_test.go} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename client/{portainerTypes.go => types.go} (100%) rename client/{portainerTypes_test.go => types_test.go} (100%) diff --git a/client/portainerTypes.go b/client/types.go similarity index 100% rename from client/portainerTypes.go rename to client/types.go diff --git a/client/portainerTypes_test.go b/client/types_test.go similarity index 100% rename from client/portainerTypes_test.go rename to client/types_test.go From 5d95af3681d54e9dbb36b6b19ce8c6db9b155594 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Carlos=20Mej=C3=ADas=20Rodr=C3=ADguez?= Date: Mon, 26 Aug 2019 02:29:58 -0400 Subject: [PATCH 29/34] Split Portainer client source file into several function-related ones --- client/auth.go | 32 +++++ client/auth_test.go | 48 +++++++ client/client.go | 159 ----------------------- client/client_test.go | 86 ------------ client/endpointGroup_list.go | 12 ++ client/endpoint_docker_info.go | 13 ++ client/endpoint_list.go | 12 ++ client/errors.go | 16 +++ client/{types_test.go => errors_test.go} | 39 ------ client/stackFile_inspect.go | 26 ++++ client/stack_create.go | 56 ++++++++ client/stack_delete.go | 13 ++ client/stack_list.go | 28 ++++ client/stack_update.go | 35 +++++ client/status.go | 12 ++ client/types.go | 63 --------- client/utils.go | 44 +++++++ client/utils_test.go | 97 ++++++++++++++ 18 files changed, 444 insertions(+), 347 deletions(-) create mode 100644 client/auth.go create mode 100644 client/auth_test.go create mode 100644 client/endpointGroup_list.go create mode 100644 client/endpoint_docker_info.go create mode 100644 client/endpoint_list.go create mode 100644 client/errors.go rename client/{types_test.go => errors_test.go} (59%) create mode 100644 client/stackFile_inspect.go create mode 100644 client/stack_create.go create mode 100644 client/stack_delete.go create mode 100644 client/stack_list.go create mode 100644 client/stack_update.go create mode 100644 client/status.go delete mode 100644 client/types.go create mode 100644 client/utils.go create mode 100644 client/utils_test.go diff --git a/client/auth.go b/client/auth.go new file mode 100644 index 0000000..dbf4d93 --- /dev/null +++ b/client/auth.go @@ -0,0 +1,32 @@ +package client + +import "net/http" + +// AuthenticateUserRequest represents the body of a request to POST /auth +type AuthenticateUserRequest struct { + Username string + Password string +} + +// AuthenticateUserResponse represents the body of a response for a request to POST /auth +type AuthenticateUserResponse struct { + Jwt string +} + +func (n *portainerClientImp) Auth() (token string, err error) { + reqBody := AuthenticateUserRequest{ + Username: n.user, + Password: n.password, + } + + respBody := AuthenticateUserResponse{} + + err = n.doJSON("auth", http.MethodPost, http.Header{}, &reqBody, &respBody) + if err != nil { + return + } + + token = respBody.Jwt + + return +} diff --git a/client/auth_test.go b/client/auth_test.go new file mode 100644 index 0000000..f837d5d --- /dev/null +++ b/client/auth_test.go @@ -0,0 +1,48 @@ +package client + +import ( + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestClientAuthenticates(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + var body map[string]interface{} + err := readRequestBodyAsJSON(req, &body) + + assert.Equal(t, req.Method, http.MethodPost) + assert.Equal(t, req.RequestURI, "/api/auth") + assert.NotNil(t, req.Header["Content-Type"]) + assert.NotNil(t, req.Header["Content-Type"][0]) + assert.Equal(t, req.Header["Content-Type"][0], "application/json") + assert.NotNil(t, req.Header["User-Agent"]) + assert.NotNil(t, req.Header["User-Agent"][0]) + assert.Equal(t, req.Header["User-Agent"][0], "GE007") + assert.Nil(t, err) + assert.NotNil(t, body["Username"]) + assert.Equal(t, body["Username"], "admin") + assert.NotNil(t, body["Password"]) + assert.Equal(t, body["Password"], "a") + + writeResponseBodyAsJSON(w, map[string]interface{}{ + "jwt": "somerandomtoken", + }) + })) + defer ts.Close() + + apiURL, _ := url.Parse(ts.URL + "/api/") + + customClient := NewClient(ts.Client(), Config{ + URL: apiURL, + User: "admin", + Password: "a", + UserAgent: "GE007", + }) + token, err := customClient.Auth() + assert.Nil(t, err) + assert.Equal(t, token, "somerandomtoken") +} diff --git a/client/client.go b/client/client.go index e80be22..6f3bf88 100644 --- a/client/client.go +++ b/client/client.go @@ -3,53 +3,13 @@ package client import ( "bytes" "encoding/json" - "errors" - "fmt" "io" - "io/ioutil" "net/http" "net/url" portainer "github.com/portainer/portainer/api" ) -// StackListFilter represents a filter for a stack list -type StackListFilter struct { - SwarmID string `json:"SwarmId,omitempty"` - EndpointID portainer.EndpointID `json:"EndpointId,omitempty"` -} - -// StackListOptions represents options passed to PortainerClient.StackList() -type StackListOptions struct { - Filter StackListFilter -} - -// StackCreateSwarmOptions represents options passed to PortainerClient.StackCreateSwarm() -type StackCreateSwarmOptions struct { - StackName string - EnvironmentVariables []portainer.Pair - StackFileContent string - SwarmClusterID string - EndpointID portainer.EndpointID -} - -// StackCreateComposeOptions represents options passed to PortainerClient.StackCreateCompose() -type StackCreateComposeOptions struct { - StackName string - EnvironmentVariables []portainer.Pair - StackFileContent string - EndpointID portainer.EndpointID -} - -// StackUpdateOptions represents options passed to PortainerClient.StackUpdate() -type StackUpdateOptions struct { - Stack portainer.Stack - EnvironmentVariables []portainer.Pair - StackFileContent string - Prune bool - EndpointID portainer.EndpointID -} - // Config represents a Portainer client configuration type Config struct { URL *url.URL @@ -113,27 +73,6 @@ type portainerClientImp struct { afterResponseHooks []func(resp *http.Response) (err error) } -// 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, requestBody io.Reader, headers http.Header) (resp *http.Response, err error) { requestURL, err := n.url.Parse(uri) @@ -234,104 +173,6 @@ func (n *portainerClientImp) AfterResponse(hook func(resp *http.Response) (err e n.afterResponseHooks = append(n.afterResponseHooks, hook) } -func (n *portainerClientImp) Auth() (token string, err error) { - reqBody := AuthenticateUserRequest{ - Username: n.user, - Password: n.password, - } - - respBody := AuthenticateUserResponse{} - - err = n.doJSON("auth", http.MethodPost, http.Header{}, &reqBody, &respBody) - if err != nil { - return - } - - token = respBody.Jwt - - return -} - -func (n *portainerClientImp) EndpointList() (endpoints []portainer.Endpoint, err error) { - err = n.doJSONWithToken("endpoints", http.MethodGet, http.Header{}, nil, &endpoints) - return -} - -func (n *portainerClientImp) EndpointGroupList() (endpointGroups []portainer.EndpointGroup, err error) { - err = n.doJSONWithToken("endpoint_groups", http.MethodGet, http.Header{}, nil, &endpointGroups) - return -} - -func (n *portainerClientImp) StackList(options StackListOptions) (stacks []portainer.Stack, err error) { - filterJSONBytes, _ := json.Marshal(options.Filter) - filterJSONString := string(filterJSONBytes) - - err = n.doJSONWithToken(fmt.Sprintf("stacks?filters=%s", filterJSONString), http.MethodGet, http.Header{}, nil, &stacks) - return -} - -func (n *portainerClientImp) StackCreateSwarm(options StackCreateSwarmOptions) (stack portainer.Stack, err error) { - reqBody := StackCreateRequest{ - Name: options.StackName, - Env: options.EnvironmentVariables, - SwarmID: options.SwarmClusterID, - StackFileContent: options.StackFileContent, - } - - err = n.doJSONWithToken(fmt.Sprintf("stacks?type=%v&method=%s&endpointId=%v", 1, "string", options.EndpointID), http.MethodPost, http.Header{}, &reqBody, &stack) - return -} - -func (n *portainerClientImp) StackCreateCompose(options StackCreateComposeOptions) (stack portainer.Stack, err error) { - reqBody := StackCreateRequest{ - Name: options.StackName, - Env: options.EnvironmentVariables, - StackFileContent: options.StackFileContent, - } - - err = n.doJSONWithToken(fmt.Sprintf("stacks?type=%v&method=%s&endpointId=%v", 2, "string", options.EndpointID), http.MethodPost, http.Header{}, &reqBody, &stack) - return -} - -func (n *portainerClientImp) StackUpdate(options StackUpdateOptions) (err error) { - reqBody := StackUpdateRequest{ - Env: options.EnvironmentVariables, - StackFileContent: options.StackFileContent, - Prune: options.Prune, - } - - err = n.doJSONWithToken(fmt.Sprintf("stacks/%v?endpointId=%v", options.Stack.ID, options.EndpointID), http.MethodPut, http.Header{}, &reqBody, nil) - return -} - -func (n *portainerClientImp) StackDelete(stackID portainer.StackID) (err error) { - err = n.doJSONWithToken(fmt.Sprintf("stacks/%d", stackID), http.MethodDelete, http.Header{}, nil, nil) - return -} - -func (n *portainerClientImp) StackFileInspect(stackID portainer.StackID) (content string, err error) { - var respBody StackFileInspectResponse - - err = n.doJSONWithToken(fmt.Sprintf("stacks/%v/file", stackID), http.MethodGet, http.Header{}, nil, &respBody) - if err != nil { - return - } - - content = respBody.StackFileContent - - return -} - -func (n *portainerClientImp) EndpointDockerInfo(endpointID portainer.EndpointID) (info map[string]interface{}, err error) { - err = n.doJSONWithToken(fmt.Sprintf("endpoints/%v/docker/info", endpointID), http.MethodGet, http.Header{}, nil, &info) - return -} - -func (n *portainerClientImp) Status() (status portainer.Status, err error) { - err = n.doJSONWithToken("status", http.MethodGet, http.Header{}, nil, &status) - return -} - // NewClient creates a new Portainer API client func NewClient(httpClient *http.Client, config Config) PortainerClient { return &portainerClientImp{ diff --git a/client/client_test.go b/client/client_test.go index 88726c2..275f3df 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -1,7 +1,6 @@ package client import ( - "bytes" "encoding/json" "fmt" "io" @@ -36,44 +35,6 @@ func TestNewClient(t *testing.T) { assert.NotNil(t, validClient) } -func TestClientAuthenticates(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - var body map[string]interface{} - err := readRequestBodyAsJSON(req, &body) - - assert.Equal(t, req.Method, http.MethodPost) - assert.Equal(t, req.RequestURI, "/api/auth") - assert.NotNil(t, req.Header["Content-Type"]) - assert.NotNil(t, req.Header["Content-Type"][0]) - assert.Equal(t, req.Header["Content-Type"][0], "application/json") - assert.NotNil(t, req.Header["User-Agent"]) - assert.NotNil(t, req.Header["User-Agent"][0]) - assert.Equal(t, req.Header["User-Agent"][0], "GE007") - assert.Nil(t, err) - assert.NotNil(t, body["Username"]) - assert.Equal(t, body["Username"], "admin") - assert.NotNil(t, body["Password"]) - assert.Equal(t, body["Password"], "a") - - writeResponseBodyAsJSON(w, map[string]interface{}{ - "jwt": "somerandomtoken", - }) - })) - defer ts.Close() - - apiURL, _ := url.Parse(ts.URL + "/api/") - - customClient := NewClient(ts.Client(), Config{ - URL: apiURL, - User: "admin", - Password: "a", - UserAgent: "GE007", - }) - token, err := customClient.Auth() - assert.Nil(t, err) - assert.Equal(t, token, "somerandomtoken") -} - func Test_portainerClientImp_do(t *testing.T) { type fields struct { user string @@ -184,50 +145,3 @@ func Test_portainerClientImp_do(t *testing.T) { }) } } - -func Test_checkResponseForErrors(t *testing.T) { - type args struct { - resp *http.Response - } - tests := []struct { - name string - args args - wantErr bool - }{ - { - name: "generic error", - args: args{ - resp: func() (resp *http.Response) { - resp = &http.Response{ - StatusCode: http.StatusNotFound, - } - bodyBytes, _ := json.Marshal(map[string]interface{}{ - "Err": "Error", - "Details": "Not found", - }) - resp.Body = ioutil.NopCloser(bytes.NewReader(bodyBytes)) - return - }(), - }, - wantErr: true, - }, - { - name: "non generic error", - args: args{ - resp: func() (resp *http.Response) { - resp = &http.Response{ - StatusCode: http.StatusNotFound, - Body: ioutil.NopCloser(bytes.NewReader([]byte("Err"))), - } - return - }(), - }, - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - assert.Equal(t, tt.wantErr, checkResponseForErrors(tt.args.resp) != nil) - }) - } -} diff --git a/client/endpointGroup_list.go b/client/endpointGroup_list.go new file mode 100644 index 0000000..89de541 --- /dev/null +++ b/client/endpointGroup_list.go @@ -0,0 +1,12 @@ +package client + +import ( + "net/http" + + portainer "github.com/portainer/portainer/api" +) + +func (n *portainerClientImp) EndpointGroupList() (endpointGroups []portainer.EndpointGroup, err error) { + err = n.doJSONWithToken("endpoint_groups", http.MethodGet, http.Header{}, nil, &endpointGroups) + return +} diff --git a/client/endpoint_docker_info.go b/client/endpoint_docker_info.go new file mode 100644 index 0000000..83fb90e --- /dev/null +++ b/client/endpoint_docker_info.go @@ -0,0 +1,13 @@ +package client + +import ( + "fmt" + "net/http" + + portainer "github.com/portainer/portainer/api" +) + +func (n *portainerClientImp) EndpointDockerInfo(endpointID portainer.EndpointID) (info map[string]interface{}, err error) { + err = n.doJSONWithToken(fmt.Sprintf("endpoints/%v/docker/info", endpointID), http.MethodGet, http.Header{}, nil, &info) + return +} diff --git a/client/endpoint_list.go b/client/endpoint_list.go new file mode 100644 index 0000000..2aac0e9 --- /dev/null +++ b/client/endpoint_list.go @@ -0,0 +1,12 @@ +package client + +import ( + "net/http" + + portainer "github.com/portainer/portainer/api" +) + +func (n *portainerClientImp) EndpointList() (endpoints []portainer.Endpoint, err error) { + err = n.doJSONWithToken("endpoints", http.MethodGet, http.Header{}, nil, &endpoints) + return +} diff --git a/client/errors.go b/client/errors.go new file mode 100644 index 0000000..6a77642 --- /dev/null +++ b/client/errors.go @@ -0,0 +1,16 @@ +package client + +import "fmt" + +// GenericError represents the body of a generic error returned by the Portainer API +type GenericError struct { + Err string + Details string +} + +func (e *GenericError) Error() string { + if e.Details != "" { + return fmt.Sprintf("%s: %s", e.Err, e.Details) + } + return fmt.Sprintf("%s", e.Err) +} diff --git a/client/types_test.go b/client/errors_test.go similarity index 59% rename from client/types_test.go rename to client/errors_test.go index eb7050e..a0ebaef 100644 --- a/client/types_test.go +++ b/client/errors_test.go @@ -3,48 +3,9 @@ package client import ( "testing" - portainer "github.com/portainer/portainer/api" "github.com/stretchr/testify/assert" ) -func TestGetTranslatedStackType(t *testing.T) { - type args struct { - t portainer.StackType - } - tests := []struct { - name string - args args - want string - }{ - { - name: "swarm stack type", - args: args{ - t: portainer.DockerSwarmStack, - }, - want: "swarm", - }, - { - name: "compose stack type", - args: args{ - t: portainer.DockerComposeStack, - }, - want: "compose", - }, - { - name: "unknown stack type", - args: args{ - t: 100, - }, - want: "", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - assert.Equal(t, tt.want, GetTranslatedStackType(tt.args.t)) - }) - } -} - func TestGenericError_Error(t *testing.T) { type fields struct { Err string diff --git a/client/stackFile_inspect.go b/client/stackFile_inspect.go new file mode 100644 index 0000000..7ef6720 --- /dev/null +++ b/client/stackFile_inspect.go @@ -0,0 +1,26 @@ +package client + +import ( + "fmt" + "net/http" + + portainer "github.com/portainer/portainer/api" +) + +// StackFileInspectResponse represents the body of a response for a request to GET /stack/{id}/file +type StackFileInspectResponse struct { + StackFileContent string +} + +func (n *portainerClientImp) StackFileInspect(stackID portainer.StackID) (content string, err error) { + var respBody StackFileInspectResponse + + err = n.doJSONWithToken(fmt.Sprintf("stacks/%v/file", stackID), http.MethodGet, http.Header{}, nil, &respBody) + if err != nil { + return + } + + content = respBody.StackFileContent + + return +} diff --git a/client/stack_create.go b/client/stack_create.go new file mode 100644 index 0000000..d93011c --- /dev/null +++ b/client/stack_create.go @@ -0,0 +1,56 @@ +package client + +import ( + "fmt" + "net/http" + + portainer "github.com/portainer/portainer/api" +) + +// StackCreateComposeOptions represents options passed to PortainerClient.StackCreateCompose() +type StackCreateComposeOptions struct { + StackName string + EnvironmentVariables []portainer.Pair + StackFileContent string + EndpointID portainer.EndpointID +} + +// StackCreateRequest represents the body of a request to POST /stacks +type StackCreateRequest struct { + Name string + SwarmID string + StackFileContent string + Env []portainer.Pair `json:",omitempty"` +} + +// StackCreateSwarmOptions represents options passed to PortainerClient.StackCreateSwarm() +type StackCreateSwarmOptions struct { + StackName string + EnvironmentVariables []portainer.Pair + StackFileContent string + SwarmClusterID string + EndpointID portainer.EndpointID +} + +func (n *portainerClientImp) StackCreateCompose(options StackCreateComposeOptions) (stack portainer.Stack, err error) { + reqBody := StackCreateRequest{ + Name: options.StackName, + Env: options.EnvironmentVariables, + StackFileContent: options.StackFileContent, + } + + err = n.doJSONWithToken(fmt.Sprintf("stacks?type=%v&method=%s&endpointId=%v", 2, "string", options.EndpointID), http.MethodPost, http.Header{}, &reqBody, &stack) + return +} + +func (n *portainerClientImp) StackCreateSwarm(options StackCreateSwarmOptions) (stack portainer.Stack, err error) { + reqBody := StackCreateRequest{ + Name: options.StackName, + Env: options.EnvironmentVariables, + SwarmID: options.SwarmClusterID, + StackFileContent: options.StackFileContent, + } + + err = n.doJSONWithToken(fmt.Sprintf("stacks?type=%v&method=%s&endpointId=%v", 1, "string", options.EndpointID), http.MethodPost, http.Header{}, &reqBody, &stack) + return +} diff --git a/client/stack_delete.go b/client/stack_delete.go new file mode 100644 index 0000000..f67fc20 --- /dev/null +++ b/client/stack_delete.go @@ -0,0 +1,13 @@ +package client + +import ( + "fmt" + "net/http" + + portainer "github.com/portainer/portainer/api" +) + +func (n *portainerClientImp) StackDelete(stackID portainer.StackID) (err error) { + err = n.doJSONWithToken(fmt.Sprintf("stacks/%d", stackID), http.MethodDelete, http.Header{}, nil, nil) + return +} diff --git a/client/stack_list.go b/client/stack_list.go new file mode 100644 index 0000000..7b74c6d --- /dev/null +++ b/client/stack_list.go @@ -0,0 +1,28 @@ +package client + +import ( + "encoding/json" + "fmt" + "net/http" + + portainer "github.com/portainer/portainer/api" +) + +// StackListFilter represents a filter for a stack list +type StackListFilter struct { + SwarmID string `json:"SwarmId,omitempty"` + EndpointID portainer.EndpointID `json:"EndpointId,omitempty"` +} + +// StackListOptions represents options passed to PortainerClient.StackList() +type StackListOptions struct { + Filter StackListFilter +} + +func (n *portainerClientImp) StackList(options StackListOptions) (stacks []portainer.Stack, err error) { + filterJSONBytes, _ := json.Marshal(options.Filter) + filterJSONString := string(filterJSONBytes) + + err = n.doJSONWithToken(fmt.Sprintf("stacks?filters=%s", filterJSONString), http.MethodGet, http.Header{}, nil, &stacks) + return +} diff --git a/client/stack_update.go b/client/stack_update.go new file mode 100644 index 0000000..45ea6b8 --- /dev/null +++ b/client/stack_update.go @@ -0,0 +1,35 @@ +package client + +import ( + "fmt" + "net/http" + + portainer "github.com/portainer/portainer/api" +) + +// StackUpdateOptions represents options passed to PortainerClient.StackUpdate() +type StackUpdateOptions struct { + Stack portainer.Stack + EnvironmentVariables []portainer.Pair + StackFileContent string + Prune bool + EndpointID portainer.EndpointID +} + +// StackUpdateRequest represents the body of a request to PUT /stacks/{id} +type StackUpdateRequest struct { + StackFileContent string + Env []portainer.Pair `json:",omitempty"` + Prune bool +} + +func (n *portainerClientImp) StackUpdate(options StackUpdateOptions) (err error) { + reqBody := StackUpdateRequest{ + Env: options.EnvironmentVariables, + StackFileContent: options.StackFileContent, + Prune: options.Prune, + } + + err = n.doJSONWithToken(fmt.Sprintf("stacks/%v?endpointId=%v", options.Stack.ID, options.EndpointID), http.MethodPut, http.Header{}, &reqBody, nil) + return +} diff --git a/client/status.go b/client/status.go new file mode 100644 index 0000000..62744bd --- /dev/null +++ b/client/status.go @@ -0,0 +1,12 @@ +package client + +import ( + "net/http" + + portainer "github.com/portainer/portainer/api" +) + +func (n *portainerClientImp) Status() (status portainer.Status, err error) { + err = n.doJSONWithToken("status", http.MethodGet, http.Header{}, nil, &status) + return +} diff --git a/client/types.go b/client/types.go deleted file mode 100644 index 48d9d9a..0000000 --- a/client/types.go +++ /dev/null @@ -1,63 +0,0 @@ -package client - -import ( - "fmt" - - portainer "github.com/portainer/portainer/api" -) - -// GetTranslatedStackType returns a stack's Type field (int) translated to it's human readable form (string) -func GetTranslatedStackType(t portainer.StackType) string { - switch t { - case portainer.DockerSwarmStack: - return "swarm" - case portainer.DockerComposeStack: - return "compose" - default: - return "" - } -} - -// StackCreateRequest represents the body of a request to POST /stacks -type StackCreateRequest struct { - Name string - SwarmID string - StackFileContent string - Env []portainer.Pair `json:",omitempty"` -} - -// StackUpdateRequest represents the body of a request to PUT /stacks/{id} -type StackUpdateRequest struct { - StackFileContent string - Env []portainer.Pair `json:",omitempty"` - Prune bool -} - -// StackFileInspectResponse represents the body of a response for a request to GET /stack/{id}/file -type StackFileInspectResponse struct { - StackFileContent string -} - -// GenericError represents the body of a generic error returned by the Portainer API -type GenericError struct { - Err string - Details string -} - -func (e *GenericError) Error() string { - if e.Details != "" { - return fmt.Sprintf("%s: %s", e.Err, e.Details) - } - return fmt.Sprintf("%s", e.Err) -} - -// AuthenticateUserRequest represents the body of a request to POST /auth -type AuthenticateUserRequest struct { - Username string - Password string -} - -// AuthenticateUserResponse represents the body of a response for a request to POST /auth -type AuthenticateUserResponse struct { - Jwt string -} diff --git a/client/utils.go b/client/utils.go new file mode 100644 index 0000000..173b31f --- /dev/null +++ b/client/utils.go @@ -0,0 +1,44 @@ +package client + +import ( + "bytes" + "encoding/json" + "errors" + "io/ioutil" + "net/http" + + portainer "github.com/portainer/portainer/api" +) + +// GetTranslatedStackType returns a stack's Type field (int) translated to it's human readable form (string) +func GetTranslatedStackType(t portainer.StackType) string { + switch t { + case portainer.DockerSwarmStack: + return "swarm" + case portainer.DockerComposeStack: + return "compose" + default: + return "" + } +} + +// 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 +} diff --git a/client/utils_test.go b/client/utils_test.go new file mode 100644 index 0000000..a4f225e --- /dev/null +++ b/client/utils_test.go @@ -0,0 +1,97 @@ +package client + +import ( + "bytes" + "encoding/json" + "io/ioutil" + "net/http" + "testing" + + portainer "github.com/portainer/portainer/api" + "github.com/stretchr/testify/assert" +) + +func TestGetTranslatedStackType(t *testing.T) { + type args struct { + t portainer.StackType + } + tests := []struct { + name string + args args + want string + }{ + { + name: "swarm stack type", + args: args{ + t: portainer.DockerSwarmStack, + }, + want: "swarm", + }, + { + name: "compose stack type", + args: args{ + t: portainer.DockerComposeStack, + }, + want: "compose", + }, + { + name: "unknown stack type", + args: args{ + t: 100, + }, + want: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, GetTranslatedStackType(tt.args.t)) + }) + } +} + +func Test_checkResponseForErrors(t *testing.T) { + type args struct { + resp *http.Response + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "generic error", + args: args{ + resp: func() (resp *http.Response) { + resp = &http.Response{ + StatusCode: http.StatusNotFound, + } + bodyBytes, _ := json.Marshal(map[string]interface{}{ + "Err": "Error", + "Details": "Not found", + }) + resp.Body = ioutil.NopCloser(bytes.NewReader(bodyBytes)) + return + }(), + }, + wantErr: true, + }, + { + name: "non generic error", + args: args{ + resp: func() (resp *http.Response) { + resp = &http.Response{ + StatusCode: http.StatusNotFound, + Body: ioutil.NopCloser(bytes.NewReader([]byte("Err"))), + } + return + }(), + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.wantErr, checkResponseForErrors(tt.args.resp) != nil) + }) + } +} From 22dc5e5d753e42dda3b0cd85b0e2190e1ac676ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Carlos=20Mej=C3=ADas=20Rodr=C3=ADguez?= Date: Mon, 26 Aug 2019 02:34:01 -0400 Subject: [PATCH 30/34] Rename PortainerClient.Auth() to AuthenticateUser() --- client/auth.go | 2 +- client/auth_test.go | 2 +- client/client.go | 6 +++--- cmd/login.go | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/client/auth.go b/client/auth.go index dbf4d93..71921cf 100644 --- a/client/auth.go +++ b/client/auth.go @@ -13,7 +13,7 @@ type AuthenticateUserResponse struct { Jwt string } -func (n *portainerClientImp) Auth() (token string, err error) { +func (n *portainerClientImp) AuthenticateUser() (token string, err error) { reqBody := AuthenticateUserRequest{ Username: n.user, Password: n.password, diff --git a/client/auth_test.go b/client/auth_test.go index f837d5d..5769575 100644 --- a/client/auth_test.go +++ b/client/auth_test.go @@ -42,7 +42,7 @@ func TestClientAuthenticates(t *testing.T) { Password: "a", UserAgent: "GE007", }) - token, err := customClient.Auth() + token, err := customClient.AuthenticateUser() assert.Nil(t, err) assert.Equal(t, token, "somerandomtoken") } diff --git a/client/client.go b/client/client.go index 6f3bf88..57c8904 100644 --- a/client/client.go +++ b/client/client.go @@ -22,8 +22,8 @@ type Config struct { // PortainerClient represents a Portainer API client type PortainerClient interface { - // Auth a user to get an auth token - Auth() (token string, err error) + // AuthenticateUser a user to get an auth token + AuthenticateUser() (token string, err error) // Get endpoints EndpointList() ([]portainer.Endpoint, error) @@ -155,7 +155,7 @@ func (n *portainerClientImp) doJSON(uri, method string, headers http.Header, req func (n *portainerClientImp) doJSONWithToken(uri, method string, headers http.Header, request interface{}, response interface{}) (err error) { // Ensure there is an auth token if n.token == "" { - n.token, err = n.Auth() + n.token, err = n.AuthenticateUser() if err != nil { return } diff --git a/cmd/login.go b/cmd/login.go index a3047dc..49f3191 100644 --- a/cmd/login.go +++ b/cmd/login.go @@ -22,7 +22,7 @@ var loginCmd = &cobra.Command{ logrus.WithFields(logrus.Fields{ "user": user, }).Debug("Getting auth token") - authToken, err := client.Auth() + authToken, err := client.AuthenticateUser() common.CheckError(err) if viper.GetBool("login.print") { From 1efa02abf34ee2b20170fcabeb6197f1b24ae6fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Carlos=20Mej=C3=ADas=20Rodr=C3=ADguez?= Date: Mon, 26 Aug 2019 02:53:42 -0400 Subject: [PATCH 31/34] Add username and password to PortainerClient.AuthenticateUser() --- client/auth.go | 12 +++++++++--- client/auth_test.go | 5 ++++- client/client.go | 7 +++++-- cmd/login.go | 9 +++++++-- 4 files changed, 25 insertions(+), 8 deletions(-) diff --git a/client/auth.go b/client/auth.go index 71921cf..6a9d57c 100644 --- a/client/auth.go +++ b/client/auth.go @@ -2,6 +2,12 @@ package client import "net/http" +// AuthenticateUserOptions represents options passed to PortainerClient.AuthenticateUser() +type AuthenticateUserOptions struct { + Username string + Password string +} + // AuthenticateUserRequest represents the body of a request to POST /auth type AuthenticateUserRequest struct { Username string @@ -13,10 +19,10 @@ type AuthenticateUserResponse struct { Jwt string } -func (n *portainerClientImp) AuthenticateUser() (token string, err error) { +func (n *portainerClientImp) AuthenticateUser(options AuthenticateUserOptions) (token string, err error) { reqBody := AuthenticateUserRequest{ - Username: n.user, - Password: n.password, + Username: options.Username, + Password: options.Password, } respBody := AuthenticateUserResponse{} diff --git a/client/auth_test.go b/client/auth_test.go index 5769575..9e935f8 100644 --- a/client/auth_test.go +++ b/client/auth_test.go @@ -42,7 +42,10 @@ func TestClientAuthenticates(t *testing.T) { Password: "a", UserAgent: "GE007", }) - token, err := customClient.AuthenticateUser() + token, err := customClient.AuthenticateUser(AuthenticateUserOptions{ + Username: "admin", + Password: "a", + }) assert.Nil(t, err) assert.Equal(t, token, "somerandomtoken") } diff --git a/client/client.go b/client/client.go index 57c8904..7e14707 100644 --- a/client/client.go +++ b/client/client.go @@ -23,7 +23,7 @@ type Config struct { // PortainerClient represents a Portainer API client type PortainerClient interface { // AuthenticateUser a user to get an auth token - AuthenticateUser() (token string, err error) + AuthenticateUser(options AuthenticateUserOptions) (token string, err error) // Get endpoints EndpointList() ([]portainer.Endpoint, error) @@ -155,7 +155,10 @@ func (n *portainerClientImp) doJSON(uri, method string, headers http.Header, req func (n *portainerClientImp) doJSONWithToken(uri, method string, headers http.Header, request interface{}, response interface{}) (err error) { // Ensure there is an auth token if n.token == "" { - n.token, err = n.AuthenticateUser() + n.token, err = n.AuthenticateUser(AuthenticateUserOptions{ + Username: n.user, + Password: n.password, + }) if err != nil { return } diff --git a/cmd/login.go b/cmd/login.go index 49f3191..4f157fb 100644 --- a/cmd/login.go +++ b/cmd/login.go @@ -3,6 +3,8 @@ package cmd import ( "fmt" + "github.com/greenled/portainer-stack-utils/client" + "github.com/greenled/portainer-stack-utils/common" "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -15,14 +17,17 @@ var loginCmd = &cobra.Command{ Short: "Log in to a Portainer instance", Run: func(cmd *cobra.Command, args []string) { // Get auth token - client, err := common.GetDefaultClient() + portainerClient, err := common.GetDefaultClient() common.CheckError(err) user := viper.GetString("user") logrus.WithFields(logrus.Fields{ "user": user, }).Debug("Getting auth token") - authToken, err := client.AuthenticateUser() + authToken, err := portainerClient.AuthenticateUser(client.AuthenticateUserOptions{ + Username: viper.GetString("user"), + Password: viper.GetString("password"), + }) common.CheckError(err) if viper.GetBool("login.print") { From a98dbfdda2bff406f6e11a3f53a243d1cd396603 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Carlos=20Mej=C3=ADas=20Rodr=C3=ADguez?= Date: Mon, 26 Aug 2019 03:29:55 -0400 Subject: [PATCH 32/34] Enhance tests for Portainer client user authentication --- client/auth_test.go | 113 ++++++++++++++++++++++++++++++-------------- 1 file changed, 77 insertions(+), 36 deletions(-) diff --git a/client/auth_test.go b/client/auth_test.go index 9e935f8..8b8f36d 100644 --- a/client/auth_test.go +++ b/client/auth_test.go @@ -9,43 +9,84 @@ import ( "github.com/stretchr/testify/assert" ) -func TestClientAuthenticates(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - var body map[string]interface{} - err := readRequestBodyAsJSON(req, &body) +func Test_portainerClientImp_AuthenticateUser(t *testing.T) { + type fields struct { + server *httptest.Server + } + type args struct { + options AuthenticateUserOptions + } + tests := []struct { + name string + fields fields + args args + wantToken string + wantErr bool + }{ + { + name: "valid username and password authenticates (happy path)", + fields: fields{ + server: httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + assert.Equal(t, req.Method, http.MethodPost) + assert.Equal(t, req.RequestURI, "/api/auth") - assert.Equal(t, req.Method, http.MethodPost) - assert.Equal(t, req.RequestURI, "/api/auth") - assert.NotNil(t, req.Header["Content-Type"]) - assert.NotNil(t, req.Header["Content-Type"][0]) - assert.Equal(t, req.Header["Content-Type"][0], "application/json") - assert.NotNil(t, req.Header["User-Agent"]) - assert.NotNil(t, req.Header["User-Agent"][0]) - assert.Equal(t, req.Header["User-Agent"][0], "GE007") - assert.Nil(t, err) - assert.NotNil(t, body["Username"]) - assert.Equal(t, body["Username"], "admin") - assert.NotNil(t, body["Password"]) - assert.Equal(t, body["Password"], "a") + var body map[string]interface{} + err := readRequestBodyAsJSON(req, &body) + assert.Nil(t, err) - writeResponseBodyAsJSON(w, map[string]interface{}{ - "jwt": "somerandomtoken", + assert.NotNil(t, body["Username"]) + assert.Equal(t, body["Username"], "admin") + assert.NotNil(t, body["Password"]) + assert.Equal(t, body["Password"], "a") + + writeResponseBodyAsJSON(w, map[string]interface{}{ + "jwt": "token", + }) + })), + }, + args: args{ + options: AuthenticateUserOptions{ + Username: "admin", + Password: "a", + }, + }, + wantToken: "token", + }, + { + name: "invalid username and password does not authenticate", + fields: fields{ + server: httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.WriteHeader(http.StatusUnprocessableEntity) + writeResponseBodyAsJSON(w, map[string]interface{}{ + "Err": "Invalid credentials", + "Details": "Unauthorized", + }) + })), + }, + args: args{ + options: AuthenticateUserOptions{ + Username: "admin", + Password: "a", + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.fields.server.Start() + defer tt.fields.server.Close() + + apiURL, _ := url.Parse(tt.fields.server.URL + "/api/") + + n := &portainerClientImp{ + httpClient: tt.fields.server.Client(), + url: apiURL, + } + + gotToken, err := n.AuthenticateUser(tt.args.options) + assert.Equal(t, tt.wantErr, err != nil) + assert.Equal(t, tt.wantToken, gotToken) }) - })) - defer ts.Close() - - apiURL, _ := url.Parse(ts.URL + "/api/") - - customClient := NewClient(ts.Client(), Config{ - URL: apiURL, - User: "admin", - Password: "a", - UserAgent: "GE007", - }) - token, err := customClient.AuthenticateUser(AuthenticateUserOptions{ - Username: "admin", - Password: "a", - }) - assert.Nil(t, err) - assert.Equal(t, token, "somerandomtoken") + } } From fa0071883d9db0c68bac1c10925e00e6f7597e33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Carlos=20Mej=C3=ADas=20Rodr=C3=ADguez?= Date: Mon, 26 Aug 2019 04:20:45 -0400 Subject: [PATCH 33/34] Add tests for portainerClientImp.doJSON() --- client/client.go | 4 +- client/client_test.go | 103 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 1 deletion(-) diff --git a/client/client.go b/client/client.go index 7e14707..3dcfe6f 100644 --- a/client/client.go +++ b/client/client.go @@ -123,8 +123,8 @@ func (n *portainerClientImp) do(uri, method string, requestBody io.Reader, heade // Do a JSON http request func (n *portainerClientImp) doJSON(uri, method string, headers http.Header, requestBody interface{}, responseBody interface{}) error { + // Encode request body, if any var body io.Reader - if requestBody != nil { reqBodyBytes, err := json.Marshal(requestBody) if err != nil { @@ -133,6 +133,7 @@ func (n *portainerClientImp) doJSON(uri, method string, headers http.Header, req body = bytes.NewReader(reqBodyBytes) } + // Set content type header headers.Set("Content-Type", "application/json") resp, err := n.do(uri, method, body, headers) @@ -140,6 +141,7 @@ func (n *portainerClientImp) doJSON(uri, method string, headers http.Header, req return err } + // Decode response body, if any if responseBody != nil { d := json.NewDecoder(resp.Body) err := d.Decode(responseBody) diff --git a/client/client_test.go b/client/client_test.go index 275f3df..3f2537f 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -145,3 +145,106 @@ func Test_portainerClientImp_do(t *testing.T) { }) } } + +func Test_portainerClientImp_doJSON(t *testing.T) { + type fields struct { + httpClient *http.Client + url *url.URL + server *httptest.Server + } + type args struct { + uri string + method string + headers http.Header + requestBody interface{} + responseBody interface{} + } + tests := []struct { + name string + fields fields + args args + wantRespBody interface{} + wantErr bool + }{ + { + name: "request is made with application/json content type and expected JSON object as body", + fields: fields{ + server: httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + assert.Equal(t, "/api/stacks", req.RequestURI) + assert.Equal(t, http.MethodPost, req.Method) + assert.Equal(t, "application/json", req.Header.Get("Content-Type")) + + var body map[string]interface{} + err := readRequestBodyAsJSON(req, &body) + assert.Nil(t, err) + + assert.Equal(t, map[string]interface{}{ + "key1": "value1", + }, body) + + writeResponseBodyAsJSON(w, map[string]interface{}{ + "key2": "value2", + }) + })), + }, + args: args{ + uri: "stacks", + method: http.MethodPost, + headers: http.Header{}, + requestBody: map[string]interface{}{ + "key1": "value1", + }, + responseBody: map[string]interface{}{}, + }, + wantRespBody: map[string]interface{}{ + "key2": "value2", + }, + }, + { + name: "invalid JSON object as request body causes an error", + fields: fields{ + server: httptest.NewUnstartedServer(nil), + }, + args: args{ + uri: "stacks", + method: http.MethodPost, + headers: http.Header{}, + requestBody: func() {}, + }, + wantErr: true, + }, + { + name: "invalid JSON object as response body causes an error", + fields: fields{ + server: httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + fmt.Fprint(w, "not a JSON object") + })), + }, + args: args{ + uri: "stacks", + method: http.MethodPost, + headers: http.Header{}, + responseBody: map[string]interface{}{}, + }, + wantRespBody: map[string]interface{}{}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.fields.server.Start() + defer tt.fields.server.Close() + + apiURL, _ := url.Parse(tt.fields.server.URL + "/api/") + + n := &portainerClientImp{ + httpClient: tt.fields.server.Client(), + url: apiURL, + } + + err := n.doJSON(tt.args.uri, tt.args.method, tt.args.headers, &tt.args.requestBody, &tt.args.responseBody) + assert.Equal(t, tt.wantErr, err != nil) + assert.Equal(t, tt.wantRespBody, tt.args.responseBody) + }) + } +} From 2fe292bb6409857e201b6930efcd2f611c2186a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Carlos=20Mej=C3=ADas=20Rodr=C3=ADguez?= Date: Mon, 26 Aug 2019 22:56:15 -0400 Subject: [PATCH 34/34] Add tests for portainerClientImp.doJSONWithToken() --- client/client_test.go | 151 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 151 insertions(+) diff --git a/client/client_test.go b/client/client_test.go index 3f2537f..c69af0c 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -248,3 +248,154 @@ func Test_portainerClientImp_doJSON(t *testing.T) { }) } } + +func Test_portainerClientImp_doJSONWithToken(t *testing.T) { + type fields struct { + httpClient *http.Client + url *url.URL + user string + password string + token string + server *httptest.Server + } + type args struct { + uri string + method string + headers http.Header + requestBody interface{} + responseBody interface{} + } + tests := []struct { + name string + fields fields + args args + wantRespBody interface{} + wantErr bool + }{ + { + name: "when a token is present, it is used", + fields: fields{ + token: "token", + server: httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + assert.Equal(t, "/api/stacks", req.RequestURI) + assert.Equal(t, http.MethodPost, req.Method) + assert.Equal(t, "Bearer token", req.Header.Get("Authorization")) + + var body map[string]interface{} + err := readRequestBodyAsJSON(req, &body) + assert.Nil(t, err) + + assert.Equal(t, map[string]interface{}{ + "key1": "value1", + }, body) + + writeResponseBodyAsJSON(w, map[string]interface{}{ + "key2": "value2", + }) + })), + }, + args: args{ + uri: "stacks", + method: http.MethodPost, + headers: http.Header{}, + requestBody: map[string]interface{}{ + "key1": "value1", + }, + responseBody: map[string]interface{}{}, + }, + wantRespBody: map[string]interface{}{ + "key2": "value2", + }, + }, + { + name: "when a token is not present, a new one is obtained and used", + fields: fields{ + user: "admin", + password: "a", + server: httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + switch req.RequestURI { + case "/api/auth": + assert.Equal(t, "", req.Header.Get("Authorization")) + + writeResponseBodyAsJSON(w, map[string]interface{}{ + "jwt": "token", + }) + case "/api/stacks": + assert.Equal(t, "/api/stacks", req.RequestURI) + assert.Equal(t, http.MethodPost, req.Method) + assert.Equal(t, "Bearer token", req.Header.Get("Authorization")) + + var body map[string]interface{} + err := readRequestBodyAsJSON(req, &body) + assert.Nil(t, err) + + assert.Equal(t, map[string]interface{}{ + "key1": "value1", + }, body) + + writeResponseBodyAsJSON(w, map[string]interface{}{ + "key2": "value2", + }) + } + })), + }, + args: args{ + uri: "stacks", + method: http.MethodPost, + headers: http.Header{}, + requestBody: map[string]interface{}{ + "key1": "value1", + }, + responseBody: map[string]interface{}{}, + }, + wantRespBody: map[string]interface{}{ + "key2": "value2", + }, + }, + { + name: "when authentication error occurs, an error is returned", + fields: fields{ + user: "admin", + password: "a", + server: httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.WriteHeader(http.StatusUnprocessableEntity) + writeResponseBodyAsJSON(w, map[string]interface{}{ + "Err": "Invalid credentials", + "Details": "Unauthorized", + }) + })), + }, + args: args{ + uri: "stacks", + method: http.MethodPost, + headers: http.Header{}, + requestBody: map[string]interface{}{ + "key1": "value1", + }, + responseBody: map[string]interface{}{}, + }, + wantRespBody: map[string]interface{}{}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.fields.server.Start() + defer tt.fields.server.Close() + + apiURL, _ := url.Parse(tt.fields.server.URL + "/api/") + + n := &portainerClientImp{ + httpClient: tt.fields.server.Client(), + url: apiURL, + user: tt.fields.user, + password: tt.fields.password, + token: tt.fields.token, + } + + err := n.doJSONWithToken(tt.args.uri, tt.args.method, tt.args.headers, &tt.args.requestBody, &tt.args.responseBody) + assert.Equal(t, tt.wantErr, err != nil) + assert.Equal(t, tt.wantRespBody, tt.args.responseBody) + }) + } +}