diff --git a/Makefile b/Makefile index cbba865d..d669945f 100644 --- a/Makefile +++ b/Makefile @@ -23,8 +23,6 @@ lint: lint-golangci-lint lint-tf generate-docs: @echo "Generating documentation with tfplugindocs" @$(SCRIPTS_BASE)/tfplugindocs.sh - # workaround until STACKITTPR-165 will be resolved - @git apply $(SCRIPTS_BASE)/docs.patch build: @go build -o bin/terraform-provider-stackit diff --git a/docs/data-sources/resourcemanager_project.md b/docs/data-sources/resourcemanager_project.md index 9fdabecd..d8a35809 100644 --- a/docs/data-sources/resourcemanager_project.md +++ b/docs/data-sources/resourcemanager_project.md @@ -25,7 +25,6 @@ data "stackit_resourcemanager_project" "example" { ### Optional - `container_id` (String) Project container ID. Globally unique, user-friendly identifier. -- `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. - `project_id` (String) Project UUID identifier. This is the ID that can be used in most of the other resources to identify the project. ### Read-Only diff --git a/scripts/docs.patch b/scripts/docs.patch deleted file mode 100644 index 314d505f..00000000 --- a/scripts/docs.patch +++ /dev/null @@ -1,46 +0,0 @@ -diff --git a/docs/data-sources/resourcemanager_project.md b/docs/data-sources/resourcemanager_project.md -index 0ca28db..9fdabec 100644 ---- a/docs/data-sources/resourcemanager_project.md -+++ b/docs/data-sources/resourcemanager_project.md -@@ -32,16 +32,5 @@ data "stackit_resourcemanager_project" "example" { - - - `id` (String) Terraform's internal data source. ID. It is structured as "`container_id`". - - `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} --- `members` (Attributes List, Deprecated) 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. -- --!> 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. (see [below for nested schema](#nestedatt--members)) - - `name` (String) Project name. - - `parent_container_id` (String) Parent resource identifier. Both container ID (user-friendly) and UUID are supported -- -- --### Nested Schema for `members` -- --Read-Only: -- --- `role` (String) The role of the member in the project. Legacy roles (`project.admin`, `project.auditor`, `project.member`, `project.owner`) are not supported. --- `subject` (String) 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. -diff --git a/docs/resources/resourcemanager_project.md b/docs/resources/resourcemanager_project.md -index ac601e3..ea0a70b 100644 ---- a/docs/resources/resourcemanager_project.md -+++ b/docs/resources/resourcemanager_project.md -@@ -36,20 +36,9 @@ resource "stackit_resourcemanager_project" "example" { - ### 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}. To add a project to a STACKIT Network Area, setting the label `networkArea=` is required. --- `members` (Attributes List, Deprecated) 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. -- --!> 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. (see [below for nested schema](#nestedatt--members)) - - ### Read-Only - - - `container_id` (String) Project container ID. Globally unique, user-friendly identifier. - - `id` (String) Terraform's internal resource ID. It is structured as "`container_id`". - - `project_id` (String) Project UUID identifier. This is the ID that can be used in most of the other resources to identify the project. -- -- --### Nested Schema for `members` -- --Required: -- --- `role` (String) The role of the member in the project. Possible values include, but are not limited to: `owner`, `editor`, `reader`. Legacy roles (`project.admin`, `project.auditor`, `project.member`, `project.owner`) are not supported. --- `subject` (String) 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. diff --git a/stackit/internal/services/resourcemanager/project/datasource.go b/stackit/internal/services/resourcemanager/project/datasource.go index 8a1bd0a5..8171a7c6 100644 --- a/stackit/internal/services/resourcemanager/project/datasource.go +++ b/stackit/internal/services/resourcemanager/project/datasource.go @@ -5,14 +5,12 @@ import ( "fmt" "net/http" "regexp" - "strings" "github.com/hashicorp/terraform-plugin-framework-validators/mapvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" "github.com/hashicorp/terraform-plugin-framework/datasource" @@ -20,7 +18,6 @@ import ( "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/stackit-sdk-go/core/config" "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - "github.com/stackitcloud/stackit-sdk-go/services/authorization" "github.com/stackitcloud/stackit-sdk-go/services/resourcemanager" ) @@ -36,8 +33,7 @@ func NewProjectDataSource() datasource.DataSource { // projectDataSource is the data source implementation. type projectDataSource struct { - resourceManagerClient *resourcemanager.APIClient - membershipClient *authorization.APIClient + client *resourcemanager.APIClient } // Metadata returns the data source type name. @@ -51,7 +47,7 @@ func (d *projectDataSource) Configure(ctx context.Context, req datasource.Config return } - var rmClient *resourcemanager.APIClient + var apiClient *resourcemanager.APIClient var err error providerData, ok := req.ProviderData.(core.ProviderData) if !ok { @@ -60,12 +56,12 @@ func (d *projectDataSource) Configure(ctx context.Context, req datasource.Config } if providerData.ResourceManagerCustomEndpoint != "" { - rmClient, err = resourcemanager.NewAPIClient( + apiClient, err = resourcemanager.NewAPIClient( config.WithCustomAuth(providerData.RoundTripper), config.WithEndpoint(providerData.ResourceManagerCustomEndpoint), ) } else { - rmClient, err = resourcemanager.NewAPIClient( + apiClient, err = resourcemanager.NewAPIClient( config.WithCustomAuth(providerData.RoundTripper), ) } @@ -74,44 +70,20 @@ func (d *projectDataSource) Configure(ctx context.Context, req datasource.Config return } - var aClient *authorization.APIClient - if providerData.AuthorizationCustomEndpoint != "" { - ctx = tflog.SetField(ctx, "authorization_custom_endpoint", providerData.AuthorizationCustomEndpoint) - aClient, err = authorization.NewAPIClient( - config.WithCustomAuth(providerData.RoundTripper), - config.WithEndpoint(providerData.AuthorizationCustomEndpoint), - ) - } else { - aClient, err = authorization.NewAPIClient( - config.WithCustomAuth(providerData.RoundTripper), - ) - } - - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring Membership API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration", err)) - return - } - - d.resourceManagerClient = rmClient - d.membershipClient = aClient + d.client = apiClient tflog.Info(ctx, "Resource Manager project client configured") } // 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. 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.", + "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}`, } resp.Schema = schema.Schema{ @@ -167,31 +139,6 @@ func (d *projectDataSource) Schema(_ context.Context, _ datasource.SchemaRequest ), }, }, - "owner_email": schema.StringAttribute{ - Description: descriptions["owner_email"], - Optional: true, - }, - "members": schema.ListNestedAttribute{ - 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{ - Description: descriptions["members.role"], - Computed: true, - Validators: []validator.String{ - validate.NonLegacyProjectRole(), - }, - }, - "subject": schema.StringAttribute{ - Description: descriptions["members.subject"], - Computed: true, - }, - }, - }, - }, }, } } @@ -222,7 +169,7 @@ func (d *projectDataSource) Read(ctx context.Context, req datasource.ReadRequest identifier = projectId } - projectResp, err := d.resourceManagerClient.GetProject(ctx, identifier).Execute() + projectResp, err := d.client.GetProject(ctx, identifier).Execute() if err != nil { oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped if ok && oapiErr.StatusCode == http.StatusForbidden { diff --git a/stackit/internal/services/resourcemanager/project/resource.go b/stackit/internal/services/resourcemanager/project/resource.go index 43163f62..c73488d2 100644 --- a/stackit/internal/services/resourcemanager/project/resource.go +++ b/stackit/internal/services/resourcemanager/project/resource.go @@ -9,22 +9,18 @@ import ( "github.com/google/uuid" "github.com/hashicorp/terraform-plugin-framework-validators/mapvalidator" - "github.com/hashicorp/terraform-plugin-framework-validators/resourcevalidator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" - "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" "github.com/hashicorp/terraform-plugin-framework/types" @@ -32,7 +28,6 @@ import ( "github.com/stackitcloud/stackit-sdk-go/core/config" "github.com/stackitcloud/stackit-sdk-go/core/oapierror" sdkUtils "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/authorization" "github.com/stackitcloud/stackit-sdk-go/services/resourcemanager" "github.com/stackitcloud/stackit-sdk-go/services/resourcemanager/wait" ) @@ -45,8 +40,7 @@ var ( ) const ( - projectResourceType = "project" - projectOwnerRole = "owner" + projectOwnerRole = "owner" ) type Model struct { @@ -56,20 +50,11 @@ type Model struct { ContainerParentId types.String `tfsdk:"parent_container_id"` Name types.String `tfsdk:"name"` Labels types.Map `tfsdk:"labels"` - OwnerEmail types.String `tfsdk:"owner_email"` - Members types.List `tfsdk:"members"` } -// Struct corresponding to Model.Members[i] -type member struct { - Role types.String `tfsdk:"role"` - Subject types.String `tfsdk:"subject"` -} - -// Types corresponding to member -var memberTypes = map[string]attr.Type{ - "role": types.StringType, - "subject": types.StringType, +type ResourceModel struct { + Model + OwnerEmail types.String `tfsdk:"owner_email"` } // NewProjectResource is a helper function to simplify the provider implementation. @@ -79,8 +64,7 @@ func NewProjectResource() resource.Resource { // projectResource is the resource implementation. type projectResource struct { - resourceManagerClient *resourcemanager.APIClient - authorizationClient *authorization.APIClient + client *resourcemanager.APIClient } // Metadata returns the resource type name. @@ -101,16 +85,16 @@ func (r *projectResource) Configure(ctx context.Context, req resource.ConfigureR return } - var rmClient *resourcemanager.APIClient + var apiClient *resourcemanager.APIClient var err error if providerData.ResourceManagerCustomEndpoint != "" { ctx = tflog.SetField(ctx, "resourcemanager_custom_endpoint", providerData.ResourceManagerCustomEndpoint) - rmClient, err = resourcemanager.NewAPIClient( + apiClient, err = resourcemanager.NewAPIClient( config.WithCustomAuth(providerData.RoundTripper), config.WithEndpoint(providerData.ResourceManagerCustomEndpoint), ) } else { - rmClient, err = resourcemanager.NewAPIClient( + apiClient, err = resourcemanager.NewAPIClient( config.WithCustomAuth(providerData.RoundTripper), ) } @@ -120,44 +104,21 @@ func (r *projectResource) Configure(ctx context.Context, req resource.ConfigureR return } - var aClient *authorization.APIClient - if providerData.AuthorizationCustomEndpoint != "" { - ctx = tflog.SetField(ctx, "authorization_custom_endpoint", providerData.AuthorizationCustomEndpoint) - aClient, err = authorization.NewAPIClient( - config.WithCustomAuth(providerData.RoundTripper), - config.WithEndpoint(providerData.AuthorizationCustomEndpoint), - ) - } else { - aClient, err = authorization.NewAPIClient( - config.WithCustomAuth(providerData.RoundTripper), - ) - } - - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring Membership API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration", err)) - return - } - - r.resourceManagerClient = rmClient - r.authorizationClient = aClient + r.client = apiClient tflog.Info(ctx, "Resource Manager project client configured") } // 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}. To add a project to a STACKIT Network Area, setting the label `networkArea=` is required.", - "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.", + "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}. To add a project to a STACKIT Network Area, setting the label `networkArea=` is required.", + "owner_email": "Email address of the owner of the project. This value is only considered during creation. Changing it afterwards will have no effect.", } resp.Schema = schema.Schema{ @@ -226,47 +187,13 @@ func (r *projectResource) Schema(_ context.Context, _ resource.SchemaRequest, re Description: descriptions["owner_email"], Required: true, }, - "members": schema.ListNestedAttribute{ - Description: descriptions["members"], - DeprecationMessage: descriptions["members_deprecation_message"], - MarkdownDescription: fmt.Sprintf("%s\n\n!> %s", descriptions["members"], descriptions["members_deprecation_message"]), - Optional: true, - PlanModifiers: []planmodifier.List{ - listplanmodifier.RequiresReplace(), - }, - NestedObject: schema.NestedAttributeObject{ - Attributes: map[string]schema.Attribute{ - "role": schema.StringAttribute{ - Description: descriptions["members.role"], - Required: true, - Validators: []validator.String{ - validate.NonLegacyProjectRole(), - }, - }, - "subject": schema.StringAttribute{ - Description: descriptions["members.subject"], - Required: true, - }, - }, - }, - }, }, } } -// ConfigValidators validates the resource configuration -func (r *projectResource) ConfigValidators(_ context.Context) []resource.ConfigValidator { - return []resource.ConfigValidator{ - resourcevalidator.AtLeastOneOf( - path.MatchRoot("owner_email"), - path.MatchRoot("members"), - ), - } -} - // Create creates the resource and sets the initial Terraform state. func (r *projectResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform - var model Model + var model ResourceModel diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -277,44 +204,33 @@ func (r *projectResource) Create(ctx context.Context, req resource.CreateRequest ctx = tflog.SetField(ctx, "project_container_id", containerId) // Generate API request body from model - payload, err := toCreatePayload(ctx, &model) + payload, err := toCreatePayload(&model) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating project", fmt.Sprintf("Creating API payload: %v", err)) return } // Create new project - createResp, err := r.resourceManagerClient.CreateProject(ctx).CreateProjectPayload(*payload).Execute() + createResp, err := r.client.CreateProject(ctx).CreateProjectPayload(*payload).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating project", fmt.Sprintf("Calling API: %v", err)) return } respContainerId := *createResp.ContainerId - // If the request has not been processed yet and the containerId doesnt exist, + // If the request has not been processed yet and the containerId doesn't exist, // the waiter will fail with authentication error, so wait some time before checking the creation - waitResp, err := wait.CreateProjectWaitHandler(ctx, r.resourceManagerClient, respContainerId).WaitWithContext(ctx) + waitResp, err := wait.CreateProjectWaitHandler(ctx, r.client, respContainerId).WaitWithContext(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating project", fmt.Sprintf("Instance creation waiting: %v", err)) return } - err = mapProjectFields(ctx, waitResp, &model, &resp.State) + err = mapProjectFields(ctx, waitResp, &model.Model, &resp.State) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating project", fmt.Sprintf("Processing API response: %v", err)) return } - membersResp, err := r.authorizationClient.ListMembersExecute(ctx, projectResourceType, *waitResp.ProjectId) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating project", fmt.Sprintf("Reading members: %v", err)) - return - } - - err = mapMembersFields(ctx, membersResp.Members, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating project", fmt.Sprintf("Processing API payload: %v", err)) - return - } // Set state to fully populated data diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) @@ -326,7 +242,7 @@ func (r *projectResource) Create(ctx context.Context, req resource.CreateRequest // Read refreshes the Terraform state with the latest data. func (r *projectResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model + var model ResourceModel diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -335,7 +251,7 @@ func (r *projectResource) Read(ctx context.Context, req resource.ReadRequest, re containerId := model.ContainerId.ValueString() ctx = tflog.SetField(ctx, "container_id", containerId) - projectResp, err := r.resourceManagerClient.GetProject(ctx, containerId).Execute() + projectResp, err := r.client.GetProject(ctx, containerId).Execute() if err != nil { oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped if ok && oapiErr.StatusCode == http.StatusForbidden { @@ -346,7 +262,7 @@ func (r *projectResource) Read(ctx context.Context, req resource.ReadRequest, re return } - err = mapProjectFields(ctx, projectResp, &model, &resp.State) + err = mapProjectFields(ctx, projectResp, &model.Model, &resp.State) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading project", fmt.Sprintf("Processing API response: %v", err)) return @@ -364,7 +280,7 @@ func (r *projectResource) Read(ctx context.Context, req resource.ReadRequest, re // Update updates the resource and sets the updated Terraform state on success. func (r *projectResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform // Retrieve values from plan - var model Model + var model ResourceModel diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -380,20 +296,20 @@ func (r *projectResource) Update(ctx context.Context, req resource.UpdateRequest return } // Update existing project - _, err = r.resourceManagerClient.PartialUpdateProject(ctx, containerId).PartialUpdateProjectPayload(*payload).Execute() + _, err = r.client.PartialUpdateProject(ctx, containerId).PartialUpdateProjectPayload(*payload).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating project", fmt.Sprintf("Calling API: %v", err)) return } // Fetch updated project - projectResp, err := r.resourceManagerClient.GetProject(ctx, containerId).Execute() + projectResp, err := r.client.GetProject(ctx, containerId).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating project", fmt.Sprintf("Calling API for updated data: %v", err)) return } - err = mapProjectFields(ctx, projectResp, &model, &resp.State) + err = mapProjectFields(ctx, projectResp, &model.Model, &resp.State) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating project", fmt.Sprintf("Processing API response: %v", err)) return @@ -410,7 +326,7 @@ func (r *projectResource) Update(ctx context.Context, req resource.UpdateRequest // Delete deletes the resource and removes the Terraform state on success. func (r *projectResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform // Retrieve values from state - var model Model + var model ResourceModel diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -421,13 +337,13 @@ func (r *projectResource) Delete(ctx context.Context, req resource.DeleteRequest ctx = tflog.SetField(ctx, "container_id", containerId) // Delete existing project - err := r.resourceManagerClient.DeleteProject(ctx, containerId).Execute() + err := r.client.DeleteProject(ctx, containerId).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting project", fmt.Sprintf("Calling API: %v", err)) return } - _, err = wait.DeleteProjectWaitHandler(ctx, r.resourceManagerClient, containerId).WaitWithContext(ctx) + _, err = wait.DeleteProjectWaitHandler(ctx, r.client, containerId).WaitWithContext(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting project", fmt.Sprintf("Instance deletion waiting: %v", err)) return @@ -527,133 +443,31 @@ func mapProjectFields(ctx context.Context, projectResp *resourcemanager.GetProje return nil } -func mapMembersFields(ctx context.Context, members *[]authorization.Member, model *Model) error { - if members == nil { - model.Members = types.ListNull(types.ObjectType{AttrTypes: memberTypes}) - return nil - } - - if (model.Members.IsNull() || model.Members.IsUnknown()) && !model.OwnerEmail.IsNull() { - // If the new "members" field is not set and the deprecated "owner_email" field is set, - // we keep the old behavior and do map the members to avoid an inconsistent result after apply error - model.Members = types.ListNull(types.ObjectType{AttrTypes: memberTypes}) - return nil - } - - modelMembers := []member{} - if !(model.Members.IsNull() || model.Members.IsUnknown()) { - diags := model.Members.ElementsAs(ctx, &modelMembers, false) - if diags.HasError() { - return fmt.Errorf("processing members: %w", core.DiagsToError(diags)) - } - } - modelMemberIds := make([]string, len(modelMembers)) - for i, m := range modelMembers { - modelMemberIds[i] = memberId(authorization.Member{ - Role: m.Role.ValueStringPointer(), - Subject: m.Subject.ValueStringPointer(), - }) - } - - apiMemberIds := []string{} - for _, m := range *members { - if utils.IsLegacyProjectRole(*m.Role) { - continue - } - apiMemberIds = append(apiMemberIds, memberId(m)) - } - - reconciledMembersIds := utils.ReconcileStringSlices(modelMemberIds, apiMemberIds) - - membersList := []attr.Value{} - for i, m := range reconciledMembersIds { - role := roleFromId(m) - subject := subjectFromId(m) - if role == "" || subject == "" { - return fmt.Errorf("reconcile list of members") - } - - membersMap := map[string]attr.Value{ - "subject": types.StringValue(subject), - "role": types.StringValue(role), - } - - memberTF, diags := types.ObjectValue(memberTypes, membersMap) - if diags.HasError() { - return fmt.Errorf("mapping index %d: %w", i, core.DiagsToError(diags)) - } - - membersList = append(membersList, memberTF) - } - - membersTF, diags := types.ListValue( - types.ObjectType{AttrTypes: memberTypes}, - membersList, - ) - if diags.HasError() { - return core.DiagsToError(diags) - } - - model.Members = membersTF - return nil -} - -func toMembersPayload(ctx context.Context, model *Model) (*[]authorization.Member, error) { +func toMembersPayload(model *ResourceModel) (*[]resourcemanager.Member, error) { if model == nil { return nil, fmt.Errorf("nil model") } - if model.Members.IsNull() || model.Members.IsUnknown() { - if model.OwnerEmail.IsNull() { - return nil, fmt.Errorf("members and owner_email are both null or unknown") - } - - return &[]authorization.Member{ - { - Subject: model.OwnerEmail.ValueStringPointer(), - Role: sdkUtils.Ptr(projectOwnerRole), - }, - }, nil + if model.OwnerEmail.IsNull() { + return nil, fmt.Errorf("owner_email is null") } - membersModel := []member{} - diags := model.Members.ElementsAs(ctx, &membersModel, false) - if diags.HasError() { - return nil, core.DiagsToError(diags) - } - - // 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{ - Role: m.Role.ValueStringPointer(), - Subject: m.Subject.ValueStringPointer(), - }) - } - - return &members, nil + return &[]resourcemanager.Member{ + { + Subject: model.OwnerEmail.ValueStringPointer(), + Role: sdkUtils.Ptr(projectOwnerRole), + }, + }, nil } -func toCreatePayload(ctx context.Context, model *Model) (*resourcemanager.CreateProjectPayload, error) { +func toCreatePayload(model *ResourceModel) (*resourcemanager.CreateProjectPayload, error) { if model == nil { return nil, fmt.Errorf("nil model") } - members, err := toMembersPayload(ctx, model) + members, err := toMembersPayload(model) if err != nil { return nil, fmt.Errorf("processing members: %w", err) } - var convertedMembers []resourcemanager.Member - for _, m := range *members { - convertedMembers = append(convertedMembers, - resourcemanager.Member{ - Subject: m.Subject, - Role: m.Role, - }) - } - var membersPayload *[]resourcemanager.Member - if len(convertedMembers) > 0 { - membersPayload = &convertedMembers - } modelLabels := model.Labels.Elements() labels, err := conversion.ToOptStringMap(modelLabels) @@ -664,12 +478,12 @@ func toCreatePayload(ctx context.Context, model *Model) (*resourcemanager.Create return &resourcemanager.CreateProjectPayload{ ContainerParentId: conversion.StringValueToPointer(model.ContainerParentId), Labels: labels, - Members: membersPayload, + Members: members, Name: conversion.StringValueToPointer(model.Name), }, nil } -func toUpdatePayload(model *Model) (*resourcemanager.PartialUpdateProjectPayload, error) { +func toUpdatePayload(model *ResourceModel) (*resourcemanager.PartialUpdateProjectPayload, error) { if model == nil { return nil, fmt.Errorf("nil model") } @@ -686,26 +500,3 @@ func toUpdatePayload(model *Model) (*resourcemanager.PartialUpdateProjectPayload Labels: labels, }, 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) -} - -// Extract the role from the member ID representation -func roleFromId(id string) string { - parts := strings.Split(id, ",") - if len(parts) != 2 { - return "" - } - return parts[1] -} - -// Extract the subject from the member ID representation -func subjectFromId(id string) string { - parts := strings.Split(id, ",") - if len(parts) != 2 { - return "" - } - return parts[0] -} diff --git a/stackit/internal/services/resourcemanager/project/resource_test.go b/stackit/internal/services/resourcemanager/project/resource_test.go index 8ae14a89..2e6b49df 100644 --- a/stackit/internal/services/resourcemanager/project/resource_test.go +++ b/stackit/internal/services/resourcemanager/project/resource_test.go @@ -2,15 +2,13 @@ package project import ( "context" + "reflect" "testing" "github.com/google/go-cmp/cmp" "github.com/google/uuid" - "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/utils" - "github.com/stackitcloud/stackit-sdk-go/services/authorization" "github.com/stackitcloud/stackit-sdk-go/services/resourcemanager" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" ) @@ -38,7 +36,6 @@ func TestMapProjectFields(t *testing.T) { ProjectId: types.StringValue("pid"), ContainerParentId: types.StringNull(), Name: types.StringNull(), - Members: types.ListNull(types.ObjectType{AttrTypes: memberTypes}), }, nil, true, @@ -65,7 +62,6 @@ func TestMapProjectFields(t *testing.T) { ProjectId: types.StringValue("pid"), ContainerParentId: types.StringValue("parent_cid"), Name: types.StringValue("name"), - Members: types.ListNull(types.ObjectType{AttrTypes: memberTypes}), }, &map[string]string{ "label1": "ref1", @@ -95,7 +91,6 @@ func TestMapProjectFields(t *testing.T) { ProjectId: types.StringValue("pid"), ContainerParentId: types.StringValue(testUUID), Name: types.StringValue("name"), - Members: types.ListNull(types.ObjectType{AttrTypes: memberTypes}), }, &map[string]string{ "label1": "ref1", @@ -138,7 +133,6 @@ func TestMapProjectFields(t *testing.T) { model := &Model{ ContainerId: tt.expected.ContainerId, ContainerParentId: containerParentId, - Members: types.ListNull(types.ObjectType{AttrTypes: memberTypes}), } err := mapProjectFields(context.Background(), tt.projectResp, model, nil) @@ -158,195 +152,22 @@ func TestMapProjectFields(t *testing.T) { } } -func TestMapMembersFields(t *testing.T) { - tests := []struct { - description string - configMembers basetypes.ListValue - membersResp *[]authorization.Member - expected Model - expectedLabels *map[string]string - isValid bool - }{ - { - "default_ok", - types.ListNull(types.ObjectType{AttrTypes: memberTypes}), - &[]authorization.Member{ - { - Subject: utils.Ptr("owner_email"), - Role: utils.Ptr("owner"), - }, - { - Subject: utils.Ptr("reader_email"), - Role: utils.Ptr("reader"), - }, - }, - Model{ - Id: types.StringNull(), - ProjectId: types.StringNull(), - ContainerId: types.StringNull(), - ContainerParentId: types.StringNull(), - Name: types.StringNull(), - Labels: types.MapNull(types.StringType), - Members: types.ListValueMust(types.ObjectType{AttrTypes: memberTypes}, []attr.Value{ - types.ObjectValueMust( - memberTypes, - map[string]attr.Value{ - "subject": types.StringValue("owner_email"), - "role": types.StringValue("owner"), - }, - ), - types.ObjectValueMust( - memberTypes, - map[string]attr.Value{ - "subject": types.StringValue("reader_email"), - "role": types.StringValue("reader"), - }, - ), - }), - }, - nil, - true, - }, - { - "default_ok (preserve model order)", - types.ListValueMust(types.ObjectType{AttrTypes: memberTypes}, []attr.Value{ - types.ObjectValueMust( - memberTypes, - map[string]attr.Value{ - "subject": types.StringValue("reader_email"), - "role": types.StringValue("reader"), - }, - ), - types.ObjectValueMust( - memberTypes, - map[string]attr.Value{ - "subject": types.StringValue("owner_email"), - "role": types.StringValue("owner"), - }, - ), - }), - &[]authorization.Member{ - { - Subject: utils.Ptr("owner_email"), - Role: utils.Ptr("owner"), - }, - { - Subject: utils.Ptr("reader_email"), - Role: utils.Ptr("reader"), - }, - }, - Model{ - Id: types.StringNull(), - ProjectId: types.StringNull(), - ContainerId: types.StringNull(), - ContainerParentId: types.StringNull(), - Name: types.StringNull(), - Labels: types.MapNull(types.StringType), - Members: types.ListValueMust(types.ObjectType{AttrTypes: memberTypes}, []attr.Value{ - types.ObjectValueMust( - memberTypes, - map[string]attr.Value{ - "subject": types.StringValue("reader_email"), - "role": types.StringValue("reader"), - }, - ), - types.ObjectValueMust( - memberTypes, - map[string]attr.Value{ - "subject": types.StringValue("owner_email"), - "role": types.StringValue("owner"), - }, - ), - }), - }, - nil, - true, - }, - { - "empty members", - types.ListNull(types.ObjectType{AttrTypes: memberTypes}), - &[]authorization.Member{}, - Model{ - Id: types.StringNull(), - ProjectId: types.StringNull(), - ContainerId: types.StringNull(), - ContainerParentId: types.StringNull(), - Name: types.StringNull(), - Labels: types.MapNull(types.StringType), - Members: types.ListValueMust(types.ObjectType{AttrTypes: memberTypes}, []attr.Value{}), - }, - nil, - true, - }, - { - "nil members", - types.ListNull(types.ObjectType{AttrTypes: memberTypes}), - nil, - Model{ - Id: types.StringNull(), - ProjectId: types.StringNull(), - ContainerId: types.StringNull(), - ContainerParentId: types.StringNull(), - Name: types.StringNull(), - Members: types.ListNull(types.ObjectType{AttrTypes: memberTypes}), - Labels: types.MapNull(types.StringType), - }, - nil, - true, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - state := &Model{ - Id: types.StringNull(), - ProjectId: types.StringNull(), - ContainerId: types.StringNull(), - ContainerParentId: types.StringNull(), - Name: types.StringNull(), - Labels: types.MapNull(types.StringType), - } - if !tt.configMembers.IsNull() { - state.Members = tt.configMembers - } - err := mapMembersFields(context.Background(), tt.membersResp, state) - 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(state, &tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - func TestToCreatePayload(t *testing.T) { tests := []struct { description string - input *Model + input *ResourceModel inputLabels *map[string]string expected *resourcemanager.CreateProjectPayload isValid bool }{ { - "mapping_with_conversions_single_member", - &Model{ - ContainerParentId: types.StringValue("pid"), - Name: types.StringValue("name"), - Members: types.ListValueMust(types.ObjectType{AttrTypes: memberTypes}, []attr.Value{ - types.ObjectValueMust( - memberTypes, - map[string]attr.Value{ - "subject": types.StringValue("owner_email"), - "role": types.StringValue("owner"), - }, - ), - }), + "mapping_with_conversions", + &ResourceModel{ + Model: Model{ + ContainerParentId: types.StringValue("pid"), + Name: types.StringValue("name"), + }, + OwnerEmail: types.StringValue("john.doe@stackit.cloud"), }, &map[string]string{ "label1": "1", @@ -360,7 +181,7 @@ func TestToCreatePayload(t *testing.T) { }, Members: &[]resourcemanager.Member{ { - Subject: utils.Ptr("owner_email"), + Subject: utils.Ptr("john.doe@stackit.cloud"), Role: utils.Ptr("owner"), }, }, @@ -369,119 +190,12 @@ func TestToCreatePayload(t *testing.T) { true, }, { - "mapping_with_conversions_ok_multiple_members", - &Model{ - ContainerParentId: types.StringValue("pid"), - Name: types.StringValue("name"), - Members: types.ListValueMust(types.ObjectType{AttrTypes: memberTypes}, []attr.Value{ - types.ObjectValueMust( - memberTypes, - map[string]attr.Value{ - "subject": types.StringValue("owner_email"), - "role": types.StringValue("owner"), - }, - ), - types.ObjectValueMust( - memberTypes, - map[string]attr.Value{ - "subject": types.StringValue("reader_email"), - "role": types.StringValue("reader"), - }, - ), - }), - }, - &map[string]string{ - "label1": "1", - "label2": "2", - }, - &resourcemanager.CreateProjectPayload{ - ContainerParentId: utils.Ptr("pid"), - Labels: &map[string]string{ - "label1": "1", - "label2": "2", + "no owner_email fails", + &ResourceModel{ + Model: Model{ + ContainerParentId: types.StringValue("pid"), + Name: types.StringValue("name"), }, - Members: &[]resourcemanager.Member{ - { - Subject: utils.Ptr("owner_email"), - Role: utils.Ptr("owner"), - }, - { - Subject: utils.Ptr("reader_email"), - Role: utils.Ptr("reader"), - }, - }, - Name: utils.Ptr("name"), - }, - true, - }, - { - "new members field takes precedence over deprecated owner_email field", - &Model{ - ContainerParentId: types.StringValue("pid"), - Name: types.StringValue("name"), - OwnerEmail: types.StringValue("some_email_deprecated"), - Members: types.ListValueMust(types.ObjectType{AttrTypes: memberTypes}, []attr.Value{ - types.ObjectValueMust( - memberTypes, - map[string]attr.Value{ - "subject": types.StringValue("owner_email"), - "role": types.StringValue("owner"), - }, - ), - }), - }, - &map[string]string{ - "label1": "1", - "label2": "2", - }, - &resourcemanager.CreateProjectPayload{ - ContainerParentId: utils.Ptr("pid"), - Labels: &map[string]string{ - "label1": "1", - "label2": "2", - }, - Members: &[]resourcemanager.Member{ - { - Subject: utils.Ptr("owner_email"), - Role: utils.Ptr("owner"), - }, - }, - Name: utils.Ptr("name"), - }, - true, - }, - { - "deprecated owner_email field still works", - &Model{ - ContainerParentId: types.StringValue("pid"), - Name: types.StringValue("name"), - OwnerEmail: types.StringValue("some_email_deprecated"), - }, - &map[string]string{ - "label1": "1", - "label2": "2", - }, - &resourcemanager.CreateProjectPayload{ - ContainerParentId: utils.Ptr("pid"), - Labels: &map[string]string{ - "label1": "1", - "label2": "2", - }, - Members: &[]resourcemanager.Member{ - { - Subject: utils.Ptr("some_email_deprecated"), - Role: utils.Ptr("owner"), - }, - }, - Name: utils.Ptr("name"), - }, - true, - }, - { - "no members or owner_email fails", - &Model{ - ContainerParentId: types.StringValue("pid"), - Name: types.StringValue("name"), }, &map[string]string{}, nil, @@ -508,7 +222,7 @@ func TestToCreatePayload(t *testing.T) { tt.input.Labels = convertedLabels } } - output, err := toCreatePayload(context.Background(), tt.input) + output, err := toCreatePayload(tt.input) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } @@ -528,14 +242,14 @@ func TestToCreatePayload(t *testing.T) { func TestToUpdatePayload(t *testing.T) { tests := []struct { description string - input *Model + input *ResourceModel inputLabels *map[string]string expected *resourcemanager.PartialUpdateProjectPayload isValid bool }{ { "default_ok", - &Model{}, + &ResourceModel{}, nil, &resourcemanager.PartialUpdateProjectPayload{ ContainerParentId: nil, @@ -546,10 +260,12 @@ func TestToUpdatePayload(t *testing.T) { }, { "mapping_with_conversions_ok", - &Model{ - ContainerParentId: types.StringValue("pid"), - Name: types.StringValue("name"), - OwnerEmail: types.StringValue("owner_email"), + &ResourceModel{ + Model: Model{ + ContainerParentId: types.StringValue("pid"), + Name: types.StringValue("name"), + }, + OwnerEmail: types.StringValue("owner_email"), }, &map[string]string{ "label1": "1", @@ -602,3 +318,57 @@ func TestToUpdatePayload(t *testing.T) { }) } } + +func TestToMembersPayload(t *testing.T) { + type args struct { + model *ResourceModel + } + tests := []struct { + name string + args args + want *[]resourcemanager.Member + wantErr bool + }{ + { + name: "missing model", + args: args{}, + want: nil, + wantErr: true, + }, + { + name: "empty model", + args: args{ + model: &ResourceModel{}, + }, + want: nil, + wantErr: true, + }, + { + name: "ok", + args: args{ + model: &ResourceModel{ + OwnerEmail: types.StringValue("john.doe@stackit.cloud"), + }, + }, + want: &[]resourcemanager.Member{ + { + Subject: utils.Ptr("john.doe@stackit.cloud"), + Role: utils.Ptr("owner"), + }, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := toMembersPayload(tt.args.model) + if (err != nil) != tt.wantErr { + t.Errorf("toMembersPayload() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("toMembersPayload() got = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/stackit/internal/services/resourcemanager/resourcemanager_acc_test.go b/stackit/internal/services/resourcemanager/resourcemanager_acc_test.go index 5487eb6b..7fb666d2 100644 --- a/stackit/internal/services/resourcemanager/resourcemanager_acc_test.go +++ b/stackit/internal/services/resourcemanager/resourcemanager_acc_test.go @@ -3,7 +3,6 @@ package resourcemanager_test import ( "context" "fmt" - "strings" "testing" "github.com/hashicorp/terraform-plugin-testing/helper/acctest" @@ -11,7 +10,6 @@ import ( "github.com/hashicorp/terraform-plugin-testing/terraform" "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" "github.com/stackitcloud/stackit-sdk-go/services/resourcemanager/wait" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil" @@ -26,19 +24,7 @@ var projectResource = map[string]string{ "new_label": "a-label", } -func membersConfig(members []authorization.Member) string { - membersConfig := make([]string, 0, len(members)) - for _, m := range members { - memberConfig := fmt.Sprintf(`{ - subject = "%s" - role = "%s" - }`, *m.Subject, *m.Role) - membersConfig = append(membersConfig, memberConfig) - } - return strings.Join(membersConfig, ",\n") -} - -func resourceConfig(name string, label *string, members string) string { +func resourceConfig(name string, label *string) string { labelConfig := "" if label != nil { labelConfig = fmt.Sprintf("new_label = %q", *label) @@ -53,19 +39,13 @@ func resourceConfig(name string, label *string, members string) string { "billing_reference" = "%[4]s" %[5]s } - members = [ - %[7]s - ] - owner_email = "%[8]s" + owner_email = "%[7]s" } resource "stackit_resourcemanager_project" "parent_by_uuid" { parent_container_id = "%[6]s" name = "%[3]s-uuid" - members = [ - %[7]s - ] - owner_email = "%[8]s" + owner_email = "%[7]s" } `, testutil.ResourceManagerProviderConfig(), @@ -74,26 +54,18 @@ func resourceConfig(name string, label *string, members string) string { projectResource["billing_reference"], labelConfig, projectResource["parent_uuid"], - members, testutil.TestProjectServiceAccountEmail, ) } func TestAccResourceManagerResource(t *testing.T) { - initialMembersConfig := membersConfig([]authorization.Member{ - { - Subject: &testutil.TestProjectUserEmail, - Role: utils.Ptr("owner"), - }, - }) - resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, CheckDestroy: testAccCheckResourceManagerDestroy, Steps: []resource.TestStep{ // Creation { - Config: resourceConfig(projectResource["name"], nil, initialMembersConfig), + Config: resourceConfig(projectResource["name"], nil), Check: resource.ComposeAggregateTestCheckFunc( // Parent container id project data resource.TestCheckResourceAttrSet("stackit_resourcemanager_project.parent_by_container", "container_id"), @@ -128,7 +100,7 @@ func TestAccResourceManagerResource(t *testing.T) { project_id = stackit_resourcemanager_project.parent_by_container.project_id } `, - resourceConfig(projectResource["name"], nil, initialMembersConfig), + resourceConfig(projectResource["name"], nil), ), Check: resource.ComposeAggregateTestCheckFunc( // Container project data @@ -178,11 +150,11 @@ func TestAccResourceManagerResource(t *testing.T) { ImportStateVerify: true, // The owner_email attributes don't exist in the // API, therefore there is no value for it during import. - ImportStateVerifyIgnore: []string{"owner_email", "members"}, + ImportStateVerifyIgnore: []string{"owner_email"}, }, // Update { - Config: resourceConfig(fmt.Sprintf("%s-new", projectResource["name"]), utils.Ptr("a-label"), initialMembersConfig), + Config: resourceConfig(fmt.Sprintf("%s-new", projectResource["name"]), utils.Ptr("a-label")), Check: resource.ComposeAggregateTestCheckFunc( // Project data resource.TestCheckResourceAttrSet("stackit_resourcemanager_project.parent_by_container", "container_id"), @@ -191,6 +163,7 @@ func TestAccResourceManagerResource(t *testing.T) { resource.TestCheckResourceAttr("stackit_resourcemanager_project.parent_by_container", "labels.%", "2"), resource.TestCheckResourceAttr("stackit_resourcemanager_project.parent_by_container", "labels.billing_reference", projectResource["billing_reference"]), resource.TestCheckResourceAttr("stackit_resourcemanager_project.parent_by_container", "labels.new_label", projectResource["new_label"]), + resource.TestCheckResourceAttr("stackit_resourcemanager_project.parent_by_container", "owner_email", testutil.TestProjectServiceAccountEmail), ), }, // Deletion is done by the framework implicitly