From 4347c6ea2d8f97de86258a66e9b4718929a16fe5 Mon Sep 17 00:00:00 2001 From: GokceGK <161626272+GokceGK@users.noreply.github.com> Date: Fri, 13 Sep 2024 09:57:08 +0200 Subject: [PATCH] Handle project members (#531) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * deprecate members field and make it valid only in creation * remove owner and members from datasource * Revert "remove owner and members from datasource" This reverts commit 31d2302166cc85abd84c2c72a0ac2ce6e70ec103. * update acc test * add creation limitation in members description --------- Co-authored-by: Gökçe Gök Klingel --- docs/resources/resourcemanager_project.md | 3 +- .../resourcemanager/project/datasource.go | 40 +-- .../resourcemanager/project/resource.go | 158 +-------- .../resourcemanager/project/resource_test.go | 311 ------------------ .../resourcemanager_acc_test.go | 16 +- 5 files changed, 39 insertions(+), 489 deletions(-) diff --git a/docs/resources/resourcemanager_project.md b/docs/resources/resourcemanager_project.md index dd17c357..b64b4e5f 100644 --- a/docs/resources/resourcemanager_project.md +++ b/docs/resources/resourcemanager_project.md @@ -24,18 +24,17 @@ resource "stackit_resourcemanager_project" "example" { ``` - ## Schema ### Required - `name` (String) Project name. +- `owner_email` (String) Email address of the owner of the project. This value is only considered during creation. Changing it afterwards will have no effect. - `parent_container_id` (String) Parent resource identifier. Both container ID (user-friendly) and UUID are supported ### Optional - `labels` (Map of String) Labels are key-value string pairs which can be attached to a resource container. A label key must match the regex [A-ZÄÜÖa-zäüöß0-9_-]{1,64}. A label value must match the regex ^$|[A-ZÄÜÖa-zäüöß0-9_-]{1,64} -- `owner_email` (String) Email address of the owner of the project. This value is only considered during creation. Changing it afterwards will have no effect. ### Read-Only diff --git a/stackit/internal/services/resourcemanager/project/datasource.go b/stackit/internal/services/resourcemanager/project/datasource.go index 665ae8b5..f6439ab3 100644 --- a/stackit/internal/services/resourcemanager/project/datasource.go +++ b/stackit/internal/services/resourcemanager/project/datasource.go @@ -102,17 +102,18 @@ func (d *projectDataSource) Configure(ctx context.Context, req datasource.Config // Schema defines the schema for the data source. func (d *projectDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { descriptions := map[string]string{ - "main": "Resource Manager project data source schema. To identify the project, you need to provider either project_id or container_id. If you provide both, project_id will be used.", - "id": "Terraform's internal data source. ID. It is structured as \"`container_id`\".", - "project_id": "Project UUID identifier. This is the ID that can be used in most of the other resources to identify the project.", - "container_id": "Project container ID. Globally unique, user-friendly identifier.", - "parent_container_id": "Parent resource identifier. Both container ID (user-friendly) and UUID are supported", - "name": "Project name.", - "labels": `Labels are key-value string pairs which can be attached to a resource container. A label key must match the regex [A-ZÄÜÖa-zäüöß0-9_-]{1,64}. A label value must match the regex ^$|[A-ZÄÜÖa-zäüöß0-9_-]{1,64}`, - "owner_email": "Email address of the owner of the project. This value is only considered during creation. Changing it afterwards will have no effect.", - "members": "The members assigned to the project. At least one subject needs to be a user, and not a client or service account.", - "members.role": fmt.Sprintf("The role of the member in the project. Legacy roles (%s) are not supported.", strings.Join(utils.QuoteValues(utils.LegacyProjectRoles), ", ")), - "members.subject": "Unique identifier of the user, service account or client. This is usually the email address for users or service accounts, and the name in case of clients.", + "main": "Resource Manager project data source schema. To identify the project, you need to provider either project_id or container_id. If you provide both, project_id will be used.", + "id": "Terraform's internal data source. ID. It is structured as \"`container_id`\".", + "project_id": "Project UUID identifier. This is the ID that can be used in most of the other resources to identify the project.", + "container_id": "Project container ID. Globally unique, user-friendly identifier.", + "parent_container_id": "Parent resource identifier. Both container ID (user-friendly) and UUID are supported", + "name": "Project name.", + "labels": `Labels are key-value string pairs which can be attached to a resource container. A label key must match the regex [A-ZÄÜÖa-zäüöß0-9_-]{1,64}. A label value must match the regex ^$|[A-ZÄÜÖa-zäüöß0-9_-]{1,64}`, + "owner_email": "Email address of the owner of the project. This value is only considered during creation. Changing it afterwards will have no effect.", + "members": "The members assigned to the project. At least one subject needs to be a user, and not a client or service account. This value is only considered during creation. Changing it afterwards will have no effect.", + "members.role": fmt.Sprintf("The role of the member in the project. Legacy roles (%s) are not supported.", strings.Join(utils.QuoteValues(utils.LegacyProjectRoles), ", ")), + "members.subject": "Unique identifier of the user, service account or client. This is usually the email address for users or service accounts, and the name in case of clients.", + "members_deprecation_message": "The \"members\" field has been deprecated in favor of the \"owner_email\" field. Please use the \"owner_email\" field to assign the owner role to a user.", } resp.Schema = schema.Schema{ @@ -173,8 +174,10 @@ func (d *projectDataSource) Schema(_ context.Context, _ datasource.SchemaRequest Optional: true, }, "members": schema.ListNestedAttribute{ - Description: descriptions["members"], - Computed: true, + Description: descriptions["members"], + DeprecationMessage: descriptions["members_deprecation_message"], + MarkdownDescription: fmt.Sprintf("%s\n\n!> %s", descriptions["members"], descriptions["members_deprecation_message"]), + Computed: true, NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "role": schema.StringAttribute{ @@ -237,17 +240,6 @@ func (d *projectDataSource) Read(ctx context.Context, req datasource.ReadRequest return } - membersResp, err := d.membershipClient.ListMembersExecute(ctx, projectResourceType, *projectResp.ProjectId) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading project", fmt.Sprintf("Reading members: %v", err)) - return - } - - err = mapMembersFields(ctx, membersResp.Members, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading project", fmt.Sprintf("Processing API response: %v", err)) - return - } diags = resp.State.Set(ctx, &model) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { diff --git a/stackit/internal/services/resourcemanager/project/resource.go b/stackit/internal/services/resourcemanager/project/resource.go index 522d813b..d654f138 100644 --- a/stackit/internal/services/resourcemanager/project/resource.go +++ b/stackit/internal/services/resourcemanager/project/resource.go @@ -147,17 +147,18 @@ func (r *projectResource) Configure(ctx context.Context, req resource.ConfigureR // Schema defines the schema for the resource. func (r *projectResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { descriptions := map[string]string{ - "main": "Resource Manager project resource schema. To use this resource, it is required that you set the service account email in the provider configuration.", - "id": "Terraform's internal resource ID. It is structured as \"`container_id`\".", - "project_id": "Project UUID identifier. This is the ID that can be used in most of the other resources to identify the project.", - "container_id": "Project container ID. Globally unique, user-friendly identifier.", - "parent_container_id": "Parent resource identifier. Both container ID (user-friendly) and UUID are supported", - "name": "Project name.", - "labels": "Labels are key-value string pairs which can be attached to a resource container. A label key must match the regex [A-ZÄÜÖa-zäüöß0-9_-]{1,64}. A label value must match the regex ^$|[A-ZÄÜÖa-zäüöß0-9_-]{1,64}", - "owner_email": "Email address of the owner of the project. This value is only considered during creation. Changing it afterwards will have no effect.", - "members": "The members assigned to the project. At least one subject needs to be a user, and not a client or service account.", - "members.role": fmt.Sprintf("The role of the member in the project. Possible values include, but are not limited to: `owner`, `editor`, `reader`. Legacy roles (%s) are not supported.", strings.Join(utils.QuoteValues(utils.LegacyProjectRoles), ", ")), - "members.subject": "Unique identifier of the user, service account or client. This is usually the email address for users or service accounts, and the name in case of clients.", + "main": "Resource Manager project resource schema. To use this resource, it is required that you set the service account email in the provider configuration.", + "id": "Terraform's internal resource ID. It is structured as \"`container_id`\".", + "project_id": "Project UUID identifier. This is the ID that can be used in most of the other resources to identify the project.", + "container_id": "Project container ID. Globally unique, user-friendly identifier.", + "parent_container_id": "Parent resource identifier. Both container ID (user-friendly) and UUID are supported", + "name": "Project name.", + "labels": "Labels are key-value string pairs which can be attached to a resource container. A label key must match the regex [A-ZÄÜÖa-zäüöß0-9_-]{1,64}. A label value must match the regex ^$|[A-ZÄÜÖa-zäüöß0-9_-]{1,64}", + "owner_email": "Email address of the owner of the project. This value is only considered during creation. Changing it afterwards will have no effect.", + "members": "The members assigned to the project. At least one subject needs to be a user, and not a client or service account. This value is only considered during creation. Changing it afterwards will have no effect.", + "members.role": fmt.Sprintf("The role of the member in the project. Possible values include, but are not limited to: `owner`, `editor`, `reader`. Legacy roles (%s) are not supported.", strings.Join(utils.QuoteValues(utils.LegacyProjectRoles), ", ")), + "members.subject": "Unique identifier of the user, service account or client. This is usually the email address for users or service accounts, and the name in case of clients.", + "members_deprecation_message": "The \"members\" field has been deprecated in favor of the \"owner_email\" field. Please use the \"owner_email\" field to assign the owner role to a user.", } resp.Schema = schema.Schema{ @@ -224,12 +225,13 @@ func (r *projectResource) Schema(_ context.Context, _ resource.SchemaRequest, re }, "owner_email": schema.StringAttribute{ Description: descriptions["owner_email"], - // When removing the owner_email field, we should mark the members field as required and add a listvalidator.SizeAtLeast(1) validator to it - Optional: true, + Required: true, }, "members": schema.ListNestedAttribute{ - Description: descriptions["members"], - Optional: true, + Description: descriptions["members"], + DeprecationMessage: descriptions["members_deprecation_message"], + MarkdownDescription: fmt.Sprintf("%s\n\n!> %s", descriptions["members"], descriptions["members_deprecation_message"]), + Optional: true, NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "role": schema.StringAttribute{ @@ -393,17 +395,6 @@ func (r *projectResource) Read(ctx context.Context, req resource.ReadRequest, re return } - membersResp, err := r.authorizationClient.ListMembersExecute(ctx, projectResourceType, *projectResp.ProjectId) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading project", fmt.Sprintf("Reading members: %v", err)) - return - } - - err = mapMembersFields(ctx, membersResp.Members, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading project", fmt.Sprintf("Processing API response: %v", err)) - return - } // Set refreshed model diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) @@ -451,23 +442,6 @@ func (r *projectResource) Update(ctx context.Context, req resource.UpdateRequest return } - members, err := toMembersPayload(ctx, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating project", fmt.Sprintf("Processing members: %v", err)) - return - } - - err = updateMembers(ctx, *projectResp.ProjectId, members, r.authorizationClient) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating project", fmt.Sprintf("Updating members: %v", err)) - return - } - - err = mapMembersFields(ctx, members, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating project", fmt.Sprintf("Processing API response: %v", err)) - return - } diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -690,7 +664,7 @@ func toMembersPayload(ctx context.Context, model *Model) (*[]authorization.Membe return nil, core.DiagsToError(diags) } - // If the new "members" fields is set, it has precedence over the deprecated "owner_email" field + // If the new "members" fields is set, it has precedence over the "owner_email" field members := []authorization.Member{} for _, m := range membersModel { members = append(members, authorization.Member{ @@ -756,102 +730,6 @@ func toUpdatePayload(model *Model) (*resourcemanager.PartialUpdateProjectPayload }, nil } -// updateMembers adds and removes members to match the model -func updateMembers(ctx context.Context, projectId string, modelMembers *[]authorization.Member, client *authorization.APIClient) error { - if modelMembers == nil || len(*modelMembers) == 0 { - return nil - } - - // Get current members - currentMembersResp, err := client.ListMembersExecute(ctx, projectResourceType, projectId) - if err != nil { - return fmt.Errorf("get members: %w", err) - } - - type memberState struct { - isInModel bool - isCreated bool - subject string - role string - } - - membersState := make(map[string]*memberState) // Key in the form of "subject,role" - for _, m := range *modelMembers { - mId := memberId(m) - membersState[mId] = &memberState{ - isInModel: true, - subject: *m.Subject, - role: *m.Role, - } - } - - for _, m := range *currentMembersResp.Members { - if utils.IsLegacyProjectRole(*m.Role) { - continue - } - - mId := memberId(m) - _, ok := membersState[mId] - if !ok { - membersState[mId] = &memberState{} - } - membersState[mId].isCreated = true - membersState[mId].subject = *m.Subject - membersState[mId].role = *m.Role - } - - // Add/remove members - membersToAdd := make([]authorization.Member, 0) - membersToRemove := make([]authorization.Member, 0) - for _, state := range membersState { - if state.isInModel && !state.isCreated { - m := authorization.Member{ - Subject: &state.subject, - Role: &state.role, - } - membersToAdd = append(membersToAdd, m) - - infoMsg := fmt.Sprintf("### Will add member to project: { role: %s, subject: %s }", state.role, state.subject) - tflog.Warn(ctx, infoMsg) - } - - if !state.isInModel && state.isCreated { - m := authorization.Member{ - Subject: &state.subject, - Role: &state.role, - } - membersToRemove = append(membersToRemove, m) - - infoMsg := fmt.Sprintf("### Will remove member from project: { role: %s, subject: %s }", state.role, state.subject) - tflog.Warn(ctx, infoMsg) - } - } - - if len(membersToAdd) > 0 { - payload := authorization.AddMembersPayload{ - Members: &membersToAdd, - ResourceType: sdkUtils.Ptr(projectResourceType), - } - _, err := client.AddMembers(ctx, projectId).AddMembersPayload(payload).Execute() - if err != nil { - return fmt.Errorf("add members: %w", err) - } - } - - if len(membersToRemove) > 0 { - payload := authorization.RemoveMembersPayload{ - Members: &membersToRemove, - ResourceType: sdkUtils.Ptr(projectResourceType), - } - _, err := client.RemoveMembers(ctx, projectId).RemoveMembersPayload(payload).Execute() - if err != nil { - return fmt.Errorf("remove members: %w", err) - } - } - - return nil -} - // Internal representation of a member, which is uniquely identified by the subject and role func memberId(member authorization.Member) string { return fmt.Sprintf("%s,%s", *member.Subject, *member.Role) diff --git a/stackit/internal/services/resourcemanager/project/resource_test.go b/stackit/internal/services/resourcemanager/project/resource_test.go index 4e60fc5a..8ae14a89 100644 --- a/stackit/internal/services/resourcemanager/project/resource_test.go +++ b/stackit/internal/services/resourcemanager/project/resource_test.go @@ -2,18 +2,13 @@ package project import ( "context" - "encoding/json" - "net/http" - "net/http/httptest" "testing" "github.com/google/go-cmp/cmp" "github.com/google/uuid" - "github.com/gorilla/mux" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/stackitcloud/stackit-sdk-go/core/config" "github.com/stackitcloud/stackit-sdk-go/core/utils" "github.com/stackitcloud/stackit-sdk-go/services/authorization" "github.com/stackitcloud/stackit-sdk-go/services/resourcemanager" @@ -607,309 +602,3 @@ func TestToUpdatePayload(t *testing.T) { }) } } - -var fixtureMembers = []authorization.Member{ - { - Subject: utils.Ptr("email_owner"), - Role: utils.Ptr("owner"), - }, - { - Subject: utils.Ptr("email_owner_2"), - Role: utils.Ptr("owner"), - }, - { - Subject: utils.Ptr("email_reader"), - Role: utils.Ptr("reader"), - }, -} - -func TestUpdateMembers(t *testing.T) { - // This is the response used when getting all members currently, across all tests - getAllMembersResp := authorization.MembersResponse{ - Members: &fixtureMembers, - } - getAllMembersRespBytes, err := json.Marshal(getAllMembersResp) - if err != nil { - t.Fatalf("Failed to marshal get all Members response: %v", err) - } - - // This is the response used whenever an API returns a failure response - failureRespBytes := []byte("{\"message\": \"Something bad happened\"") - - tests := []struct { - description string - modelMembers []authorization.Member - getAllMembersFails bool - addMembersFails bool - removeMembersFails bool - isValid bool - expectedMembersStates map[string]bool // Keys are member; value is true if member should exist at the end, false otherwise - }{ - { - description: "no changes", - modelMembers: fixtureMembers, - expectedMembersStates: map[string]bool{ - memberId(fixtureMembers[0]): true, - memberId(fixtureMembers[1]): true, - memberId(fixtureMembers[2]): true, - }, - isValid: true, - }, - { - description: "add one member", - modelMembers: append( - fixtureMembers, - authorization.Member{Subject: utils.Ptr("email_reader_2"), Role: utils.Ptr("reader")}, - ), - expectedMembersStates: map[string]bool{ - memberId(fixtureMembers[0]): true, - memberId(fixtureMembers[1]): true, - memberId(fixtureMembers[2]): true, - "email_reader_2,reader": true, - }, - isValid: true, - }, - { - description: "add multiple members", - modelMembers: append( - fixtureMembers, - authorization.Member{Subject: utils.Ptr("email_reader_2"), Role: utils.Ptr("reader")}, - authorization.Member{Subject: utils.Ptr("email_reader_3"), Role: utils.Ptr("reader")}, - ), - expectedMembersStates: map[string]bool{ - memberId(fixtureMembers[0]): true, - memberId(fixtureMembers[1]): true, - memberId(fixtureMembers[2]): true, - "email_reader_2,reader": true, - "email_reader_3,reader": true, - }, - isValid: true, - }, - { - description: "removing member", - modelMembers: fixtureMembers[:2], - expectedMembersStates: map[string]bool{ - memberId(fixtureMembers[0]): true, - memberId(fixtureMembers[1]): true, - memberId(fixtureMembers[2]): false, - }, - isValid: true, - }, - { - description: "removing multiple members", - modelMembers: fixtureMembers[:1], - expectedMembersStates: map[string]bool{ - memberId(fixtureMembers[0]): true, - memberId(fixtureMembers[1]): false, - memberId(fixtureMembers[2]): false, - }, - isValid: true, - }, - { - description: "multiple changes (add and remove)", - modelMembers: append( - fixtureMembers[:2], - authorization.Member{Subject: utils.Ptr("email_reader_2"), Role: utils.Ptr("reader")}, - authorization.Member{Subject: utils.Ptr("email_reader_3"), Role: utils.Ptr("reader")}, - ), - expectedMembersStates: map[string]bool{ - memberId(fixtureMembers[0]): true, - memberId(fixtureMembers[1]): true, - memberId(fixtureMembers[2]): false, - "email_reader_2,reader": true, - "email_reader_3,reader": true, - }, - isValid: true, - }, - { - description: "multiple changes 2 (add and remove)", - modelMembers: []authorization.Member{ - {Subject: utils.Ptr("email_reader_2"), Role: utils.Ptr("reader")}, - {Subject: utils.Ptr("email_reader_3"), Role: utils.Ptr("reader")}, - }, - expectedMembersStates: map[string]bool{ - memberId(fixtureMembers[0]): false, - memberId(fixtureMembers[1]): false, - memberId(fixtureMembers[2]): false, - "email_reader_2,reader": true, - "email_reader_3,reader": true, - }, - isValid: true, - }, - { - description: "get fails", - modelMembers: fixtureMembers, - getAllMembersFails: true, - isValid: false, - }, - { - description: "add fails", - modelMembers: append( - fixtureMembers, - authorization.Member{Subject: utils.Ptr("email_reader_2"), Role: utils.Ptr("reader")}, - ), - addMembersFails: true, - isValid: false, - }, - { - description: "remove fails", - modelMembers: fixtureMembers[:1], - removeMembersFails: true, - isValid: false, - }, - } - - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - // Will be compared to tt.expectedMembersStates at the end - membersStates := map[string]bool{ - memberId(fixtureMembers[0]): true, - memberId(fixtureMembers[1]): true, - memberId(fixtureMembers[2]): true, - } - - // Handler for getting all Members - getAllMembersHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - if tt.getAllMembersFails { - w.WriteHeader(http.StatusInternalServerError) - _, err := w.Write(failureRespBytes) - if err != nil { - t.Errorf("Get all Members handler: failed to write bad response: %v", err) - } - return - } - - _, err := w.Write(getAllMembersRespBytes) - if err != nil { - t.Errorf("Get all Members handler: failed to write response: %v", err) - } - }) - - // Handler for adding members - addMembersHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - decoder := json.NewDecoder(r.Body) - var payload authorization.AddMembersPayload - err := decoder.Decode(&payload) - if err != nil { - t.Errorf("Add members handler: failed to parse payload") - return - } - if payload.Members == nil { - t.Errorf("Add members handler: nil members") - return - } - members := *payload.Members - for _, m := range members { - if memberExists, memberWasAdded := membersStates[memberId(m)]; memberWasAdded && memberExists { - t.Errorf("Add members handler: attempted to add member '%v' that already exists", memberId(m)) - return - } - } - - w.Header().Set("Content-Type", "application/json") - if tt.addMembersFails { - w.WriteHeader(http.StatusInternalServerError) - _, err := w.Write(failureRespBytes) - if err != nil { - t.Errorf("Add members handler: failed to write bad response: %v", err) - } - return - } - - for _, m := range members { - membersStates[memberId(m)] = true - } - }) - - // Handler for removing members - removeMembersHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - decoder := json.NewDecoder(r.Body) - var payload authorization.RemoveMembersPayload - err := decoder.Decode(&payload) - if err != nil { - t.Errorf("Remove members handler: failed to parse payload") - return - } - if payload.Members == nil { - t.Errorf("Remove members handler: nil members") - return - } - members := *payload.Members - for _, m := range members { - memberExists, memberWasCreated := membersStates[memberId(m)] - if !memberWasCreated { - t.Errorf("Remove members handler: attempted to remove member '%v' that wasn't created", memberId(m)) - return - } - if memberWasCreated && !memberExists { - t.Errorf("Remove members handler: attempted to remove member '%v' that was already removed", memberId(m)) - return - } - } - - w.Header().Set("Content-Type", "application/json") - if tt.removeMembersFails { - w.WriteHeader(http.StatusInternalServerError) - _, err := w.Write(failureRespBytes) - if err != nil { - t.Errorf("Remove members handler: failed to write bad response: %v", err) - } - return - } - - for _, m := range members { - membersStates[memberId(m)] = false - } - }) - - // Setup server and client - router := mux.NewRouter() - router.HandleFunc("/v2/{resourceType}/{resourceId}/members", func(w http.ResponseWriter, r *http.Request) { - if r.Method == http.MethodGet { - getAllMembersHandler(w, r) - } else { - t.Fatalf("Unexpected method: %v", r.Method) - } - }) - router.HandleFunc("/v2/{resourceId}/members", func(w http.ResponseWriter, r *http.Request) { - if r.Method == http.MethodPatch { - addMembersHandler(w, r) - } else { - t.Fatalf("Unexpected method: %v", r.Method) - } - }) - router.HandleFunc("/v2/{resourceId}/members/remove", func(w http.ResponseWriter, r *http.Request) { - if r.Method == http.MethodPost { - removeMembersHandler(w, r) - } else { - t.Fatalf("Unexpected method: %v", r.Method) - } - }) - mockedServer := httptest.NewServer(router) - defer mockedServer.Close() - client, err := authorization.NewAPIClient( - config.WithEndpoint(mockedServer.URL), - config.WithoutAuthentication(), - ) - if err != nil { - t.Fatalf("Failed to initialize client: %v", err) - } - - // Run test - err = updateMembers(context.Background(), "pid", &tt.modelMembers, client) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(membersStates, tt.expectedMembersStates) - if diff != "" { - t.Fatalf("Member states do not match: %s", diff) - } - } - }) - } -} diff --git a/stackit/internal/services/resourcemanager/resourcemanager_acc_test.go b/stackit/internal/services/resourcemanager/resourcemanager_acc_test.go index 4877a4ae..5487eb6b 100644 --- a/stackit/internal/services/resourcemanager/resourcemanager_acc_test.go +++ b/stackit/internal/services/resourcemanager/resourcemanager_acc_test.go @@ -56,6 +56,7 @@ func resourceConfig(name string, label *string, members string) string { members = [ %[7]s ] + owner_email = "%[8]s" } resource "stackit_resourcemanager_project" "parent_by_uuid" { @@ -64,6 +65,7 @@ func resourceConfig(name string, label *string, members string) string { members = [ %[7]s ] + owner_email = "%[8]s" } `, testutil.ResourceManagerProviderConfig(), @@ -73,6 +75,7 @@ func resourceConfig(name string, label *string, members string) string { labelConfig, projectResource["parent_uuid"], members, + testutil.TestProjectServiceAccountEmail, ) } @@ -84,17 +87,6 @@ func TestAccResourceManagerResource(t *testing.T) { }, }) - updatedMembersConfig := membersConfig([]authorization.Member{ - { - Subject: &testutil.TestProjectUserEmail, - Role: utils.Ptr("owner"), - }, - { - Subject: &testutil.TestProjectUserEmail, - Role: utils.Ptr("reader"), - }, - }) - resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, CheckDestroy: testAccCheckResourceManagerDestroy, @@ -190,7 +182,7 @@ func TestAccResourceManagerResource(t *testing.T) { }, // Update { - Config: resourceConfig(fmt.Sprintf("%s-new", projectResource["name"]), utils.Ptr("a-label"), updatedMembersConfig), + Config: resourceConfig(fmt.Sprintf("%s-new", projectResource["name"]), utils.Ptr("a-label"), initialMembersConfig), Check: resource.ComposeAggregateTestCheckFunc( // Project data resource.TestCheckResourceAttrSet("stackit_resourcemanager_project.parent_by_container", "container_id"),