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) + }) + } +}