From 1ab18c698f17791aaadef41460c4191460e8029a Mon Sep 17 00:00:00 2001 From: Dean Date: Fri, 28 Oct 2022 11:20:07 +0200 Subject: [PATCH] Remove old APIs from client, promote RMv2 and Members v2 to productive services (#35) * Add support for new RMv2 + Members v2 APIs * Keep previous RMv1 projects 'get' functionality under archived * Major restructure of Client config * Removal of Auth client Co-authored-by: Dean Oren --- Makefile | 5 + README.md | 22 +- auth.go | 135 ------- auth_test.go | 199 ----------- client.go | 61 ++-- client_test.go | 46 +-- config.go | 62 +--- config_test.go | 93 +---- examples/bucket/bucket.go | 5 +- examples/retry/retry.go | 5 +- internal/clients/clients.go | 28 +- internal/common/client.go | 1 - pkg/api/v1/costs/costs.go | 6 +- pkg/api/v1/costs/costs_test.go | 15 +- pkg/api/v1/projects/projects.go | 331 ------------------ pkg/api/v1/projects/projects_test.go | 285 --------------- pkg/api/v1/projects/validate.go | 41 --- .../resource-management/projects/projects.go | 67 ++++ .../projects/projects_test.go | 66 ++++ .../resource_management.go | 21 ++ pkg/api/v1/roles/roles.go | 269 -------------- pkg/api/v1/users/users.go | 99 ------ pkg/api/v1/users/users_test.go | 121 ------- pkg/api/v2/membership/members/members_test.go | 4 +- .../organizations/organization.go | 2 +- .../organizations/organization_test.go | 10 +- .../projects/project.go | 55 ++- .../projects/project_test.go | 178 ++++++++-- .../projects/validate.go | 0 .../resource_management.go} | 12 +- pkg/consts/apis.go | 12 +- pkg/consts/consts.go | 9 +- 32 files changed, 452 insertions(+), 1813 deletions(-) delete mode 100644 auth.go delete mode 100644 auth_test.go delete mode 100644 pkg/api/v1/projects/projects.go delete mode 100644 pkg/api/v1/projects/projects_test.go delete mode 100644 pkg/api/v1/projects/validate.go create mode 100644 pkg/api/v1/resource-management/projects/projects.go create mode 100644 pkg/api/v1/resource-management/projects/projects_test.go create mode 100644 pkg/api/v1/resource-management/resource_management.go delete mode 100644 pkg/api/v1/roles/roles.go delete mode 100644 pkg/api/v1/users/users.go delete mode 100644 pkg/api/v1/users/users_test.go rename pkg/api/v2/{resource-manager => resource-management}/organizations/organization.go (96%) rename pkg/api/v2/{resource-manager => resource-management}/organizations/organization_test.go (83%) rename pkg/api/v2/{resource-manager => resource-management}/projects/project.go (77%) rename pkg/api/v2/{resource-manager => resource-management}/projects/project_test.go (68%) rename pkg/api/v2/{resource-manager => resource-management}/projects/validate.go (100%) rename pkg/api/v2/{resource-manager/resource_manager.go => resource-management/resource_management.go} (70%) diff --git a/Makefile b/Makefile index 9b005bfa..e22f142d 100644 --- a/Makefile +++ b/Makefile @@ -4,5 +4,10 @@ test: @go test $(TEST) || exit 1 @echo $(TEST) | xargs -t -n4 go test $(TESTARGS) -timeout=30s -parallel=4 +coverage: + @go test ./... -coverprofile cover.out + @go tool cover -func cover.out | grep total: + @rm cover.out + quality: @goreportcard-cli -v ./... diff --git a/README.md b/README.md index 43bbd944..0dfbdb62 100644 --- a/README.md +++ b/README.md @@ -14,12 +14,11 @@ To install the latest stable release, run: go get github.com/SchwarzIT/community-stackit-go-client@latest ``` +## Usage Example -## Usage example -To get started, a Service Account[^1] and a Customer Account[^2] must be in place - -If you're not sure how to get this information, please contact [STACKIT support](https://support.stackit.cloud) +In order to use the client, a STACKIT Service Account [must be created](https://api.stackit.schwarz/service-account/openapi.v1.html#operation/post-projects-projectId-service-accounts-v2) and have relevant roles [assigned to it](https://api.stackit.schwarz/membership-service/openapi.v1.html#operation/post-organizations-organizationId-projects-projectId-roles-roleName-service-accounts).
+For further assistance, please contact [STACKIT support](https://support.stackit.cloud) ```Go package main @@ -35,9 +34,8 @@ import ( func main() { ctx := context.Background() c, err := client.New(ctx, &client.Config{ - ServiceAccountID: os.Getenv("STACKIT_SERVICE_ACCOUNT_ID"), - Token: os.Getenv("STACKIT_SERVICE_ACCOUNT_TOKEN"), - OrganizationID: os.Getenv("STACKIT_ORGANIZATION_ID"), + ServiceAccountEmail: os.Getenv("STACKIT_SERVICE_ACCOUNT_EMAIL"), + ServiceAccountToken: os.Getenv("STACKIT_SERVICE_ACCOUNT_TOKEN"), }) if err != nil { panic(err) @@ -64,7 +62,7 @@ func main() { Further usage examples can be found in [`terraform-provider-stackit`](https://github.com/SchwarzIT/terraform-provider-stackit) -## Auto retry +### Enabling auto-retry The client can automatically retry failed calls using `pkg/retry` @@ -75,11 +73,3 @@ c, _ := client.New(context.Background(), &client.Config{}) c.SetRetry(retry.New()) ``` - -[^1]: In order to use the client, a Service Account and Token must be created using [Service Account API](https://api.stackit.schwarz/service-account/openapi.v1.html#operation/post-projects-projectId-service-accounts-v2)
-After creation, assign roles to the Service Account using [Membership API](https://api.stackit.schwarz/membership-service/openapi.v1.html#operation/post-organizations-organizationId-projects-projectId-roles-roleName-service-accounts)
-If your Service Account needs to operate outside the scope of your project, you may need to contact STACKIT to assign further permissions - -
- -[^2]: The Customer Account ID is also referred to as Organization ID diff --git a/auth.go b/auth.go deleted file mode 100644 index c14ff651..00000000 --- a/auth.go +++ /dev/null @@ -1,135 +0,0 @@ -// the auth client is a separate client consuming STACKIT's auth API -// it was developed for the purpose of migrating Schwarz IT KG users into STACKIT -// but this behavior is deprecated and will soon be removed - -package client - -import ( - "context" - "encoding/json" - "net/http" - "net/http/httptest" - "net/url" - "strings" - "time" - - "github.com/SchwarzIT/community-stackit-go-client/pkg/validate" - "golang.org/x/oauth2" -) - -// AuthClient manages communication with Auth API -type AuthClient struct { - Client *http.Client - Config *AuthConfig -} - -// NewAuth returns a new Auth API client -func NewAuth(ctx context.Context, cfg *AuthConfig) (*AuthClient, error) { - if err := cfg.Validate(); err != nil { - return nil, err - } - - c := &AuthClient{Config: cfg} - return c.init(ctx), nil -} - -// init initializes the client -func (c *AuthClient) init(ctx context.Context) *AuthClient { - c.setHttpClient(ctx) - return c -} - -// setHttpClient creates the client's oauth client -func (c *AuthClient) setHttpClient(ctx context.Context) { - hcl := oauth2.NewClient(ctx, nil) - hcl.Timeout = time.Second * 10 - c.Client = hcl -} - -// Request creates an API request -func (c *AuthClient) Request(ctx context.Context, method, path string, body string) (*http.Request, error) { - if ctx != nil && ctx.Err() != nil { - return nil, ctx.Err() - } - rel := &url.URL{Path: path} - u := c.Config.BaseUrl.ResolveReference(rel) - payload := strings.NewReader(body) - req, err := http.NewRequestWithContext(ctx, method, u.String(), payload) - if err != nil { - return nil, err - } - if body != "" { - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - } - req.Header.Set("Accept", "application/json") - return req, nil -} - -// Do performs the request and decodes the response if given interface != nil -func (c *AuthClient) Do(req *http.Request, v interface{}, errorHandlers ...func(*http.Response) error) (*http.Response, error) { - resp, err := c.Client.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - // handle errors in the response - if len(errorHandlers) == 0 { - errorHandlers = append(errorHandlers, validate.DefaultResponseErrorHandler) - } - for _, fn := range errorHandlers { - if err := fn(resp); err != nil { - return resp, err - } - } - - // parse response JSON - if v != nil { - err = json.NewDecoder(resp.Body).Decode(v) - } - return resp, err -} - -// AuthAPIClientCredentialFlowRes response structure for client credentials flow -type AuthAPIClientCredentialFlowRes struct { - AccessToken string `json:"access_token"` - TokenType string `json:"token_type"` - ExpiresIn int `json:"expires_in"` - Scope string `json:"scope"` - JTI string `json:"jti"` -} - -// GetToken returns a token from Auth API -func (c *AuthClient) GetToken(ctx context.Context) (res AuthAPIClientCredentialFlowRes, err error) { - params := url.Values{} - params.Add("client_id", c.Config.ClientID) - params.Add("client_secret", c.Config.ClientSecret) - params.Add("grant_type", "client_credentials") - body := params.Encode() - - req, err := c.Request(ctx, http.MethodPost, "/oauth/token", body) - if err != nil { - return - } - - _, err = c.Do(req, &res) - return -} - -// MockAuthServer mocks an authentication server -// and returns an auth client pointing to it, mux, teardown function and an error indicator -func MockAuthServer() (c *AuthClient, mux *http.ServeMux, teardown func(), err error) { - mux = http.NewServeMux() - server := httptest.NewServer(mux) - teardown = server.Close - - u, _ := url.Parse(server.URL) - - c, err = NewAuth(context.Background(), &AuthConfig{ - BaseUrl: u, - ClientID: "id", - ClientSecret: "secret", - }) - - return -} diff --git a/auth_test.go b/auth_test.go deleted file mode 100644 index 7441d22a..00000000 --- a/auth_test.go +++ /dev/null @@ -1,199 +0,0 @@ -package client - -import ( - "context" - "encoding/json" - "fmt" - "io" - "log" - "net/http" - "net/url" - "reflect" - "testing" - - "github.com/SchwarzIT/community-stackit-go-client/pkg/consts" -) - -func TestNewAuth(t *testing.T) { - cfg := &AuthConfig{ClientID: consts.SCHWARZ_ORGANIZATION_ID, ClientSecret: "secret"} - type args struct { - ctx context.Context - cfg *AuthConfig - } - tests := []struct { - name string - args args - want *AuthClient - wantErr bool - }{ - {"no client ID", args{context.Background(), &AuthConfig{}}, &AuthClient{}, true}, - {"no client secret", args{context.Background(), &AuthConfig{ClientID: consts.SCHWARZ_ORGANIZATION_ID}}, &AuthClient{}, true}, - {"all ok", args{context.Background(), cfg}, &AuthClient{Config: cfg}, false}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := NewAuth(tt.args.ctx, tt.args.cfg) - if (err != nil) != tt.wantErr { - t.Errorf("NewAuth() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !tt.wantErr && !reflect.DeepEqual(got.Config, tt.want.Config) { - t.Errorf("NewAuth() = %v, want %v", got, tt.want) - } - }) - } -} - -func TestAuthClient_Request(t *testing.T) { - c, err := NewAuth(context.Background(), &AuthConfig{ClientID: consts.SCHWARZ_ORGANIZATION_ID, ClientSecret: "secret"}) - if err != nil { - t.Error(err) - } - - type args struct { - ctx context.Context - method string - path string - body string - } - tests := []struct { - name string - args args - want *http.Request - wantErr bool - }{ - {"bad context", args{nil, "something", "my-path", ""}, &http.Request{}, true}, - {"bad method", args{context.Background(), "something", "my-path", ""}, &http.Request{}, true}, - {"all ok", args{context.Background(), http.MethodGet, "my-path", ""}, &http.Request{Method: http.MethodGet, URL: &url.URL{Path: "/my-path"}}, false}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := c.Request(tt.args.ctx, tt.args.method, tt.args.path, tt.args.body) - if !tt.wantErr { - if err != nil { - t.Error(err) - } - if tt.want.Method != got.Method { - t.Error("wrong method") - } - if tt.want.URL.Path != got.URL.Path { - t.Error("wrong url path", tt.want.URL.Path, got.URL.Path) - } - } - }) - } -} - -func TestAuthClient_Do(t *testing.T) { - type Test struct { - Payload string `json:"payload,omitempty"` - } - - c, mux, teardown, err := MockAuthServer() - defer teardown() - if err != nil { - t.Errorf("error from mock.AuthServer: %s", err.Error()) - } - - mux.HandleFunc("/echo", func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - - b, err := io.ReadAll(r.Body) - if err != nil { - t.Fatal(err) - } - fmt.Fprint(w, string(b)) - }) - - p := Test{Payload: "my-payload"} - body, err := json.Marshal(p) - if err != nil { - t.Errorf("marshal err: %v", err) - return - } - - req, err := c.Request(context.Background(), http.MethodPost, "/echo", string(body)) - if err != nil { - t.Errorf("new request: %v", err) - return - } - - var got Test - _, err = c.Do(req, &got) - if err != nil { - t.Errorf("do request: %v", err) - } - - want := p - if !reflect.DeepEqual(got, want) { - t.Errorf("got = %v, want %v", got, want) - } - - req.URL.Path = "/blah" - if _, err = c.Do(req, &got); err == nil { - t.Error("expected error over path") - } - - ctx2, cancel := context.WithTimeout(context.TODO(), 0) - defer cancel() - req = req.WithContext(ctx2) - if _, err = c.Do(req, &got); err == nil { - t.Error("expected error over context timeout") - } - -} - -func TestAuthClient_GetToken(t *testing.T) { - c, mux, teardown, err := MockAuthServer() - if err != nil { - t.Errorf("error from mock.AuthServer: %s", err.Error()) - } - defer teardown() - - mux.HandleFunc("/oauth/token", func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - - r.ParseForm() - cid := r.Form.Get("client_id") - if cid != "id" { - t.Error("bad client id provided") - } - csc := r.Form.Get("client_secret") - if csc != "secret" { - t.Error("bad client secret provided") - } - cgt := r.Form.Get("grant_type") - if cgt != "client_credentials" { - t.Error("bad grant type provided") - } - - b, err := json.Marshal(AuthAPIClientCredentialFlowRes{ - AccessToken: "my-access-token", - }) - if err != nil { - log.Fatalf("json response marshal: %v", err) - } - fmt.Fprint(w, string(b)) - }) - - got, err := c.GetToken(context.Background()) - if err != nil { - t.Fatal(err) - } - - want := AuthAPIClientCredentialFlowRes{ - AccessToken: "my-access-token", - } - - if !reflect.DeepEqual(got, want) { - t.Errorf("got = %v, want %v", got, want) - } - - ctx2, cancel := context.WithTimeout(context.TODO(), 0) - defer cancel() - if _, err = c.GetToken(ctx2); err == nil { - t.Error("expected error over context timeout") - } -} diff --git a/client.go b/client.go index d3edf52e..1a8f14d4 100644 --- a/client.go +++ b/client.go @@ -21,12 +21,9 @@ import ( "github.com/SchwarzIT/community-stackit-go-client/pkg/api/v1/mongodb" objectstorage "github.com/SchwarzIT/community-stackit-go-client/pkg/api/v1/object-storage" "github.com/SchwarzIT/community-stackit-go-client/pkg/api/v1/postgres" - "github.com/SchwarzIT/community-stackit-go-client/pkg/api/v1/projects" - "github.com/SchwarzIT/community-stackit-go-client/pkg/api/v1/roles" - "github.com/SchwarzIT/community-stackit-go-client/pkg/api/v1/users" + resourceManagementV1 "github.com/SchwarzIT/community-stackit-go-client/pkg/api/v1/resource-management" "github.com/SchwarzIT/community-stackit-go-client/pkg/api/v2/membership" - resourceManager "github.com/SchwarzIT/community-stackit-go-client/pkg/api/v2/resource-manager" - "github.com/SchwarzIT/community-stackit-go-client/pkg/consts" + resourceManagement "github.com/SchwarzIT/community-stackit-go-client/pkg/api/v2/resource-management" "github.com/SchwarzIT/community-stackit-go-client/pkg/retry" "github.com/SchwarzIT/community-stackit-go-client/pkg/validate" "golang.org/x/oauth2" @@ -45,6 +42,9 @@ type Client struct { // Incubator - services under development or currently being tested // not ready for production usage Incubator IncubatorServices + + // Archived - for services that are phased out + Archived ArchivedServices } // New returns a new client @@ -65,21 +65,23 @@ func New(ctx context.Context, cfg *Config) (*Client, error) { // ProductiveServices is the struct representing all productive services type ProductiveServices struct { - Argus *argus.ArgusService - Costs *costs.CostsService - Kubernetes *kubernetes.KubernetesService - ObjectStorage *objectstorage.ObjectStorageService - Projects *projects.ProjectService - Roles *roles.RolesService - Users *users.UsersService + Argus *argus.ArgusService + Costs *costs.CostsService + Kubernetes *kubernetes.KubernetesService + Membership *membership.MembershipService + ObjectStorage *objectstorage.ObjectStorageService + ResourceManagement *resourceManagement.ResourceManagementService } // IncubatorServices is the struct representing all services that are under development type IncubatorServices struct { - Membership *membership.MembershipService - MongoDB *mongodb.MongoDBService - Postgres *postgres.PostgresService - ResourceManager *resourceManager.ResourceManagerService + MongoDB *mongodb.MongoDBService + Postgres *postgres.PostgresService +} + +// ArchivedServices is used for services that are being phased out +type ArchivedServices struct { + ResourceManagementV1 *resourceManagementV1.ResourceManagementV1Service } // init initializes the client and its services and returns the client @@ -90,17 +92,14 @@ func (c *Client) init() *Client { c.Argus = argus.New(c) c.Costs = costs.New(c) c.Kubernetes = kubernetes.New(c) + c.Membership = membership.New(c) c.ObjectStorage = objectstorage.New(c) - c.Projects = projects.New(c) - c.Roles = roles.New(c) - c.Users = users.New(c) + c.ResourceManagement = resourceManagement.New(c) // init incubator services c.Incubator = IncubatorServices{ - Membership: membership.New(c), - MongoDB: mongodb.New(c), - Postgres: postgres.New(c), - ResourceManager: resourceManager.New(c), + MongoDB: mongodb.New(c), + Postgres: postgres.New(c), } return c } @@ -114,13 +113,13 @@ func (c *Client) GetConfig() *Config { } func (c *Client) SetToken(token string) { - c.config.Token = token + c.config.ServiceAccountToken = token } // setHttpClient creates the client's oauth client func (c *Client) setHttpClient(ctx context.Context) { ts := oauth2.StaticTokenSource( - &oauth2.Token{AccessToken: c.config.Token}, + &oauth2.Token{AccessToken: c.config.ServiceAccountToken}, ) hcl := oauth2.NewClient(ctx, ts) hcl.Timeout = time.Second * 10 @@ -190,11 +189,6 @@ func (c *Client) do(req *http.Request, v interface{}, errorHandlers ...func(*htt return resp, err } -// OrganizationID returns the organization ID defined in the configuration -func (c *Client) OrganizationID() string { - return c.config.OrganizationID -} - // Retry returns the defined retry func (c *Client) Retry() *retry.Retry { return c.retry @@ -215,10 +209,9 @@ func MockServer() (c *Client, mux *http.ServeMux, teardown func(), err error) { u, _ := url.Parse(server.URL) c, err = New(context.Background(), &Config{ - BaseUrl: u, - Token: "token", - ServiceAccountID: "sa-id", - OrganizationID: consts.SCHWARZ_ORGANIZATION_ID, + BaseUrl: u, + ServiceAccountToken: "token", + ServiceAccountEmail: "sa-id", }) return diff --git a/client_test.go b/client_test.go index bb6f7e92..79745d6e 100644 --- a/client_test.go +++ b/client_test.go @@ -11,15 +11,13 @@ import ( "testing" "time" - "github.com/SchwarzIT/community-stackit-go-client/pkg/consts" "github.com/SchwarzIT/community-stackit-go-client/pkg/retry" ) func TestNew(t *testing.T) { cfg := &Config{ - Token: "token", - ServiceAccountID: "sa-id", - OrganizationID: consts.SCHWARZ_ORGANIZATION_ID, + ServiceAccountToken: "token", + ServiceAccountEmail: "sa-id", } type args struct { ctx context.Context @@ -32,8 +30,7 @@ func TestNew(t *testing.T) { wantErr bool }{ {"no token", args{context.Background(), &Config{}}, &Client{}, true}, - {"no sa id", args{context.Background(), &Config{Token: "token"}}, &Client{}, true}, - {"no org id", args{context.Background(), &Config{Token: "token", ServiceAccountID: "sa-id"}}, &Client{}, true}, + {"no sa id", args{context.Background(), &Config{ServiceAccountToken: "token"}}, &Client{}, true}, {"all ok", args{context.Background(), cfg}, &Client{config: cfg}, false}, } for _, tt := range tests { @@ -52,9 +49,8 @@ func TestNew(t *testing.T) { func TestClient_Request(t *testing.T) { cfg := &Config{ - Token: "token", - ServiceAccountID: "sa-id", - OrganizationID: consts.SCHWARZ_ORGANIZATION_ID, + ServiceAccountToken: "token", + ServiceAccountEmail: "sa-id", } c, err := New(context.Background(), cfg) if err != nil { @@ -162,34 +158,6 @@ func TestClient_Do(t *testing.T) { } -func TestClient_OrganizationID(t *testing.T) { - cfg := &Config{ - Token: "token", - ServiceAccountID: "sa-id", - OrganizationID: consts.SCHWARZ_ORGANIZATION_ID, - } - type fields struct { - config *Config - } - tests := []struct { - name string - fields fields - want string - }{ - {"test-1", fields{config: cfg}, cfg.OrganizationID}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - c := &Client{ - config: tt.fields.config, - } - if got := c.OrganizationID(); got != tt.want { - t.Errorf("OrganizationID() = %v, want %v", got, tt.want) - } - }) - } -} - func TestClient_GetHTTPClient(t *testing.T) { type fields struct { client *http.Client @@ -258,13 +226,13 @@ func TestClient_SetToken(t *testing.T) { type args struct { token string } - c := &Config{Token: "abc"} + c := &Config{ServiceAccountToken: "abc"} tests := []struct { name string fields fields args args }{ - {"all ok", fields{config: c}, args{token: c.Token}}, + {"all ok", fields{config: c}, args{token: c.ServiceAccountToken}}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/config.go b/config.go index 113cdad9..08bc6a48 100644 --- a/config.go +++ b/config.go @@ -8,15 +8,13 @@ import ( "net/url" "github.com/SchwarzIT/community-stackit-go-client/pkg/consts" - "github.com/SchwarzIT/community-stackit-go-client/pkg/validate" ) // Config is the STACKIT client configuration type Config struct { - BaseUrl *url.URL - Token string - ServiceAccountID string - OrganizationID string + BaseUrl *url.URL + ServiceAccountToken string + ServiceAccountEmail string } // Validate verifies that the given config is valid @@ -25,18 +23,13 @@ func (c *Config) Validate() error { c.SetURL("") // set default } - if c.Token == "" { - return errors.New("STACKIT API: access token is empty") + if c.ServiceAccountToken == "" { + return errors.New("Service Account Access Token cannot be empty") } - if c.ServiceAccountID == "" { - return errors.New("STACKIT API: service account ID cannot be empty") + if c.ServiceAccountEmail == "" { + return errors.New("Service Account Email cannot be empty") } - - if err := validate.OrganizationID(c.OrganizationID); err != nil { - return err - } - return nil } @@ -53,44 +46,3 @@ func (c *Config) SetURL(value string) error { c.BaseUrl = u return nil } - -// Auth Config -// Warning: This code is deprecated and will be removed - -// AuthConfig holds information for using auth API -type AuthConfig struct { - BaseUrl *url.URL - ClientID string - ClientSecret string -} - -// Validate verifies that the given config is valid -func (c *AuthConfig) Validate() error { - if c.BaseUrl == nil { - c.SetURL("") // set default - } - - if c.ClientID == "" { - return errors.New("auth API: client ID is empty") - } - - if c.ClientSecret == "" { - return errors.New("auth API: client Secret is empty") - } - - return nil -} - -// SetURL sets a given url string as the base url in the config -// if the given value is empty, the default auth base URL will be used -func (c *AuthConfig) SetURL(value string) error { - if value == "" { - value = consts.DEFAULT_AUTH_BASE_URL - } - u, err := url.Parse(value) - if err != nil { - return err - } - c.BaseUrl = u - return nil -} diff --git a/config_test.go b/config_test.go index efd53d16..4785cc1c 100644 --- a/config_test.go +++ b/config_test.go @@ -3,16 +3,13 @@ package client import ( "net/url" "testing" - - "github.com/SchwarzIT/community-stackit-go-client/pkg/consts" ) func TestConfig_Validate(t *testing.T) { type fields struct { - BaseUrl *url.URL - Token string - ServiceAccountID string - OrganizationID string + BaseUrl *url.URL + Token string + ServiceAccountEmail string } tests := []struct { name string @@ -21,16 +18,14 @@ func TestConfig_Validate(t *testing.T) { }{ {"empty token", fields{}, true}, {"empty service account id", fields{Token: "a"}, true}, - {"empty org id", fields{Token: "a", ServiceAccountID: consts.SCHWARZ_ORGANIZATION_ID}, true}, - {"all ok", fields{Token: "a", ServiceAccountID: consts.SCHWARZ_ORGANIZATION_ID, OrganizationID: consts.SCHWARZ_ORGANIZATION_ID}, false}, + {"all ok", fields{Token: "a", ServiceAccountEmail: "b"}, false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := &Config{ - BaseUrl: tt.fields.BaseUrl, - Token: tt.fields.Token, - ServiceAccountID: tt.fields.ServiceAccountID, - OrganizationID: tt.fields.OrganizationID, + BaseUrl: tt.fields.BaseUrl, + ServiceAccountToken: tt.fields.Token, + ServiceAccountEmail: tt.fields.ServiceAccountEmail, } if err := c.Validate(); (err != nil) != tt.wantErr { t.Errorf("Config.Validate() error = %v, wantErr %v", err, tt.wantErr) @@ -41,10 +36,9 @@ func TestConfig_Validate(t *testing.T) { func TestConfig_SetURL(t *testing.T) { type fields struct { - BaseUrl *url.URL - Token string - ServiceAccountID string - OrganizationID string + BaseUrl *url.URL + Token string + ServiceAccountEmail string } type args struct { value string @@ -60,10 +54,9 @@ func TestConfig_SetURL(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := &Config{ - BaseUrl: tt.fields.BaseUrl, - Token: tt.fields.Token, - ServiceAccountID: tt.fields.ServiceAccountID, - OrganizationID: tt.fields.OrganizationID, + BaseUrl: tt.fields.BaseUrl, + ServiceAccountToken: tt.fields.Token, + ServiceAccountEmail: tt.fields.ServiceAccountEmail, } if err := c.SetURL(tt.args.value); (err != nil) != tt.wantErr { t.Errorf("Config.SetURL() error = %v, wantErr %v", err, tt.wantErr) @@ -71,63 +64,3 @@ func TestConfig_SetURL(t *testing.T) { }) } } - -func TestAuthConfig_Validate(t *testing.T) { - type fields struct { - BaseUrl *url.URL - ClientID string - ClientSecret string - } - tests := []struct { - name string - fields fields - wantErr bool - }{ - {"empty client ID", fields{}, true}, - {"empty client secret", fields{ClientID: consts.SCHWARZ_ORGANIZATION_ID}, true}, - {"all ok", fields{ClientID: consts.SCHWARZ_ORGANIZATION_ID, ClientSecret: "something"}, false}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - c := &AuthConfig{ - BaseUrl: tt.fields.BaseUrl, - ClientID: tt.fields.ClientID, - ClientSecret: tt.fields.ClientSecret, - } - if err := c.Validate(); (err != nil) != tt.wantErr { - t.Errorf("AuthConfig.Validate() error = %v, wantErr %v", err, tt.wantErr) - } - }) - } -} - -func TestAuthConfig_SetURL(t *testing.T) { - type fields struct { - BaseUrl *url.URL - ClientID string - ClientSecret string - } - type args struct { - value string - } - tests := []struct { - name string - fields fields - args args - wantErr bool - }{ - {"bad url", fields{}, args{"a@b!://#!@$!^&"}, true}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - c := &AuthConfig{ - BaseUrl: tt.fields.BaseUrl, - ClientID: tt.fields.ClientID, - ClientSecret: tt.fields.ClientSecret, - } - if err := c.SetURL(tt.args.value); (err != nil) != tt.wantErr { - t.Errorf("AuthConfig.SetURL() error = %v, wantErr %v", err, tt.wantErr) - } - }) - } -} diff --git a/examples/bucket/bucket.go b/examples/bucket/bucket.go index 2510bb09..bc7f7925 100644 --- a/examples/bucket/bucket.go +++ b/examples/bucket/bucket.go @@ -11,9 +11,8 @@ import ( func main() { ctx := context.Background() c, err := client.New(ctx, &client.Config{ - ServiceAccountID: os.Getenv("STACKIT_SERVICE_ACCOUNT_ID"), - Token: os.Getenv("STACKIT_SERVICE_ACCOUNT_TOKEN"), - OrganizationID: os.Getenv("STACKIT_ORGANIZATION_ID"), + ServiceAccountEmail: os.Getenv("STACKIT_SERVICE_ACCOUNT_EMAIL"), + ServiceAccountToken: os.Getenv("STACKIT_SERVICE_ACCOUNT_TOKEN"), }) if err != nil { panic(err) diff --git a/examples/retry/retry.go b/examples/retry/retry.go index 62750417..a0570d54 100644 --- a/examples/retry/retry.go +++ b/examples/retry/retry.go @@ -10,9 +10,8 @@ import ( func main() { c, err := client.New(context.Background(), &client.Config{ - ServiceAccountID: os.Getenv("STACKIT_SERVICE_ACCOUNT_ID"), - Token: os.Getenv("STACKIT_SERVICE_ACCOUNT_TOKEN"), - OrganizationID: os.Getenv("STACKIT_ORGANIZATION_ID"), + ServiceAccountEmail: os.Getenv("STACKIT_SERVICE_ACCOUNT_ID"), + ServiceAccountToken: os.Getenv("STACKIT_SERVICE_ACCOUNT_TOKEN"), }) if err != nil { panic(err) diff --git a/internal/clients/clients.go b/internal/clients/clients.go index c7ed11f1..48ea7664 100644 --- a/internal/clients/clients.go +++ b/internal/clients/clients.go @@ -5,8 +5,7 @@ import ( "errors" "os" - "github.com/SchwarzIT/community-stackit-go-client" - "github.com/SchwarzIT/community-stackit-go-client/pkg/consts" + client "github.com/SchwarzIT/community-stackit-go-client" ) // LocalClient is an implementation of the client @@ -24,28 +23,7 @@ func LocalClient() (*client.Client, error) { } return client.New(context.Background(), &client.Config{ - ServiceAccountID: aid, - Token: ato, - OrganizationID: consts.SCHWARZ_ORGANIZATION_ID, - }) -} - -// LocalAuthClient is an implementation of the auth client -// relying on env variables for initialization -// the env vars are: STACKIT_AUTH_CLIENT_ID, STACKIT_AUTH_CLIENT_SECRET -func LocalAuthClient() (*client.AuthClient, error) { - aci := os.Getenv("STACKIT_AUTH_CLIENT_ID") - if aci == "" { - return nil, errors.New("STACKIT_AUTH_CLIENT_ID is missing from env variables") - } - - acs := os.Getenv("STACKIT_AUTH_CLIENT_SECRET") - if acs == "" { - return nil, errors.New("STACKIT_AUTH_CLIENT_SECRET is missing from env variables") - } - - return client.NewAuth(context.Background(), &client.AuthConfig{ - ClientID: aci, - ClientSecret: acs, + ServiceAccountEmail: aid, + ServiceAccountToken: ato, }) } diff --git a/internal/common/client.go b/internal/common/client.go index a097b8e9..ad7d0802 100644 --- a/internal/common/client.go +++ b/internal/common/client.go @@ -15,7 +15,6 @@ type Client interface { Request(ctx context.Context, method, path string, body []byte) (*http.Request, error) Do(req *http.Request, v interface{}, errorHandlers ...func(*http.Response) error) (*http.Response, error) Retry() *retry.Retry - OrganizationID() string } // Service is the struct every extending service is built on diff --git a/pkg/api/v1/costs/costs.go b/pkg/api/v1/costs/costs.go index 218f3fc5..e9d7339d 100644 --- a/pkg/api/v1/costs/costs.go +++ b/pkg/api/v1/costs/costs.go @@ -82,11 +82,12 @@ type ProjectDetailResponse struct { // See https://api.stackit.schwarz/costs-service/openapi.v1.html#operation/get-costs-reports-customer-account func (svc *CostsService) GetCustomerAccountCosts( ctx context.Context, + customerAccountID string, from, to time.Time, granularity, depth string, ) (*CustomerAccountResponse, error) { path := fmt.Sprintf(apiPath, - svc.Client.OrganizationID(), + customerAccountID, from.Format(timeFormat), to.Format(timeFormat), granularity, @@ -113,6 +114,7 @@ func (svc *CostsService) GetCustomerAccountCosts( // See https://api.stackit.schwarz/costs-service/openapi.v1.html#operation/get-costs-project func (svc *CostsService) GetProjectCosts( ctx context.Context, + customerAccountID, projectID string, from, to time.Time, granularity, depth string, @@ -123,7 +125,7 @@ func (svc *CostsService) GetProjectCosts( } path := fmt.Sprintf(apiPathProjects, - svc.Client.OrganizationID(), + customerAccountID, projectID, from.Format(timeFormat), to.Format(timeFormat), diff --git a/pkg/api/v1/costs/costs_test.go b/pkg/api/v1/costs/costs_test.go index a338ab0e..95651c9b 100644 --- a/pkg/api/v1/costs/costs_test.go +++ b/pkg/api/v1/costs/costs_test.go @@ -8,7 +8,7 @@ import ( "testing" "time" - "github.com/SchwarzIT/community-stackit-go-client" + client "github.com/SchwarzIT/community-stackit-go-client" "github.com/SchwarzIT/community-stackit-go-client/pkg/api/v1/costs" "github.com/SchwarzIT/community-stackit-go-client/pkg/consts" ) @@ -64,7 +64,7 @@ func TestCostsService_GetCustomerAccountCosts(t *testing.T) { from := time.Date(int(2022), time.May, int(22), int(0), int(0), int(0), int(0), time.UTC) to := time.Date(int(2022), time.June, int(21), int(0), int(0), int(0), int(0), time.UTC) - resp, err := costsSvc.GetCustomerAccountCosts(context.Background(), from, to, consts.COSTS_GRANULARITY_DAILY, consts.COSTS_DEPTH_AUTO) + resp, err := costsSvc.GetCustomerAccountCosts(context.Background(), "07a1ed91-2efb-42c2-9d00-e84ae71bce0d", from, to, consts.COSTS_GRANULARITY_DAILY, consts.COSTS_DEPTH_AUTO) if err != nil { t.Errorf("wanted no error, got %v", err) @@ -88,7 +88,7 @@ func TestCostsService_GetCustomerAccountCosts(t *testing.T) { from := time.Date(int(2022), time.May, int(22), int(0), int(0), int(0), int(0), time.UTC) to := time.Date(int(2022), time.June, int(21), int(0), int(0), int(0), int(0), time.UTC) - _, err := costsSvc.GetCustomerAccountCosts(context.Background(), from, to, consts.COSTS_GRANULARITY_DAILY, consts.COSTS_DEPTH_AUTO) + _, err := costsSvc.GetCustomerAccountCosts(context.Background(), "07a1ed91-2efb-42c2-9d00-e84ae71bce0d", from, to, consts.COSTS_GRANULARITY_DAILY, consts.COSTS_DEPTH_AUTO) if err == nil { t.Errorf("wanted error, got nil") @@ -108,7 +108,7 @@ func TestCostsService_GetCustomerAccountCosts(t *testing.T) { from := time.Date(int(2022), time.May, int(22), int(0), int(0), int(0), int(0), time.UTC) to := time.Date(int(2022), time.June, int(21), int(0), int(0), int(0), int(0), time.UTC) - _, err := costsSvc.GetCustomerAccountCosts(context.Background(), from, to, consts.COSTS_GRANULARITY_DAILY, consts.COSTS_DEPTH_AUTO) + _, err := costsSvc.GetCustomerAccountCosts(context.Background(), "07a1ed91-2efb-42c2-9d00-e84ae71bce0d", from, to, consts.COSTS_GRANULARITY_DAILY, consts.COSTS_DEPTH_AUTO) if err == nil { t.Errorf("wanted error, got nil") @@ -132,7 +132,7 @@ func TestCostsService_GetCustomerAccountCosts(t *testing.T) { from := time.Date(int(2022), time.May, int(22), int(0), int(0), int(0), int(0), time.UTC) to := time.Date(int(2022), time.June, int(21), int(0), int(0), int(0), int(0), time.UTC) - _, err := costsSvc.GetCustomerAccountCosts(ctx, from, to, consts.COSTS_GRANULARITY_DAILY, consts.COSTS_DEPTH_AUTO) + _, err := costsSvc.GetCustomerAccountCosts(ctx, "07a1ed91-2efb-42c2-9d00-e84ae71bce0d", from, to, consts.COSTS_GRANULARITY_DAILY, consts.COSTS_DEPTH_AUTO) if err == nil { t.Error("wanted error, got nil") @@ -179,6 +179,7 @@ func TestCostsService_GetProjectCosts(t *testing.T) { costsSvc := costs.New(client) _, err := costsSvc.GetProjectCosts( context.Background(), + "07a1ed91-2efb-42c2-9d00-e84ae71bce0d", "invalid-id", time.Now().AddDate(0, 0, -30), time.Now(), @@ -207,6 +208,7 @@ func TestCostsService_GetProjectCosts(t *testing.T) { to := time.Date(int(2022), time.June, int(21), int(0), int(0), int(0), int(0), time.UTC) resp, err := costsSvc.GetProjectCosts( context.Background(), + "07a1ed91-2efb-42c2-9d00-e84ae71bce0d", "ba04b091-ec32-4423-81e6-976f1b8a8363", from, to, @@ -247,6 +249,7 @@ func TestCostsService_GetProjectCosts(t *testing.T) { to := time.Date(int(2022), time.June, int(21), int(0), int(0), int(0), int(0), time.UTC) _, err := costsSvc.GetProjectCosts( ctx, + "07a1ed91-2efb-42c2-9d00-e84ae71bce0d", "ba04b091-ec32-4423-81e6-976f1b8a8363", from, to, @@ -270,6 +273,7 @@ func TestCostsService_GetProjectCosts(t *testing.T) { to := time.Date(int(2022), time.June, int(21), int(0), int(0), int(0), int(0), time.UTC) _, err := costsSvc.GetProjectCosts( context.Background(), + "07a1ed91-2efb-42c2-9d00-e84ae71bce0d", "ba04b091-ec32-4423-81e6-976f1b8a8363", from, to, @@ -297,6 +301,7 @@ func TestCostsService_GetProjectCosts(t *testing.T) { to := time.Date(int(2022), time.June, int(21), int(0), int(0), int(0), int(0), time.UTC) _, err := costsSvc.GetProjectCosts( context.Background(), + "07a1ed91-2efb-42c2-9d00-e84ae71bce0d", "ba04b091-ec32-4423-81e6-976f1b8a8363", from, to, diff --git a/pkg/api/v1/projects/projects.go b/pkg/api/v1/projects/projects.go deleted file mode 100644 index 7e148043..00000000 --- a/pkg/api/v1/projects/projects.go +++ /dev/null @@ -1,331 +0,0 @@ -// package projects handles creation and management of STACKIT projects -// For this use case, the Service Account used may need special permissions to manage STACKIT projects -// If needed, contact STACKIT support for further assistance on the matter - -package projects - -import ( - "context" - "encoding/json" - "fmt" - "net/http" - "strings" - - "github.com/SchwarzIT/community-stackit-go-client/internal/common" - "github.com/SchwarzIT/community-stackit-go-client/pkg/consts" - "github.com/SchwarzIT/community-stackit-go-client/pkg/validate" - "github.com/SchwarzIT/community-stackit-go-client/pkg/wait" - "github.com/pkg/errors" -) - -// constants -const ( - apiPath = consts.API_PATH_RESOURCE_MANAGEMENT_PROJECTS - apiPathCreate = consts.API_PATH_RESOURCE_MANAGEMENT_ORG_PROJECTS -) - -// New returns a new handler for the service -func New(c common.Client) *ProjectService { - return &ProjectService{ - Client: c, - } -} - -// ProjectService is the service that handles -// CRUD functionality for STACKIT projects -type ProjectService common.Service - -// Project struct holds important info about a STACKIT project -type Project struct { - ID string - Name string - BillingReference string - OrganizationID string -} - -// ProjectRole represents a role in the project -type ProjectRole struct { - Name string `json:"name"` - Users []ProjectRoleMember `json:"users"` - ServiceAccounts []ProjectRoleMember `json:"service_accounts"` -} - -// ProjectRoleMember represents a user or service account -type ProjectRoleMember struct { - ID string `json:"id"` - Email string `json:"email,omitempty"` -} - -// Requests - -type projectsCreateReqBody struct { - Name string `json:"name"` - Scope string `json:"scope"` - Members projectsMembersReqBody `json:"members"` - Labels projectsLabelsReqBody `json:"labels"` -} - -type projectsUpdateReqBody struct { - Name string `json:"name"` - Labels projectsLabelsReqBody `json:"labels"` -} - -type projectsEntityReqBody struct { - Role string `json:"role"` - ID string `json:"id"` -} - -type projectsMembersReqBody struct { - Users []projectsEntityReqBody `json:"users"` - ServiceAccounts []projectsEntityReqBody `json:"serviceAccounts"` -} - -type projectsLabelsReqBody struct { - BillingReference string `json:"billingReference"` -} - -// Responses - -// ProjectsResBody is the generic api response struct -type ProjectsResBody struct { - ProjectID string `json:"projectId"` - LifecycleState string `json:"lifecycleState"` - Scope string `json:"scope"` - Name string `json:"name"` - CreateTime string `json:"createTime"` - Labels ProjectsLabelsResBody `json:"labels"` - Parent ProjectsParentResBody -} - -// ProjectsLabelsResBody is the labels response -type ProjectsLabelsResBody struct { - BillingReference string `json:"billingReference"` -} - -// ProjectsParentResBody is the parent entity response -type ProjectsParentResBody struct { - Type string `json:"type"` - ID string `json:"id"` -} - -// Implementation - -// Create creates a new STACKIT project -// it returns a wait handler - running Wait() will wait for the project to be active -// See also https://api.stackit.schwarz/resource-management/openapi.v1.html#operation/post-organizations-organizationId-projects -func (svc *ProjectService) Create(ctx context.Context, name, billingRef string, roles ...ProjectRole) (Project, *wait.Handler, error) { - if err := ValidateProjectCreationRoles(roles); err != nil { - return Project{}, nil, validate.WrapError(err) - } - if err := validate.ProjectName(name); err != nil { - return Project{}, nil, validate.WrapError(err) - } - - if err := validate.BillingRef(billingRef); err != nil { - return Project{}, nil, validate.WrapError(err) - } - - body, err := svc.buildCreateRequestBody(name, billingRef, roles...) - if err != nil { - return Project{}, nil, err - } - - req, err := svc.Client.Request( - ctx, - http.MethodPost, - fmt.Sprintf(apiPathCreate, svc.Client.OrganizationID()), - body, - ) - if err != nil { - return Project{}, nil, err - } - - resBody := &ProjectsResBody{} - if _, err = svc.Client.Do(req, resBody); err != nil { - return Project{}, nil, errors.Wrap(err, fmt.Sprintf("request was:\n%s", string(body))) - } - - p := Project{ - ID: resBody.ProjectID, - Name: resBody.Name, - BillingReference: resBody.Labels.BillingReference, - OrganizationID: resBody.Parent.ID, - } - - w := wait.New(svc.waitForCreation(ctx, p.ID)) - return p, w, nil -} - -func (svc *ProjectService) waitForCreation(ctx context.Context, projectID string) wait.WaitFn { - return func() (res interface{}, done bool, err error) { - state, err := svc.GetLifecycleState(ctx, projectID) - if err != nil { - if strings.Contains(err.Error(), http.StatusText(http.StatusForbidden)) { - return state, false, nil - } - return state, false, err - } - if state != consts.PROJECT_STATUS_ACTIVE { - return state, false, nil - } - return state, true, nil - } -} - -func (svc *ProjectService) buildCreateRequestBody(name, billingRef string, roles ...ProjectRole) ([]byte, error) { - users := []projectsEntityReqBody{} - serviceAcounts := []projectsEntityReqBody{} - isMemberDefined := false - for _, r := range roles { - if len(r.Users) > 0 { - isMemberDefined = true - users = append(users, projectsEntityReqBody{ - Role: r.Name, - ID: r.Users[0].ID, - }) - } - if len(r.ServiceAccounts) > 0 { - isMemberDefined = true - serviceAcounts = append(serviceAcounts, projectsEntityReqBody{ - Role: r.Name, - ID: r.ServiceAccounts[0].ID, - }) - } - } - if !isMemberDefined { - return []byte{}, errors.New("no user ID or service account ID provided") - } - return json.Marshal(projectsCreateReqBody{ - Name: name, - Scope: consts.PROJECT_SCOPE_PUBLIC, - Members: projectsMembersReqBody{ - ServiceAccounts: serviceAcounts, - Users: users, - }, - Labels: projectsLabelsReqBody{ - BillingReference: billingRef, - }, - }) -} - -// Get returns the project by id -// See also https://api.stackit.schwarz/resource-management/openapi.v1.html#operation/get-projects-projectId -func (svc *ProjectService) Get(ctx context.Context, projectID string) (Project, error) { - req, err := svc.Client.Request( - ctx, - http.MethodGet, - fmt.Sprintf(apiPath, projectID), - nil, - ) - if err != nil { - return Project{}, err - } - - resBody := &ProjectsResBody{} - if _, err = svc.Client.Do(req, resBody); err != nil { - return Project{}, err - } - - return Project{ - ID: resBody.ProjectID, - Name: resBody.Name, - BillingReference: resBody.Labels.BillingReference, - OrganizationID: resBody.Parent.ID, - }, nil -} - -// GetLifecycleState returns the project state -// See also https://api.stackit.schwarz/resource-management/openapi.v1.html#operation/get-projects-projectId -func (svc *ProjectService) GetLifecycleState(ctx context.Context, projectID string) (string, error) { - req, err := svc.Client.Request( - ctx, - http.MethodGet, - fmt.Sprintf(apiPath, projectID), - nil, - ) - if err != nil { - return "", err - } - - resBody := &ProjectsResBody{} - if _, err = svc.Client.Do(req, resBody); err != nil { - return "", err - } - - return resBody.LifecycleState, nil -} - -// Update updates an existing STACKIT project -// See also https://api.stackit.schwarz/resource-management/openapi.v1.html#operation/patch-project-projectId -func (svc *ProjectService) Update(ctx context.Context, id, name, billingRef string) error { - if err := validate.ProjectName(name); err != nil { - return validate.WrapError(err) - } - - if err := validate.BillingRef(billingRef); err != nil { - return validate.WrapError(err) - } - - body, err := svc.buildUpdateRequestBody(name, billingRef) - if err != nil { - return err - } - - req, err := svc.Client.Request( - ctx, - http.MethodPatch, - fmt.Sprintf(apiPath, id), - body, - ) - if err != nil { - return err - } - - if _, err = svc.Client.Do(req, nil); err != nil { - return errors.Wrap(err, fmt.Sprintf("request was:\n%s", string(body))) - } - - return nil -} - -func (svc *ProjectService) buildUpdateRequestBody(name, billingRef string) ([]byte, error) { - return json.Marshal(projectsUpdateReqBody{ - Name: name, - Labels: projectsLabelsReqBody{ - BillingReference: billingRef, - }, - }) -} - -// Delete deletes a project by ID -// it returns a wait handler - running Wait() will wait for the project to be deleted -// See also https://api.stackit.schwarz/resource-management/openapi.v1.html#operation/delete-projects-projectId -func (svc *ProjectService) Delete(ctx context.Context, projectID string) (*wait.Handler, error) { - req, err := svc.Client.Request( - ctx, - http.MethodDelete, - fmt.Sprintf(apiPath, projectID), - nil, - ) - if err != nil { - return nil, err - } - - if _, err = svc.Client.Do(req, nil); err != nil { - return nil, err - } - - w := wait.New(svc.waitForDeletion(ctx, projectID)) - - return w, nil -} - -func (svc *ProjectService) waitForDeletion(ctx context.Context, projectID string) wait.WaitFn { - return func() (interface{}, bool, error) { - state, err := svc.GetLifecycleState(ctx, projectID) - if err != nil { - return state, true, nil - } - return state, false, nil - } -} diff --git a/pkg/api/v1/projects/projects_test.go b/pkg/api/v1/projects/projects_test.go deleted file mode 100644 index 4ba24556..00000000 --- a/pkg/api/v1/projects/projects_test.go +++ /dev/null @@ -1,285 +0,0 @@ -package projects_test - -import ( - "context" - "encoding/json" - "fmt" - "log" - "net/http" - "reflect" - "testing" - - client "github.com/SchwarzIT/community-stackit-go-client" - "github.com/SchwarzIT/community-stackit-go-client/internal/clients" - p "github.com/SchwarzIT/community-stackit-go-client/pkg/api/v1/projects" - "github.com/SchwarzIT/community-stackit-go-client/pkg/consts" -) - -var ( - skipAcceptanceTestGet = true - skipAcceptanceTestCreate = true - skipAcceptanceTestDelete = true - skipAcceptanceTestUpdate = true -) - -// Requests - -func TestProjectsService_Get(t *testing.T) { - c, mux, teardown, _ := client.MockServer() - defer teardown() - projects := p.New(c) - - mux.HandleFunc("/resource-management/v1/projects/abc", func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - - b, err := json.Marshal(p.ProjectsResBody{ - ProjectID: "123", - Name: "I-am_the-law", - Labels: p.ProjectsLabelsResBody{ - "T-1234567B", - }, - Parent: p.ProjectsParentResBody{ - ID: "987", - }, - }) - if err != nil { - log.Fatalf("json response marshal: %v", err) - } - fmt.Fprint(w, string(b)) - }) - - got, err := projects.Get(context.Background(), "abc") - if err != nil { - t.Fatal(err) - } - - want := p.Project{ - ID: "123", - Name: "I-am_the-law", - BillingReference: "T-1234567B", - OrganizationID: "987", - } - - if !reflect.DeepEqual(got, want) { - t.Errorf("got = %v, want %v", got, want) - } -} - -func TestAccProjectsService_Get(t *testing.T) { - if skipAcceptanceTestGet { - t.Skip() - } - - c, err := clients.LocalClient() - if err != nil { - t.Error(err) - } - projects := p.New(c) - - want := "6ea70fa9-af49-4550-80ad-8317788b4c4d" - got, err := projects.Get(context.Background(), want) - if err != nil { - t.Fatal(err) - } - - if got.ID != want { - t.Errorf("got = %v, want %v", got.ID, want) - } -} - -func TestProjectsService_GetLifecycleState(t *testing.T) { - c, mux, teardown, _ := client.MockServer() - defer teardown() - projects := p.New(c) - want := "some-state" - - mux.HandleFunc("/resource-management/v1/projects/abc", func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - - b, err := json.Marshal(p.ProjectsResBody{ - LifecycleState: want, - }) - if err != nil { - log.Fatalf("json response marshal: %v", err) - } - fmt.Fprint(w, string(b)) - }) - - got, err := projects.GetLifecycleState(context.Background(), "abc") - if err != nil { - t.Fatal(err) - } - - if !reflect.DeepEqual(got, want) { - t.Errorf("got = %v, want %v", got, want) - } -} - -func TestProjectsService_Create(t *testing.T) { - c, mux, teardown, _ := client.MockServer() - defer teardown() - projects := p.New(c) - - mux.HandleFunc(fmt.Sprintf("/resource-management/v1/organizations/%s/projects", consts.SCHWARZ_ORGANIZATION_ID), func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - - b, err := json.Marshal(p.ProjectsResBody{ - ProjectID: "123", - Name: "Fancy-new-project", - Labels: p.ProjectsLabelsResBody{ - "T-9876543B", - }, - Parent: p.ProjectsParentResBody{ - ID: consts.SCHWARZ_ORGANIZATION_ID, - }, - }) - if err != nil { - log.Fatalf("json response marshal: %v", err) - } - fmt.Fprint(w, string(b)) - }) - - mux.HandleFunc("/resource-management/v1/projects/123", func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - fmt.Fprint(w, ` - { - "projectId": "123", - "lifecycleState": "ACTIVE", - "scope": "PUBLIC" - } - `) - }) - - role := p.ProjectRole{ - Name: "project.owner", - Users: []p.ProjectRoleMember{ - { - ID: "0d3a2fb9-1472-4284-9655-d7aae2cd5bd5", - }, - }, - } - got, w, err := projects.Create(context.Background(), "Fancy-new-project", "T-9876543B", role) - if err != nil { - t.Fatal(err) - } - - want := p.Project{ - ID: "123", - Name: "Fancy-new-project", - BillingReference: "T-9876543B", - OrganizationID: consts.SCHWARZ_ORGANIZATION_ID, - } - - if !reflect.DeepEqual(got, want) { - t.Errorf("got = %v, want %v", got, want) - } - - if _, err := w.Wait(); err != nil { - t.Error(err) - } -} - -func TestAccProjectsService_Create(t *testing.T) { - if skipAcceptanceTestCreate { - t.Skip() - } - - c, err := clients.LocalClient() - if err != nil { - t.Error(err) - } - projects := p.New(c) - - want := p.Project{ - Name: "test-odj-stackit-client-at-0", - BillingReference: "T-0012253B", - } - role := p.ProjectRole{ - Name: "project.owner", - Users: []p.ProjectRoleMember{ - { - ID: "0d3a2fb9-1472-4284-9655-d7aae2cd5bd5", - }, - }, - } - got, _, err := projects.Create(context.Background(), want.Name, want.BillingReference, role) - if err != nil { - t.Fatal(err) - } - - if got.Name != want.Name || got.BillingReference != want.BillingReference { - t.Errorf("got = %v, want %v", got, want) - } -} - -func TestProjectsService_Delete(t *testing.T) { - c, mux, teardown, _ := client.MockServer() - defer teardown() - projects := p.New(c) - - mux.HandleFunc("/resource-management/v1/projects/abc", func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - }) - - w, err := projects.Delete(context.Background(), "abc") - if err != nil { - t.Errorf("delete project: %v", err) - } - - if _, err := w.Wait(); err != nil { - t.Error(err) - } - -} - -func TestAccProjectsService_Delete(t *testing.T) { - if skipAcceptanceTestDelete { - t.Skip() - } - c, err := clients.LocalClient() - if err != nil { - t.Error(err) - } - projects := p.New(c) - if _, err := projects.Delete(context.Background(), "0d3a2fb9-1472-4284-9655-d7aae2cd5bd5"); err != nil { - t.Fatal(err) - } -} - -func TestProjectsService_Update(t *testing.T) { - c, mux, teardown, _ := client.MockServer() - defer teardown() - projects := p.New(c) - - mux.HandleFunc("/resource-management/v1/projects/123", func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - }) - - err := projects.Update(context.Background(), "123", "Fancy-new-project", "T-9876543B") - if err != nil { - t.Fatal(err) - } -} - -func TestAccProjectsService_Update(t *testing.T) { - if skipAcceptanceTestUpdate { - t.Skip() - } - c, err := clients.LocalClient() - if err != nil { - t.Error(err) - } - projects := p.New(c) - want := p.Project{ - ID: "6ea70fa9-af49-4550-80ad-8317788b4c4d", - Name: "my-odj-test-project", - BillingReference: "T-0012253B", - } - - if err := projects.Update(context.Background(), want.ID, want.Name, want.BillingReference); err != nil { - t.Fatal(err) - } -} diff --git a/pkg/api/v1/projects/validate.go b/pkg/api/v1/projects/validate.go deleted file mode 100644 index 68acaa4c..00000000 --- a/pkg/api/v1/projects/validate.go +++ /dev/null @@ -1,41 +0,0 @@ -// This file is used for validation functions that validate project management data - -package projects - -import ( - "errors" - - "github.com/SchwarzIT/community-stackit-go-client/pkg/validate" -) - -// ValidateProjectCreationRoles validates that the given users and roles are correctly defined -// to fit project creation requirements -// Reference: https://api.stackit.schwarz/resource-management/openapi.v1.html#operation/post-organizations-organizationId-projects -func ValidateProjectCreationRoles(roles []ProjectRole) error { - if len(roles) == 0 { - return errors.New("no role definition found. at least one role needs to be defined") - } - foundSA := false - foundUser := false - for _, role := range roles { - if err := validate.Role(role.Name); err != nil { - return err - } - if len(role.Users) > 1 || len(role.ServiceAccounts) > 1 { - return errors.New("during project creation, up to 1 service account and/or user can be defined") - } - if len(role.Users) == 1 { - if foundUser { - return errors.New("up to 1 user can be defined during project creation") - } - foundUser = true - } - if len(role.ServiceAccounts) == 1 { - if foundSA { - return errors.New("up to 1 service account can be defined during project creation") - } - foundSA = true - } - } - return nil -} diff --git a/pkg/api/v1/resource-management/projects/projects.go b/pkg/api/v1/resource-management/projects/projects.go new file mode 100644 index 00000000..31294ac4 --- /dev/null +++ b/pkg/api/v1/resource-management/projects/projects.go @@ -0,0 +1,67 @@ +package projects + +import ( + "context" + "fmt" + "net/http" + + "github.com/SchwarzIT/community-stackit-go-client/internal/common" + "github.com/SchwarzIT/community-stackit-go-client/pkg/consts" +) + +// constants +const ( + apiPath = consts.API_PATH_RESOURCE_MANAGEMENT_PROJECTS + apiPathCreate = consts.API_PATH_RESOURCE_MANAGEMENT_ORG_PROJECTS +) + +// New returns a new handler for the service +func New(c common.Client) *ProjectService { + return &ProjectService{ + Client: c, + } +} + +// ProjectService is the service that handles +// CRUD functionality for STACKIT projects +type ProjectService common.Service + +// ProjectGetResponse is the generic api response struct +type ProjectGetResponse struct { + ProjectID string `json:"projectId"` + ContainerID string `json:"containerId"` + LifecycleState string `json:"lifecycleState"` + Scope string `json:"scope"` + Name string `json:"name"` + CreateTime string `json:"createTime"` + Labels ProjectsLabels `json:"labels"` + Parent ProjectsParent +} + +// ProjectsLabels is the labels response +type ProjectsLabels struct { + BillingReference string `json:"billingReference"` +} + +// ProjectsParent is the parent entity response +type ProjectsParent struct { + Type string `json:"type"` + ID string `json:"id"` +} + +// Get returns the project by id +// See also https://api.stackit.schwarz/resource-management/openapi.v1.html#operation/get-projects-projectId +func (svc *ProjectService) Get(ctx context.Context, projectID string) (res ProjectGetResponse, err error) { + req, err := svc.Client.Request( + ctx, + http.MethodGet, + fmt.Sprintf(apiPath, projectID), + nil, + ) + if err != nil { + return + } + + _, err = svc.Client.Do(req, &res) + return +} diff --git a/pkg/api/v1/resource-management/projects/projects_test.go b/pkg/api/v1/resource-management/projects/projects_test.go new file mode 100644 index 00000000..0b2978ca --- /dev/null +++ b/pkg/api/v1/resource-management/projects/projects_test.go @@ -0,0 +1,66 @@ +package projects_test + +import ( + "context" + "encoding/json" + "fmt" + "log" + "net/http" + "reflect" + "testing" + + client "github.com/SchwarzIT/community-stackit-go-client" + "github.com/SchwarzIT/community-stackit-go-client/pkg/api/v1/resource-management/projects" +) + +func TestProjectService_Get(t *testing.T) { + c, mux, teardown, _ := client.MockServer() + defer teardown() + p := projects.New(c) + want := projects.ProjectGetResponse{ + ProjectID: "abc", + Name: "I-am_the-law", + Labels: projects.ProjectsLabels{ + "T-1234567B", + }, + Parent: projects.ProjectsParent{ + ID: "987", + }, + } + mux.HandleFunc("/resource-management/v1/projects/abc", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + b, err := json.Marshal(want) + if err != nil { + log.Fatalf("json response marshal: %v", err) + } + fmt.Fprint(w, string(b)) + }) + + type args struct { + ctx context.Context + projectID string + } + tests := []struct { + name string + args args + wantRes projects.ProjectGetResponse + wantErr bool + }{ + {"ok", args{context.Background(), "abc"}, want, false}, + {"nil ctx", args{nil, "abc"}, want, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotRes, err := p.Get(tt.args.ctx, tt.args.projectID) + if (err != nil) != tt.wantErr { + t.Errorf("ProjectService.Get() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(gotRes, tt.wantRes) && !tt.wantErr { + t.Errorf("ProjectService.Get() = %v, want %v", gotRes, tt.wantRes) + } + }) + } +} diff --git a/pkg/api/v1/resource-management/resource_management.go b/pkg/api/v1/resource-management/resource_management.go new file mode 100644 index 00000000..28cfaf0e --- /dev/null +++ b/pkg/api/v1/resource-management/resource_management.go @@ -0,0 +1,21 @@ +// package resourcemanager groups together services that implement STACKIT's Resource Manager v2 API + +package resourcemanager + +import ( + "github.com/SchwarzIT/community-stackit-go-client/internal/common" + "github.com/SchwarzIT/community-stackit-go-client/pkg/api/v1/resource-management/projects" +) + +// New returns a new handler for the service +func New(c common.Client) *ResourceManagementV1Service { + return &ResourceManagementV1Service{ + Projects: projects.New(c), + } +} + +// ResourceManagementService is the service that handles +// project, organization and folder related services +type ResourceManagementV1Service struct { + Projects *projects.ProjectService +} diff --git a/pkg/api/v1/roles/roles.go b/pkg/api/v1/roles/roles.go deleted file mode 100644 index c9c0382c..00000000 --- a/pkg/api/v1/roles/roles.go +++ /dev/null @@ -1,269 +0,0 @@ -// package roles is used for managing roles for users and service accounts - -package roles - -import ( - "context" - "encoding/json" - "fmt" - "net/http" - - "github.com/SchwarzIT/community-stackit-go-client/internal/common" - "github.com/SchwarzIT/community-stackit-go-client/pkg/consts" - "github.com/SchwarzIT/community-stackit-go-client/pkg/validate" - "github.com/pkg/errors" -) - -// constants -const ( - apiPath = consts.API_PATH_MEMBERSHIP_ROLES -) - -// New returns a new handler for the service -func New(c common.Client) *RolesService { - return &RolesService{ - Client: c, - } -} - -// RolesService is the service that handles -// CRUD functionality for users in roles in a STACKIT project -type RolesService common.Service - -// Service Response - -// ProjectRoles is the main response struct -// representing a project, its roles and members that belong to the role -type ProjectRoles struct { - ProjectID string `json:"projectID"` - Roles []ProjectRole `json:"roles"` -} - -// ProjectRole represents a role and its members -type ProjectRole struct { - Name string `json:"name"` - Users []ProjectRoleMember `json:"users"` - ServiceAccounts []ProjectRoleMember `json:"service_accounts"` -} - -// ProjectRoleMember represents a user or service account -type ProjectRoleMember struct { - ID string `json:"id"` - Email string `json:"email,omitempty"` -} - -// Requests - -// projectRolesAddUserReq represents a request to add users to a project role -type projectRolesAddUserReq struct { - Role string `json:"role"` - Users []string `json:"users"` -} - -type projectRolesDeleteUserReq projectRolesAddUserReq - -// projectRoleAddSAReq represents a request to add service account to a project role -type projectRoleAddSAReq struct { - ServiceAccountID string `json:"serviceAccountId"` -} - -// Responses - -type projectRolesGetResBody struct { - Items []struct { - Role string `json:"role"` - ProjectID string `json:"projectId"` - Members []struct { - GlobalID string `json:"globalId"` - Email string `json:"email,omitempty"` - } `json:"members"` - } `json:"items"` -} - -// Implementation - -// Get returns the project roles -// Reference: https://api.stackit.schwarz/membership-service/openapi.v1.html#operation/get-projects-projectId-roles -func (svc *RolesService) Get(ctx context.Context, projectID string) (ProjectRoles, error) { - req, err := svc.Client.Request( - ctx, - http.MethodGet, - fmt.Sprintf(apiPath, projectID), - nil, - ) - if err != nil { - return ProjectRoles{}, err - } - - resBody := &projectRolesGetResBody{} - if _, err = svc.Client.Do(req, resBody); err != nil { - return ProjectRoles{}, err - } - - return svc.transformResponse(resBody), nil -} - -// AddUsers adds users and/or service accounts to a given project role -func (svc *RolesService) AddUsers(ctx context.Context, projectID, role string, users []string, serviceAccounts []string) error { - if err := validate.Role(role); err != nil { - return validate.WrapError(err) - } - for _, sa := range serviceAccounts { - if err := svc.addServiceAccounts(ctx, projectID, role, sa); err != nil { - return err - } - } - return svc.addUsers(ctx, projectID, role, users...) -} - -// addUsers adds users to project role -// Reference: https://api.stackit.schwarz/membership-service/openapi.v1.html#operation/patch-projects-projectId-roles -func (svc *RolesService) addUsers(ctx context.Context, projectID, role string, users ...string) error { - body, err := svc.buildAddUsersRequestBody(role, users...) - if err != nil { - return err - } - req, err := svc.Client.Request( - ctx, - http.MethodPatch, - fmt.Sprintf(apiPath, projectID), - body, - ) - if err != nil { - return err - } - - if _, err = svc.Client.Do(req, nil); err != nil { - return errors.Wrap(err, fmt.Sprintf("request was:\n%s", string(body))) - } - - return nil -} - -// addServiceAccounts adds service account to project role -// Reference: https://api.stackit.schwarz/membership-service/openapi.v1.html#operation/post-organizations-organizationId-projects-projectId-roles-roleName-service-accounts -func (svc *RolesService) addServiceAccounts(ctx context.Context, projectID, role string, serviceAccountID string) error { - body, err := svc.buildAddSARequestBody(serviceAccountID) - if err != nil { - return err - } - req, err := svc.Client.Request( - ctx, - http.MethodPost, - fmt.Sprintf(consts.API_PATH_MEMBERSHIP_ORG_PROJECT_ROLE_SERVICE_ACCOUNTS, svc.Client.OrganizationID(), projectID, role), - body, - ) - if err != nil { - return err - } - - if _, err = svc.Client.Do(req, nil); err != nil { - return errors.Wrap(err, fmt.Sprintf("request was:\n%s", string(body))) - } - - return nil -} - -func (svc *RolesService) buildAddUsersRequestBody(role string, userIDs ...string) ([]byte, error) { - return json.Marshal([]projectRolesAddUserReq{{ - Role: role, - Users: userIDs, - }}) -} - -func (svc *RolesService) buildAddSARequestBody(serviceAccountID string) ([]byte, error) { - return json.Marshal(projectRoleAddSAReq{ - ServiceAccountID: serviceAccountID, - }) -} - -// DeleteUsers removes users from a given role -// Reference: https://api.stackit.schwarz/membership-service/openapi.v1.html#operation/patch-projects-projectId-roles -func (svc *RolesService) DeleteUsers(ctx context.Context, projectID, role string, users []string, serviceAccounts []string) error { - if err := validate.Role(role); err != nil { - return validate.WrapError(err) - } - for _, sa := range serviceAccounts { - if err := svc.deleteServiceAccount(ctx, projectID, role, sa); err != nil { - return err - } - } - return svc.deleteUsers(ctx, projectID, role, users...) -} - -// deleteUsers removes users from a given role -func (svc *RolesService) deleteUsers(ctx context.Context, projectID, role string, userIDs ...string) error { - body, err := svc.buildDeleteUsersRequestBody(role, userIDs...) - if err != nil { - return err - } - req, err := svc.Client.Request( - ctx, - http.MethodPatch, - fmt.Sprintf(consts.API_PATH_MEMBERSHIP_ROLES_DELETE, projectID), - body, - ) - if err != nil { - return err - } - - if _, err = svc.Client.Do(req, nil); err != nil { - return errors.Wrap(err, fmt.Sprintf("request was:\n%s", string(body))) - } - - return nil -} - -// deleteServiceAccount removes a service account from a given role -// Reference: https://api.stackit.schwarz/membership-service/openapi.v1.html#operation/patch-projects-projectId-roles -func (svc *RolesService) deleteServiceAccount(ctx context.Context, projectID, role string, serviceAccountID string) error { - req, err := svc.Client.Request( - ctx, - http.MethodDelete, - fmt.Sprintf(consts.API_PATH_MEMBERSHIP_ORG_PROJECT_ROLE_SERVICE_ACCOUNT, svc.Client.OrganizationID(), projectID, role, serviceAccountID), - nil, - ) - if err != nil { - return err - } - - if _, err = svc.Client.Do(req, nil); err != nil { - return err - } - - return nil -} - -func (svc *RolesService) buildDeleteUsersRequestBody(role string, userIDs ...string) ([]byte, error) { - return json.Marshal([]projectRolesDeleteUserReq{{ - Role: role, - Users: userIDs, - }}) -} - -func (svc *RolesService) transformResponse(clientRes *projectRolesGetResBody) ProjectRoles { - pr := ProjectRoles{Roles: []ProjectRole{}} - for _, role := range clientRes.Items { - pr.ProjectID = role.ProjectID - users := []ProjectRoleMember{} - sas := []ProjectRoleMember{} - for _, m := range role.Members { - if m.Email == "" { - sas = append(sas, ProjectRoleMember{ - ID: m.GlobalID, - }) - continue - } - users = append(users, ProjectRoleMember{ - Email: m.Email, - ID: m.GlobalID, - }) - } - pr.Roles = append(pr.Roles, ProjectRole{ - Name: role.Role, - Users: users, - ServiceAccounts: sas, - }) - } - return pr -} diff --git a/pkg/api/v1/users/users.go b/pkg/api/v1/users/users.go deleted file mode 100644 index 78ec2d62..00000000 --- a/pkg/api/v1/users/users.go +++ /dev/null @@ -1,99 +0,0 @@ -// package users is used for migrating Schwarz IT KG users to STACKIT -// this package is intended to be used by the authClient -// IMPORTANT: this package and the authClient will soon be removed - -package users - -import ( - "context" - "encoding/json" - "net/http" - - "github.com/SchwarzIT/community-stackit-go-client/internal/common" - "github.com/SchwarzIT/community-stackit-go-client/pkg/consts" - "github.com/SchwarzIT/community-stackit-go-client/pkg/validate" -) - -// Public types - -// constants -const ( - apiPath = consts.API_PATH_SHADOW_USERS -) - -// New returns a new handler for the service -func New(c common.Client) *UsersService { - return &UsersService{ - Client: c, - } -} - -// UsersService is the service that handles -// CRUD functionality for STACKIT shadow users -type UsersService common.Service - -// User struct holds important info about a shadow user -type User struct { - UUID string - Email string - Origin string - OrganizationID string // Also called Customer Account -} - -// Requests - -type usersGetOrCreateUUIDReqBody struct { - Email string `json:"email"` - Origin string `json:"origin"` - CustomerAccount string `json:"customer-account"` -} - -// Responses - -// ShadowUsersResBody is the response struct from the shadow api -type ShadowUsersResBody struct { - UUID string `json:"uuid"` - Username string `json:"username"` - Origin string `json:"origin"` -} - -// Implementation - -// Get returns the user's their UUID (this is a shadow user UUID) -// Reference https://api.stackit.schwarz/appcloud-shadow-user-creator/openapi.v1.html#section/Authentication -func (svc *UsersService) Get(ctx context.Context, email, origin string) (User, error) { - // validate origin - if err := validate.UserOrigin(origin); err != nil { - return User{}, err - } - - body, err := svc.usersBuildGetOrCreateUUIDBody(email, origin, svc.Client.OrganizationID()) - if err != nil { - return User{}, err - } - - req, err := svc.Client.Request(ctx, http.MethodPut, apiPath, body) - if err != nil { - return User{}, err - } - - resBody := &ShadowUsersResBody{} - if _, err = svc.Client.Do(req, resBody); err != nil { - return User{}, err - } - - return User{ - UUID: resBody.UUID, - Origin: resBody.Origin, - Email: resBody.Username, - OrganizationID: svc.Client.OrganizationID(), - }, nil -} - -func (svc *UsersService) usersBuildGetOrCreateUUIDBody(email, origin, organizationID string) ([]byte, error) { - return json.Marshal(usersGetOrCreateUUIDReqBody{ - Email: email, - Origin: origin, - CustomerAccount: organizationID, - }) -} diff --git a/pkg/api/v1/users/users_test.go b/pkg/api/v1/users/users_test.go deleted file mode 100644 index 462a687e..00000000 --- a/pkg/api/v1/users/users_test.go +++ /dev/null @@ -1,121 +0,0 @@ -package users_test - -import ( - "context" - "encoding/json" - "fmt" - "log" - "net/http" - "reflect" - "testing" - - "github.com/SchwarzIT/community-stackit-go-client" - "github.com/SchwarzIT/community-stackit-go-client/internal/clients" - u "github.com/SchwarzIT/community-stackit-go-client/pkg/api/v1/users" - "github.com/SchwarzIT/community-stackit-go-client/pkg/consts" - "github.com/SchwarzIT/community-stackit-go-client/pkg/validate" -) - -var ( - skipAcceptanceTestGetUser = true -) - -func TestUsersService_ValidateUserOrigin(t *testing.T) { - type args struct { - origin string - } - tests := []struct { - name string - args args - wantErr bool - }{ - { - name: "test fail", - args: args{ - origin: "something", - }, - wantErr: true, - }, - { - name: "test success", - args: args{ - origin: consts.SCHWARZ_AUTH_ORIGIN, - }, - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if err := validate.UserOrigin(tt.args.origin); (err != nil) != tt.wantErr { - t.Errorf("ValidateUserOrigin() error = %v, wantErr %v", err, tt.wantErr) - } - }) - } -} - -func TestUsersService_Get(t *testing.T) { - c, mux, teardown, _ := client.MockServer() - defer teardown() - users := u.New(c) - - want := u.User{ - Email: "some@one.com", - Origin: consts.SCHWARZ_AUTH_ORIGIN, - UUID: "some-id", - OrganizationID: consts.SCHWARZ_ORGANIZATION_ID, - } - - mux.HandleFunc("/ucp-shadow-user-management/v1/createcuaashadowuser/user", - func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - - b, err := json.Marshal(u.ShadowUsersResBody{ - Username: want.Email, - UUID: want.UUID, - Origin: want.Origin, - }) - if err != nil { - log.Fatalf("json response marshal: %v", err) - } - fmt.Fprint(w, string(b)) - }) - - got, err := users.Get(context.Background(), want.Email, want.Origin) - if err != nil { - t.Fatal(err) - } - - if !reflect.DeepEqual(got, want) { - t.Errorf("got = %v, want %v", got, want) - } -} - -func TestAccUsersService_Get(t *testing.T) { - if skipAcceptanceTestGetUser { - t.Skip() - } - - ac, err := clients.LocalAuthClient() - if err != nil { - t.Error(err) - } - - res, err := ac.GetToken(context.Background()) - if err != nil { - t.Fatalf("failed to get token: %v", err) - } - - c, err := clients.LocalClient() - if err != nil { - t.Error(err) - } - c.SetToken(res.AccessToken) - users := u.New(c) - user, err := users.Get(context.Background(), "deangili.oren@mail.schwarz", "schwarz-federation") - if err != nil { - t.Fatal(err) - } - - t.Log(user.Email, user.UUID) -} diff --git a/pkg/api/v2/membership/members/members_test.go b/pkg/api/v2/membership/members/members_test.go index 58bbe0a0..8cc3d10b 100644 --- a/pkg/api/v2/membership/members/members_test.go +++ b/pkg/api/v2/membership/members/members_test.go @@ -8,7 +8,7 @@ import ( "reflect" "testing" - "github.com/SchwarzIT/community-stackit-go-client" + client "github.com/SchwarzIT/community-stackit-go-client" "github.com/SchwarzIT/community-stackit-go-client/internal/common" "github.com/SchwarzIT/community-stackit-go-client/pkg/api/v2/membership/members" ) @@ -25,7 +25,7 @@ func TestNew(t *testing.T) { want *members.MembersService }{ {"test directly", args{c}, members.New(c)}, - {"test through client", args{c}, &c.Incubator.Membership.Members}, + {"test through client", args{c}, &c.Membership.Members}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/pkg/api/v2/resource-manager/organizations/organization.go b/pkg/api/v2/resource-management/organizations/organization.go similarity index 96% rename from pkg/api/v2/resource-manager/organizations/organization.go rename to pkg/api/v2/resource-management/organizations/organization.go index 4775cfd3..3d942ea7 100644 --- a/pkg/api/v2/resource-manager/organizations/organization.go +++ b/pkg/api/v2/resource-management/organizations/organization.go @@ -13,7 +13,7 @@ import ( // constants const ( - apiPath = consts.API_PATH_RESOURCE_MANAGER_V2_ORG + apiPath = consts.API_PATH_RESOURCE_MANAGEMENT_V2_ORG ) // New returns a new handler for the service diff --git a/pkg/api/v2/resource-manager/organizations/organization_test.go b/pkg/api/v2/resource-management/organizations/organization_test.go similarity index 83% rename from pkg/api/v2/resource-manager/organizations/organization_test.go rename to pkg/api/v2/resource-management/organizations/organization_test.go index 298ac223..4fe2b6b0 100644 --- a/pkg/api/v2/resource-manager/organizations/organization_test.go +++ b/pkg/api/v2/resource-management/organizations/organization_test.go @@ -8,19 +8,19 @@ import ( "reflect" "testing" - "github.com/SchwarzIT/community-stackit-go-client" - resourceManager "github.com/SchwarzIT/community-stackit-go-client/pkg/api/v2/resource-manager" - "github.com/SchwarzIT/community-stackit-go-client/pkg/api/v2/resource-manager/organizations" + client "github.com/SchwarzIT/community-stackit-go-client" + resourceManagement "github.com/SchwarzIT/community-stackit-go-client/pkg/api/v2/resource-management" + "github.com/SchwarzIT/community-stackit-go-client/pkg/api/v2/resource-management/organizations" ) const ( - apiPath = "/resource-manager/v2/organizations/%s" + apiPath = "/resource-management/v2/organizations/%s" ) func TestOrganizationsService_Get(t *testing.T) { c, mux, teardown, _ := client.MockServer() defer teardown() - p := resourceManager.New(c).Organizations + p := resourceManagement.New(c).Organizations containerID := "my-container-id-b18796aa7e78" want := organizations.OrganizationResponse{ diff --git a/pkg/api/v2/resource-manager/projects/project.go b/pkg/api/v2/resource-management/projects/project.go similarity index 77% rename from pkg/api/v2/resource-manager/projects/project.go rename to pkg/api/v2/resource-management/projects/project.go index a7b26d4c..f65b7323 100644 --- a/pkg/api/v2/resource-manager/projects/project.go +++ b/pkg/api/v2/resource-management/projects/project.go @@ -8,16 +8,18 @@ import ( "fmt" "net/http" "net/url" + "strings" "github.com/SchwarzIT/community-stackit-go-client/internal/common" "github.com/SchwarzIT/community-stackit-go-client/pkg/consts" "github.com/SchwarzIT/community-stackit-go-client/pkg/validate" + "github.com/SchwarzIT/community-stackit-go-client/pkg/wait" ) // constants const ( - apiPath = consts.API_PATH_RESOURCE_MANAGER_V2_PROJECTS - apiPathProject = consts.API_PATH_RESOURCE_MANAGER_V2_PROJECT + apiPath = consts.API_PATH_RESOURCE_MANAGEMENT_V2_PROJECTS + apiPathProject = consts.API_PATH_RESOURCE_MANAGEMENT_V2_PROJECT ) // New returns a new handler for the service @@ -92,31 +94,51 @@ type ProjectsResponse struct { // Create creates a new STACKIT project // See also https://api.stackit.schwarz/resource-management/openapi.v2.html#operation/post-projects -func (svc *ProjectsService) Create(ctx context.Context, name string, labels map[string]string, members ...ProjectMember) (res ProjectResponse, err error) { - if err = svc.ValidateCreateData(name, labels, members); err != nil { +func (svc *ProjectsService) Create(ctx context.Context, parentContainerID, projectName string, labels map[string]string, members ...ProjectMember) (res ProjectResponse, w *wait.Handler, err error) { + if err = svc.ValidateCreateData(projectName, labels, members); err != nil { err = validate.WrapError(err) return } - body, _ := svc.buildCreateRequest(name, labels, members) + body, _ := svc.buildCreateRequest(parentContainerID, projectName, labels, members) req, err := svc.Client.Request(ctx, http.MethodPost, apiPath, body) if err != nil { return } _, err = svc.Client.Do(req, &res) + w = wait.New(svc.waitForCreation(ctx, res.ContainerID)) return } -func (svc *ProjectsService) buildCreateRequest(name string, labels map[string]string, members []ProjectMember) ([]byte, error) { +func (svc *ProjectsService) buildCreateRequest(parentContainerID, projectName string, labels map[string]string, members []ProjectMember) ([]byte, error) { return json.Marshal(CreateProjectRequest{ - Name: name, - ParentID: svc.Client.OrganizationID(), + Name: projectName, + ParentID: parentContainerID, Members: members, Labels: labels, }) } +func (svc *ProjectsService) waitForCreation(ctx context.Context, containerID string) wait.WaitFn { + return func() (res interface{}, done bool, err error) { + state, err := svc.GetLifecycleState(ctx, containerID) + if err != nil { + if strings.Contains(err.Error(), http.StatusText(http.StatusForbidden)) { + return state, false, nil + } + return state, false, err + } + switch state { + case consts.PROJECT_STATUS_ACTIVE: + return state, true, nil + case consts.PROJECT_STATUS_CREATING: + return state, false, nil + } + return state, false, fmt.Errorf("received project state '%s'. aborting", state) + } +} + // Get returns the project by id // See also https://api.stackit.schwarz/resource-management/openapi.v2.html#operation/get-projects-containerId func (svc *ProjectsService) Get(ctx context.Context, containerID string) (res ProjectResponse, err error) { @@ -187,7 +209,7 @@ type UpdateProjectRequest struct { // Update updates an existing STACKIT project // See also https://api.stackit.schwarz/resource-management/openapi.v2.html#operation/patch-projects-containerId -func (svc *ProjectsService) Update(ctx context.Context, containerID, name, containerParentID string, labels map[string]string) (res ProjectResponse, err error) { +func (svc *ProjectsService) Update(ctx context.Context, containerParentID, containerID, name string, labels map[string]string) (res ProjectResponse, err error) { if err = svc.ValidateUpdateData(containerID, containerParentID, name, labels); err != nil { err = validate.WrapError(err) return @@ -213,11 +235,22 @@ func (svc *ProjectsService) buildUpdateRequest(name, containerParentID string, l // Delete deletes a project by ID // See also https://api.stackit.schwarz/resource-management/openapi.v2.html#operation/delete-projects-containerId -func (svc *ProjectsService) Delete(ctx context.Context, containerID string) (err error) { +func (svc *ProjectsService) Delete(ctx context.Context, containerID string) (w *wait.Handler, err error) { req, err := svc.Client.Request(ctx, http.MethodDelete, fmt.Sprintf(apiPathProject, containerID), nil) if err != nil { return } _, err = svc.Client.Do(req, nil) - return err + w = wait.New(svc.waitForDeletion(ctx, containerID)) + return +} + +func (svc *ProjectsService) waitForDeletion(ctx context.Context, containerID string) wait.WaitFn { + return func() (interface{}, bool, error) { + state, err := svc.GetLifecycleState(ctx, containerID) + if err != nil { + return state, true, nil + } + return state, false, nil + } } diff --git a/pkg/api/v2/resource-manager/projects/project_test.go b/pkg/api/v2/resource-management/projects/project_test.go similarity index 68% rename from pkg/api/v2/resource-manager/projects/project_test.go rename to pkg/api/v2/resource-management/projects/project_test.go index d5d16ad8..9c81575e 100644 --- a/pkg/api/v2/resource-manager/projects/project_test.go +++ b/pkg/api/v2/resource-management/projects/project_test.go @@ -8,11 +8,13 @@ import ( "reflect" "strconv" "testing" + "time" - "github.com/SchwarzIT/community-stackit-go-client" - resourcemanager "github.com/SchwarzIT/community-stackit-go-client/pkg/api/v2/resource-manager" - "github.com/SchwarzIT/community-stackit-go-client/pkg/api/v2/resource-manager/projects" + client "github.com/SchwarzIT/community-stackit-go-client" + resourcemanager "github.com/SchwarzIT/community-stackit-go-client/pkg/api/v2/resource-management" + "github.com/SchwarzIT/community-stackit-go-client/pkg/api/v2/resource-management/projects" "github.com/SchwarzIT/community-stackit-go-client/pkg/consts" + "github.com/SchwarzIT/community-stackit-go-client/pkg/wait" ) func TestProjectsService_Get(t *testing.T) { @@ -26,7 +28,7 @@ func TestProjectsService_Get(t *testing.T) { ContainerID: containerID, } - mux.HandleFunc(fmt.Sprintf("/resource-manager/v2/projects/%s", containerID), func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc(fmt.Sprintf("/resource-management/v2/projects/%s", containerID), func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { t.Error("wrong method") } @@ -78,7 +80,7 @@ func TestProjectsService_GetLifecycleState(t *testing.T) { LifecycleState: "CREATED", } - mux.HandleFunc(fmt.Sprintf("/resource-manager/v2/projects/%s", containerID), func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc(fmt.Sprintf("/resource-management/v2/projects/%s", containerID), func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { t.Error("wrong method") } @@ -165,7 +167,7 @@ func TestProjectsService_Create(t *testing.T) { CreationTime: "2021-08-24T14:15:22Z", } - mux.HandleFunc("/resource-manager/v2/projects", func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc("/resource-management/v2/projects", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { t.Error("wrong method") } @@ -208,19 +210,22 @@ func TestProjectsService_Create(t *testing.T) { args args wantRes projects.ProjectResponse wantErr bool + useWait bool }{ - {"no owner", args{context.Background(), "my-project", map[string]string{}, []projects.ProjectMember{}}, want, true}, - {"no billing", args{context.Background(), "my-project", map[string]string{}, member}, want, true}, - {"bad project name", args{context.Background(), "my project!", map[string]string{}, member}, want, true}, - {"all ok", args{context.Background(), "my-project", map[string]string{"billingReference": "T-0123456B", "scope": "PUBLIC"}, member}, want, false}, - {"too many labels", args{context.Background(), "my-project", longLabels, member}, want, true}, - {"no scope label", args{context.Background(), "my-project", map[string]string{"billingReference": "T-0123456B"}, member}, want, true}, - {"ctx is canceled", args{ctx, "my-project", map[string]string{"billingReference": "T-0123456B", "scope": "PUBLIC"}, member}, want, true}, - {"user not found", args{context.Background(), "my-project", map[string]string{"billingReference": "T-0123456B", "scope": "PUBLIC"}, memberNotFound}, want, true}, + {"no owner", args{context.Background(), "my-project", map[string]string{}, []projects.ProjectMember{}}, want, true, false}, + {"no billing", args{context.Background(), "my-project", map[string]string{}, member}, want, true, false}, + {"bad project name", args{context.Background(), "my project!", map[string]string{}, member}, want, true, false}, + {"all ok", args{context.Background(), "my-project", map[string]string{"billingReference": "T-0123456B", "scope": "PUBLIC"}, member}, want, false, true}, + {"too many labels", args{context.Background(), "my-project", longLabels, member}, want, true, false}, + {"no scope label", args{context.Background(), "my-project", map[string]string{"billingReference": "T-0123456B"}, member}, want, true, false}, + {"ctx is canceled", args{ctx, "my-project", map[string]string{"billingReference": "T-0123456B", "scope": "PUBLIC"}, member}, want, true, false}, + {"user not found", args{context.Background(), "my-project", map[string]string{"billingReference": "T-0123456B", "scope": "PUBLIC"}, memberNotFound}, want, true, false}, } + + var process *wait.Handler for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - gotRes, err := p.Create(tt.args.ctx, tt.args.name, tt.args.labels, tt.args.members...) + gotRes, w, err := p.Create(tt.args.ctx, "54066bf4-1aff-4f7b-9f83-fb23c348fee3", tt.args.name, tt.args.labels, tt.args.members...) if (err != nil) != tt.wantErr { t.Errorf("ProjectsService.Create() error = %v, wantErr %v", err, tt.wantErr) return @@ -228,8 +233,79 @@ func TestProjectsService_Create(t *testing.T) { if !reflect.DeepEqual(gotRes, tt.wantRes) && !tt.wantErr { t.Errorf("ProjectsService.Create() = %v, want %v", gotRes, tt.wantRes) } + if tt.useWait { + process = w + } }) } + + testCreationWait(t, mux, process, want) +} + +func testCreationWait(t *testing.T, mux *http.ServeMux, process *wait.Handler, want projects.ProjectResponse) { + + baseDuration := 200 * time.Millisecond + ctx1, td1 := context.WithTimeout(context.Background(), 1*baseDuration) + defer td1() + + ctx2, td2 := context.WithTimeout(context.Background(), 2*baseDuration) + defer td2() + + ctx3, td3 := context.WithTimeout(context.Background(), 3*baseDuration) + defer td3() + + ctx4, td4 := context.WithTimeout(context.Background(), 4*baseDuration) + defer td4() + + mux.HandleFunc(fmt.Sprintf("/resource-management/v2/projects/%s", want.ContainerID), func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + t.Errorf("wrong method %s", r.Method) + return + } + + w.Header().Set("Content-Type", "application/json") + if ctx1.Err() == nil { + w.WriteHeader(http.StatusBadRequest) + return + } + + if ctx2.Err() == nil { + w.WriteHeader(http.StatusForbidden) + return + } + + if ctx3.Err() == nil { + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, `{"lifecycleState": "CREATING"}`) + return + } + + if ctx4.Err() == nil { + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, `{"lifecycleState": "ACTIVE"}`) + } + + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, `{"lifecycleState": "DELETING"}`) + }) + + process.SetThrottle(baseDuration) + // first test run: expect Wait to exit with an error + if _, err := process.Wait(); err == nil { + t.Error("expected error but got nil") + } + + time.Sleep(baseDuration) + // 2nd test: expect Forbidden, creating, and exit with success after retry + if _, err := process.Wait(); err != nil { + t.Errorf("expected no error but got: %v", err) + } + + time.Sleep(baseDuration) + // last test run: expect error because of DELETING status + if _, err := process.Wait(); err == nil { + t.Error("expected error but got nil") + } } func TestProjectsService_Update(t *testing.T) { @@ -243,7 +319,7 @@ func TestProjectsService_Update(t *testing.T) { ContainerID: containerID, } - mux.HandleFunc("/resource-manager/v2/projects/"+containerID, func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc("/resource-management/v2/projects/"+containerID, func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPatch { t.Error("wrong method") } @@ -278,14 +354,13 @@ func TestProjectsService_Update(t *testing.T) { wantRes projects.ProjectResponse wantErr bool }{ - {"no billing", args{context.Background(), containerID, "my-project", consts.SCHWARZ_ORGANIZATION_ID, map[string]string{}}, want, true}, - {"bad project name", args{context.Background(), containerID, "my project!", consts.SCHWARZ_ORGANIZATION_ID, map[string]string{}}, want, true}, - // {"bad org uuid", args{context.Background(), containerID, "my-project", "test", map[string]string{"billingReference": "T-0123456B", "scope": "PUBLIC"}}, want, true}, - {"all ok", args{context.Background(), containerID, "my-project", consts.SCHWARZ_ORGANIZATION_ID, map[string]string{"billingReference": "T-0123456B", "scope": "PUBLIC"}}, want, false}, - {"no scope", args{context.Background(), containerID, "my-project", consts.SCHWARZ_ORGANIZATION_ID, map[string]string{"billingReference": "T-0123456B"}}, want, true}, - {"bad billing", args{context.Background(), containerID, "my-project", consts.SCHWARZ_ORGANIZATION_ID, map[string]string{"billingReference": "T#$%0123456B", "scope": "PUBLIC"}}, want, true}, - {"ctx is canceled", args{ctx, containerID, "my-project", consts.SCHWARZ_ORGANIZATION_ID, map[string]string{"billingReference": "T-0123456B", "scope": "PUBLIC"}}, want, true}, - {"project not found", args{context.Background(), "something", "my-project", consts.SCHWARZ_ORGANIZATION_ID, map[string]string{"billingReference": "T-0123456B", "scope": "PUBLIC"}}, want, true}, + {"no billing", args{context.Background(), consts.SCHWARZ_ORGANIZATION_ID, containerID, "my-project", map[string]string{}}, want, true}, + {"bad project name", args{context.Background(), consts.SCHWARZ_ORGANIZATION_ID, containerID, "my project!", map[string]string{}}, want, true}, + {"all ok", args{context.Background(), consts.SCHWARZ_ORGANIZATION_ID, containerID, "my-project", map[string]string{"billingReference": "T-0123456B", "scope": "PUBLIC"}}, want, false}, + {"no scope", args{context.Background(), consts.SCHWARZ_ORGANIZATION_ID, containerID, "my-project", map[string]string{"billingReference": "T-0123456B"}}, want, true}, + {"bad billing", args{context.Background(), consts.SCHWARZ_ORGANIZATION_ID, containerID, "my-project", map[string]string{"billingReference": "T#$%0123456B", "scope": "PUBLIC"}}, want, true}, + {"ctx is canceled", args{ctx, consts.SCHWARZ_ORGANIZATION_ID, containerID, "my-project", map[string]string{"billingReference": "T-0123456B", "scope": "PUBLIC"}}, want, true}, + {"project not found", args{context.Background(), consts.SCHWARZ_ORGANIZATION_ID, "something", "my-project", map[string]string{"billingReference": "T-0123456B", "scope": "PUBLIC"}}, want, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -308,12 +383,32 @@ func TestProjectsService_Delete(t *testing.T) { containerID := "5dae0612-f5b1-4615-b7ca-b18796aa7e78" - mux.HandleFunc("/resource-manager/v2/projects/"+containerID, func(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodDelete { - t.Error("wrong method") + baseDuration := 200 * time.Millisecond + ctx1, td1 := context.WithTimeout(context.Background(), 1*baseDuration) + defer td1() + + mux.HandleFunc("/resource-management/v2/projects/"+containerID, func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodDelete { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + return } - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusAccepted) + + if r.Method == http.MethodGet { + + w.Header().Set("Content-Type", "application/json") + + if ctx1.Err() == nil { + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, `{"lifecycleState": "DELETING"}`) + return + } + + w.WriteHeader(http.StatusNotFound) + return + + } + t.Error("wrong method") }) ctx, cancel := context.WithCancel(context.TODO()) @@ -327,17 +422,30 @@ func TestProjectsService_Delete(t *testing.T) { name string args args wantErr bool + useWait bool }{ - {"all ok", args{context.Background(), containerID}, false}, - {"ctx is canceled", args{ctx, containerID}, true}, + {"all ok", args{context.Background(), containerID}, false, true}, + {"ctx is canceled", args{ctx, containerID}, true, false}, } + + var process *wait.Handler for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := p.Delete(tt.args.ctx, tt.args.containerID); (err != nil) != tt.wantErr { + w, err := p.Delete(tt.args.ctx, tt.args.containerID) + if (err != nil) != tt.wantErr { t.Errorf("ProjectsService.Delete() error = %v, wantErr %v", err, tt.wantErr) } + if tt.useWait { + process = w + } }) } + + process.SetThrottle(baseDuration) + // 1nd test: expect success after retry + if _, err := process.Wait(); err != nil { + t.Errorf("expected no error but got: %v", err) + } } func TestProjectsService_List(t *testing.T) { @@ -357,7 +465,7 @@ func TestProjectsService_List(t *testing.T) { Limit: 50, } - mux.HandleFunc("/resource-manager/v2/projects", func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc("/resource-management/v2/projects", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { t.Error("wrong method") } @@ -368,14 +476,14 @@ func TestProjectsService_List(t *testing.T) { fmt.Fprint(w, string(b)) }) - mux.HandleFunc("/resource-manager/v2/projects?offset=2&limit=50", func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc("/resource-management/v2/projects?offset=2&limit=50", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) b, _ := json.Marshal(want2) fmt.Fprint(w, string(b)) }) - mux.HandleFunc("/resource-manager/v2/projects?offset=0&limit=50&containerIds=my-container-123", func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc("/resource-management/v2/projects?offset=0&limit=50&containerIds=my-container-123", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) diff --git a/pkg/api/v2/resource-manager/projects/validate.go b/pkg/api/v2/resource-management/projects/validate.go similarity index 100% rename from pkg/api/v2/resource-manager/projects/validate.go rename to pkg/api/v2/resource-management/projects/validate.go diff --git a/pkg/api/v2/resource-manager/resource_manager.go b/pkg/api/v2/resource-management/resource_management.go similarity index 70% rename from pkg/api/v2/resource-manager/resource_manager.go rename to pkg/api/v2/resource-management/resource_management.go index 468a43c0..c0b12ddd 100644 --- a/pkg/api/v2/resource-manager/resource_manager.go +++ b/pkg/api/v2/resource-management/resource_management.go @@ -4,21 +4,21 @@ package resourcemanager import ( "github.com/SchwarzIT/community-stackit-go-client/internal/common" - "github.com/SchwarzIT/community-stackit-go-client/pkg/api/v2/resource-manager/organizations" - "github.com/SchwarzIT/community-stackit-go-client/pkg/api/v2/resource-manager/projects" + "github.com/SchwarzIT/community-stackit-go-client/pkg/api/v2/resource-management/organizations" + "github.com/SchwarzIT/community-stackit-go-client/pkg/api/v2/resource-management/projects" ) // New returns a new handler for the service -func New(c common.Client) *ResourceManagerService { - return &ResourceManagerService{ +func New(c common.Client) *ResourceManagementService { + return &ResourceManagementService{ Organizations: organizations.New(c), Projects: projects.New(c), } } -// ResourceManagerService is the service that handles +// ResourceManagementService is the service that handles // project, organization and folder related services -type ResourceManagerService struct { +type ResourceManagementService struct { Organizations *organizations.OrganizationsService Projects *projects.ProjectsService } diff --git a/pkg/consts/apis.go b/pkg/consts/apis.go index 360ce062..e3a858a3 100644 --- a/pkg/consts/apis.go +++ b/pkg/consts/apis.go @@ -88,17 +88,17 @@ const ( API_PATH_POSTGRES_FLEX_USERS = API_PATH_POSTGRES_FLEX_INSTANCE + "/users" API_PATH_POSTGRES_FLEX_USER = API_PATH_POSTGRES_FLEX_USERS + "/%s" - // Resource Management + // Resource Management v1 API_PATH_RESOURCE_MANAGEMENT = "/resource-management/v1" API_PATH_RESOURCE_MANAGEMENT_PROJECTS = API_PATH_RESOURCE_MANAGEMENT + "/projects/%s" API_PATH_RESOURCE_MANAGEMENT_ORG_PROJECTS = API_PATH_RESOURCE_MANAGEMENT + "/organizations/%s/projects" API_PATH_RESOURCE_MANAGEMENT_ORG_PROJECT = API_PATH_RESOURCE_MANAGEMENT_ORG_PROJECTS + "/%s" - // Resource Manager v2 - API_PATH_RESOURCE_MANAGER_V2 = "/resource-manager/v2" - API_PATH_RESOURCE_MANAGER_V2_PROJECTS = API_PATH_RESOURCE_MANAGER_V2 + "/projects" - API_PATH_RESOURCE_MANAGER_V2_PROJECT = API_PATH_RESOURCE_MANAGER_V2_PROJECTS + "/%s" - API_PATH_RESOURCE_MANAGER_V2_ORG = API_PATH_RESOURCE_MANAGER_V2 + "/organizations/%s" + // Resource Management v2 + API_PATH_RESOURCE_MANAGEMENT_V2 = "/resource-management/v2" + API_PATH_RESOURCE_MANAGEMENT_V2_PROJECTS = API_PATH_RESOURCE_MANAGEMENT_V2 + "/projects" + API_PATH_RESOURCE_MANAGEMENT_V2_PROJECT = API_PATH_RESOURCE_MANAGEMENT_V2_PROJECTS + "/%s" + API_PATH_RESOURCE_MANAGEMENT_V2_ORG = API_PATH_RESOURCE_MANAGEMENT_V2 + "/organizations/%s" // Shadow Users API_PATH_SHADOW_USERS = "/ucp-shadow-user-management/v1/createcuaashadowuser/user" diff --git a/pkg/consts/consts.go b/pkg/consts/consts.go index a0960858..fdcbb85c 100644 --- a/pkg/consts/consts.go +++ b/pkg/consts/consts.go @@ -7,6 +7,7 @@ const ( // schwarz specific constants SCHWARZ_ORGANIZATION_ID = "07a1ed91-2efb-42c2-9d00-e84ae71bce0d" + SCHWARZ_CONTAINER_ID = "schwarz-it-kg-WJACUK1" SCHWARZ_AUTH_ORIGIN = "schwarz-federation" // resource types @@ -24,10 +25,10 @@ const ( PROJECT_SCOPE_PRIVATE = "PRIVATE" // Project lifecycle statuses - PROJECT_STATUS_ACTIVE = "ACTIVE" - PROJECT_STATUS_CREATING = "CREATING" - PROJECT_STATUS_DELETING = "DELETING" - PROJECT_STATUS_NOT_SPECIFIED = "NOT_SPECIFIED" + PROJECT_STATUS_ACTIVE = "ACTIVE" + PROJECT_STATUS_CREATING = "CREATING" + PROJECT_STATUS_DELETING = "DELETING" + PROJECT_STATUS_INACTIVE = "INACTIVE" // SKE SKE_CLUSTERS_TAINT_EFFECT_NO_SCHED = "NoSchedule"