diff --git a/go.mod b/go.mod index ffd2c117d..e72238c3f 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/stackitcloud/terraform-provider-stackit go 1.24.0 require ( + dev.azure.com/schwarzit/schwarzit.stackit-public/stackit-sdk-go-internal.git/services/authorization v0.0.0-20251126130857-9f2211a4c524 github.com/google/go-cmp v0.7.0 github.com/google/uuid v1.6.0 github.com/gorilla/mux v1.8.1 diff --git a/go.sum b/go.sum index 78f1c3bf1..9e3a2ea95 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +dev.azure.com/schwarzit/schwarzit.stackit-public/stackit-sdk-go-internal.git/services/authorization v0.0.0-20251126130857-9f2211a4c524 h1:ITrpsUZNlZSMWF5W7Jixp5ekWxiMgy4KOURODk73uhI= +dev.azure.com/schwarzit/schwarzit.stackit-public/stackit-sdk-go-internal.git/services/authorization v0.0.0-20251126130857-9f2211a4c524/go.mod h1:ZupN/2xICyLvD0FPxihbUH2KNXtVBVpp/DzsoyFKib4= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw= diff --git a/stackit/internal/services/authorization/authorization_acc_test.go b/stackit/internal/services/authorization/authorization_acc_test.go index 7fcede14d..f995acd7b 100644 --- a/stackit/internal/services/authorization/authorization_acc_test.go +++ b/stackit/internal/services/authorization/authorization_acc_test.go @@ -11,6 +11,7 @@ import ( _ "embed" "github.com/hashicorp/terraform-plugin-testing/config" + "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/terraform" stackitSdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config" @@ -33,12 +34,33 @@ var invalidRole string //go:embed testfiles/organization-role.tf var organizationRole string +//go:embed testfiles/custom-role.tf +var customRole string + var testConfigVars = config.Variables{ "project_id": config.StringVariable(testutil.ProjectId), "test_service_account": config.StringVariable(testutil.TestProjectServiceAccountEmail), "organization_id": config.StringVariable(testutil.OrganizationId), } +var testConfigVarsCustomRole = config.Variables{ + "project_id": config.StringVariable(testutil.ProjectId), + "test_service_account": config.StringVariable(testutil.TestProjectServiceAccountEmail), + "organization_id": config.StringVariable(testutil.OrganizationId), + "role_name": config.StringVariable(fmt.Sprintf("tf-acc-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlpha))), + "role_description": config.StringVariable("Some description"), + "role_permissions_0": config.StringVariable("iam.role.list"), +} + +var testConfigVarsCustomRoleUpdated = config.Variables{ + "project_id": config.StringVariable(testutil.ProjectId), + "test_service_account": config.StringVariable(testutil.TestProjectServiceAccountEmail), + "organization_id": config.StringVariable(testutil.OrganizationId), + "role_name": config.StringVariable(fmt.Sprintf("tf-acc-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlpha))), + "role_description": config.StringVariable("Updated description"), + "role_permissions_0": config.StringVariable("iam.role.edit"), +} + func TestAccProjectRoleAssignmentResource(t *testing.T) { t.Log(testutil.AuthorizationProviderConfig()) resource.Test(t, resource.TestCase{ @@ -53,8 +75,7 @@ func TestAccProjectRoleAssignmentResource(t *testing.T) { return err } - members, err := client.ListMembers(context.TODO(), "project", testutil.ProjectId).Execute() - + members, err := client.ListMembers(context.Background(), "project", testutil.ProjectId).Execute() if err != nil { return err } @@ -93,18 +114,105 @@ func TestAccProjectRoleAssignmentResource(t *testing.T) { }, }, }) + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + ConfigVariables: testConfigVarsCustomRole, + Config: testutil.AuthorizationProviderConfig() + customRole, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("stackit_authorization_project_custom_role.custom-role", "resource_id", testutil.ConvertConfigVariable(testConfigVarsCustomRole["project_id"])), + resource.TestCheckResourceAttr("stackit_authorization_project_custom_role.custom-role", "name", testutil.ConvertConfigVariable(testConfigVarsCustomRole["role_name"])), + resource.TestCheckResourceAttr("stackit_authorization_project_custom_role.custom-role", "description", testutil.ConvertConfigVariable(testConfigVarsCustomRole["role_description"])), + resource.TestCheckResourceAttr("stackit_authorization_project_custom_role.custom-role", "permissions.#", "1"), + resource.TestCheckTypeSetElemAttr("stackit_authorization_project_custom_role.custom-role", "permissions.*", testutil.ConvertConfigVariable(testConfigVarsCustomRole["role_permissions_0"])), + resource.TestCheckResourceAttrSet("stackit_authorization_project_custom_role.custom-role", "role_id"), + ), + }, + // Data source + { + ConfigVariables: testConfigVarsCustomRole, + Config: fmt.Sprintf(` + %s + + data "stackit_authorization_project_custom_role" "custom-role" { + resource_id = stackit_authorization_project_custom_role.custom-role.resource_id + role_id = stackit_authorization_project_custom_role.custom-role.role_id + } + `, + testutil.AuthorizationProviderConfig()+customRole, + ), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("data.stackit_authorization_project_custom_role.custom-role", "resource_id", testutil.ConvertConfigVariable(testConfigVarsCustomRole["project_id"])), + resource.TestCheckResourceAttrPair( + "stackit_authorization_project_custom_role.custom-role", "resource_id", + "data.stackit_authorization_project_custom_role.custom-role", "resource_id", + ), + resource.TestCheckResourceAttrPair( + "stackit_authorization_project_custom_role.custom-role", "role_id", + "data.stackit_authorization_project_custom_role.custom-role", "role_id", + ), + resource.TestCheckResourceAttrPair( + "stackit_authorization_project_custom_role.custom-role", "name", + "data.stackit_authorization_project_custom_role.custom-role", "name", + ), + resource.TestCheckResourceAttrPair( + "stackit_authorization_project_custom_role.custom-role", "description", + "data.stackit_authorization_project_custom_role.custom-role", "description", + ), + resource.TestCheckResourceAttrPair( + "stackit_authorization_project_custom_role.custom-role", "permissions", + "data.stackit_authorization_project_custom_role.custom-role", "permissions", + ), + ), + }, + // Import + { + ConfigVariables: testConfigVarsCustomRole, + ResourceName: "stackit_authorization_project_custom_role.custom-role", + ImportStateIdFunc: func(s *terraform.State) (string, error) { + r, ok := s.RootModule().Resources["stackit_authorization_project_custom_role.custom-role"] + if !ok { + return "", fmt.Errorf("couldn't find resource stackit_authorization_project_custom_role.custom-role") + } + roleId, ok := r.Primary.Attributes["role_id"] + if !ok { + return "", fmt.Errorf("couldn't find attribute role_id") + } + + return fmt.Sprintf("%s,%s", testutil.ProjectId, roleId), nil + }, + ImportState: true, + ImportStateVerify: true, + }, + // Update + { + ConfigVariables: testConfigVarsCustomRoleUpdated, + Config: testutil.AuthorizationProviderConfig() + customRole, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("stackit_authorization_project_custom_role.custom-role", "resource_id", testutil.ConvertConfigVariable(testConfigVarsCustomRoleUpdated["project_id"])), + resource.TestCheckResourceAttr("stackit_authorization_project_custom_role.custom-role", "name", testutil.ConvertConfigVariable(testConfigVarsCustomRoleUpdated["role_name"])), + resource.TestCheckResourceAttr("stackit_authorization_project_custom_role.custom-role", "description", testutil.ConvertConfigVariable(testConfigVarsCustomRoleUpdated["role_description"])), + resource.TestCheckResourceAttr("stackit_authorization_project_custom_role.custom-role", "permissions.#", "1"), + resource.TestCheckTypeSetElemAttr("stackit_authorization_project_custom_role.custom-role", "permissions.*", testutil.ConvertConfigVariable(testConfigVarsCustomRoleUpdated["role_permissions_0"])), + resource.TestCheckResourceAttrSet("stackit_authorization_project_custom_role.custom-role", "role_id"), + ), + }, + // Deletion is done by the framework implicitly + }, + }) } func authApiClient() (*authorization.APIClient, error) { var client *authorization.APIClient var err error - if testutil.AuthorizationCustomEndpoint == "" { - client, err = authorization.NewAPIClient( - stackitSdkConfig.WithRegion("eu01"), - ) + if testutil.AuthorizationCustomEndpoint == "" || testutil.TokenCustomEndpoint == "" { + client, err = authorization.NewAPIClient() } else { client, err = authorization.NewAPIClient( stackitSdkConfig.WithEndpoint(testutil.AuthorizationCustomEndpoint), + stackitSdkConfig.WithTokenEndpoint(testutil.TokenCustomEndpoint), ) } if err != nil { diff --git a/stackit/internal/services/authorization/customrole/datasource.go b/stackit/internal/services/authorization/customrole/datasource.go new file mode 100644 index 000000000..3f4753d16 --- /dev/null +++ b/stackit/internal/services/authorization/customrole/datasource.go @@ -0,0 +1,158 @@ +package customrole + +import ( + "context" + "errors" + "fmt" + "net/http" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/stackitcloud/stackit-sdk-go/core/oapierror" + "github.com/stackitcloud/stackit-sdk-go/services/authorization" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + authorizationUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/authorization/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ datasource.DataSource = &customRoleDataSource{} +) + +// NewAuthorizationDataSource creates a new customrole of the authorizationDataSource. +func NewCustomRoleDataSource() datasource.DataSource { + return &customRoleDataSource{} +} + +// NewProjectRoleAssignmentDataSources is a helper function generate custom role +// data sources for all possible resource types. +func NewCustomRoleDataSources() []func() datasource.DataSource { + resources := make([]func() datasource.DataSource, 0) + for _, v := range resourceTypes { + resources = append(resources, func() datasource.DataSource { + return &customRoleDataSource{ + resourceType: v, + } + }) + } + + return resources +} + +// customRoleDataSource is the datasource implementation. +type customRoleDataSource struct { + resourceType string + client *authorization.APIClient +} + +// Configure sets up the API client for the authorization customrole resource. +func (d *customRoleDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + if !ok { + return + } + + if resp.Diagnostics.HasError() { + return + } + + apiClient := authorizationUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + + if resp.Diagnostics.HasError() { + return + } + + d.client = apiClient + + tflog.Info(ctx, "authorization client configured") +} + +// Metadata provides metadata for the custom role datasource. +func (d *customRoleDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = fmt.Sprintf("%s_authorization_%s_custom_role", req.ProviderTypeName, d.resourceType) +} + +// Schema defines the schema for the custom role data source. +func (d *customRoleDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: descriptions["main"], + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: descriptions["id"], + Computed: true, + }, + "role_id": schema.StringAttribute{ + Description: descriptions["role_id"], + Required: true, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "resource_id": schema.StringAttribute{ + Description: descriptions["resource_id"], + Required: true, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "name": schema.StringAttribute{ + Description: descriptions["name"], + Computed: true, + }, + "description": schema.StringAttribute{ + Description: descriptions["subject"], + Computed: true, + }, + "permissions": schema.ListAttribute{ + ElementType: types.StringType, + Description: descriptions["permissions"], + Computed: true, + }, + }, + } +} + +func (d *customRoleDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { //nolint:gocritic // function signature required by Terraform + var model Model + diags := req.Config.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + + if resp.Diagnostics.HasError() { + return + } + + resourceId := model.ResourceId.ValueString() + roleId := model.RoleId.ValueString() + + roleResp, err := d.client.GetRole(ctx, d.resourceType, resourceId, roleId).Execute() + if err != nil { + var oapiErr *oapierror.GenericOpenAPIError + + ok := errors.As(err, &oapiErr) + if ok && oapiErr.StatusCode == http.StatusNotFound { + resp.State.RemoveResource(ctx) + return + } + + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading custom role", fmt.Sprintf("Calling API: %v", err)) + + return + } + + if err = mapGetCustomRoleResponse(ctx, roleResp, &model); err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading custom role", fmt.Sprintf("Processing API response: %v", err)) + return + } + + // Set the updated state. + diags = resp.State.Set(ctx, &model) + resp.Diagnostics.Append(diags...) + tflog.Info(ctx, fmt.Sprintf("read custom role %s", roleId)) +} diff --git a/stackit/internal/services/authorization/customrole/resource.go b/stackit/internal/services/authorization/customrole/resource.go new file mode 100644 index 000000000..fa5050bf2 --- /dev/null +++ b/stackit/internal/services/authorization/customrole/resource.go @@ -0,0 +1,444 @@ +package customrole + +import ( + "context" + "errors" + "fmt" + "net/http" + "strings" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "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/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/stackitcloud/stackit-sdk-go/core/oapierror" + "github.com/stackitcloud/stackit-sdk-go/services/authorization" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + authorizationUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/authorization/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" +) + +// List of resource types which can have custom roles. +var resourceTypes = []string{ + "project", +} + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ resource.Resource = &customRoleResource{} + _ resource.ResourceWithConfigure = &customRoleResource{} + _ resource.ResourceWithImportState = &customRoleResource{} +) + +// Model represents the schema for the git resource. +type Model struct { + Id types.String `tfsdk:"id"` // Required by Terraform + RoleId types.String `tfsdk:"role_id"` + ResourceId types.String `tfsdk:"resource_id"` + Name types.String `tfsdk:"name"` + Description types.String `tfsdk:"description"` + Permissions types.List `tfsdk:"permissions"` +} + +// customRoleResource is the resource implementation. +type customRoleResource struct { + resourceType string + client *authorization.APIClient +} + +// NewProjectRoleAssignmentResources is a helper function generate custom role +// resources for all possible resource types. +func NewCustomRoleResources() []func() resource.Resource { + resources := make([]func() resource.Resource, 0) + for _, v := range resourceTypes { + resources = append(resources, func() resource.Resource { + return &customRoleResource{ + resourceType: v, + } + }) + } + + return resources +} + +// descriptions for the attributes in the Schema. +var descriptions = map[string]string{ + "main": "Custom Role resource schema.", + "id": "Terraform's internal resource identifier. It is structured as \"[resource_id],[role_id]\".", + "role_id": "The ID of the role.", + "resource_id": "Resource to add the custom role to.", + "name": "Name of the role", + "description": "A human readable description of the role.", + "permissions": "Permissions for the role", + "etag": "A version identifier for the custom role.", +} + +// Configure adds the provider configured client to the resource. +func (r *customRoleResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + if !ok { + return + } + + if resp.Diagnostics.HasError() { + return + } + + apiClient := authorizationUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + + if resp.Diagnostics.HasError() { + return + } + + r.client = apiClient + + tflog.Info(ctx, "authorization client configured") +} + +// Metadata sets the resource type name. +func (r *customRoleResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = fmt.Sprintf("%s_authorization_%s_custom_role", req.ProviderTypeName, r.resourceType) +} + +// Schema defines the schema for the resource. +func (r *customRoleResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: descriptions["main"], + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: descriptions["id"], + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "role_id": schema.StringAttribute{ + Description: descriptions["role_id"], + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), // TODO: check if this makes sense + }, + }, + "resource_id": schema.StringAttribute{ + Description: descriptions["resource_id"], + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "name": schema.StringAttribute{ + Description: descriptions["name"], + Required: true, + }, + "description": schema.StringAttribute{ + Description: descriptions["subject"], + Required: true, + }, + "permissions": schema.ListAttribute{ + Description: descriptions["permissions"], + Required: true, + ElementType: types.StringType, + }, + }, + } +} + +// Create creates the resource and sets the initial Terraform state. +func (r *customRoleResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { //nolint:gocritic // function signature required by Terraform + var model Model + diags := req.Plan.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + + if resp.Diagnostics.HasError() { + return + } + + ctx = r.annotateLogger(ctx, &model) + + payload, err := toCreatePayload(ctx, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating custom role", fmt.Sprintf("Creating API payload: %v", err)) + return + } + + createResp, err := r.client.AddRole(ctx, r.resourceType, model.ResourceId.ValueString()).AddRolePayload(*payload).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating custom role", fmt.Sprintf("Calling API: %v", err)) + return + } + + if err = mapAddCustomRoleResponse(ctx, createResp, &model); err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating custom role", fmt.Sprintf("Processing API payload: %v", err)) + return + } + + diags = resp.State.Set(ctx, model) + resp.Diagnostics.Append(diags...) + + if resp.Diagnostics.HasError() { + return + } + + tflog.Info(ctx, "custom role created") +} + +// Read refreshes the Terraform state with the latest data. +func (r *customRoleResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { //nolint:gocritic // function signature required by Terraform + // Retrieve the current state of the resource. + var model Model + diags := req.State.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + + if resp.Diagnostics.HasError() { + return + } + + ctx = r.annotateLogger(ctx, &model) + + roleResp, err := r.client.GetRoleExecute(ctx, r.resourceType, model.ResourceId.ValueString(), model.RoleId.ValueString()) + if err != nil { + var oapiErr *oapierror.GenericOpenAPIError + + ok := errors.As(err, &oapiErr) + if ok && oapiErr.StatusCode == http.StatusNotFound { + resp.State.RemoveResource(ctx) + return + } + + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading custom role", fmt.Sprintf("Calling API: %v", err)) + + return + } + + if err = mapGetCustomRoleResponse(ctx, roleResp, &model); err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading custom role", fmt.Sprintf("Processing API payload: %v", err)) + return + } + + // Set the updated state. + diags = resp.State.Set(ctx, &model) + resp.Diagnostics.Append(diags...) + tflog.Info(ctx, fmt.Sprintf("read custom role %s", model.RoleId)) +} + +// Update updates the resource and sets the updated Terraform state on success. +func (r *customRoleResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { //nolint:gocritic // function signature required by Terraform + // Retrieve values from plan + var model Model + diags := req.Plan.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + + if resp.Diagnostics.HasError() { + return + } + + ctx = r.annotateLogger(ctx, &model) + + // Generate API request body from model + payload, err := toUpdatePayload(ctx, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating custom role", fmt.Sprintf("Creating API payload: %v", err)) + return + } + + // Update existing custom role + roleResp, err := r.client.UpdateRole(ctx, r.resourceType, model.ResourceId.ValueString(), model.RoleId.ValueString()).UpdateRolePayload(*payload).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating custom role", fmt.Sprintf("Calling API: %v", err)) + return + } + + err = mapUpdateCustomRoleResponse(ctx, roleResp, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating custom role", fmt.Sprintf("Processing API response: %v", err)) + return + } + + diags = resp.State.Set(ctx, model) + resp.Diagnostics.Append(diags...) + + if resp.Diagnostics.HasError() { + return + } + + tflog.Info(ctx, "custom role updated") +} + +// Delete deletes the resource and removes the Terraform state on success. +func (r *customRoleResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { //nolint:gocritic // function signature required by Terraform + var model Model + diags := req.State.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + + if resp.Diagnostics.HasError() { + return + } + + ctx = r.annotateLogger(ctx, &model) + + _, err := r.client.DeleteRoleExecute(ctx, r.resourceType, model.ResourceId.ValueString(), model.RoleId.ValueString()) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting custom role", fmt.Sprintf("Calling API: %v", err)) + } + + tflog.Info(ctx, "custom role deleted") +} + +// ImportState imports a resource into the Terraform state on success. +// The expected format of the custom role resource import identifier is: +// resource_id,role_id. +func (r *customRoleResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + idParts := strings.Split(req.ID, core.Separator) + if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" { + core.LogAndAddError(ctx, &resp.Diagnostics, + "Error importing custom role", + fmt.Sprintf("Expected import identifier with format [resource_id],[role_id] got %q", req.ID), + ) + + return + } + + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("resource_id"), idParts[0])...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("role_id"), idParts[1])...) + tflog.Info(ctx, "custom role state imported") +} + +// mapGetCustomRoleResponse maps custom role response fields to the provider's internal model. +func mapGetCustomRoleResponse(ctx context.Context, resp *authorization.GetRoleResponse, model *Model) error { + if resp == nil { + return fmt.Errorf("response input is nil") + } + + if resp.Role == nil { + return fmt.Errorf("response role is nil") + } + + if resp.Role.Id == nil { + return fmt.Errorf("response role id is nil") + } + + if resp.Role.Permissions == nil { + return fmt.Errorf("response role permissions is nil") + } + + if model == nil { + return fmt.Errorf("model input is nil") + } + + model.Id = utils.BuildInternalTerraformId(*resp.ResourceId, *resp.Role.Id) + model.ResourceId = types.StringPointerValue(resp.ResourceId) + model.RoleId = types.StringPointerValue(resp.Role.Id) + model.Name = types.StringPointerValue(resp.Role.Name) + model.Description = types.StringPointerValue(resp.Role.Description) + + if len(*resp.Role.Permissions) == 0 { + model.Permissions = types.ListNull(types.StringType) + return nil + } + + var permissions []string + for _, p := range *resp.Role.Permissions { + if name, ok := p.GetNameOk(); ok { + permissions = append(permissions, name) + } + } + + var diags diag.Diagnostics + model.Permissions, diags = types.ListValueFrom(ctx, types.StringType, permissions) + if diags.HasError() { + return fmt.Errorf("mapping permissions: %w", core.DiagsToError(diags)) + } + + return nil +} + +func mapAddCustomRoleResponse(ctx context.Context, resp *authorization.AddCustomRoleResponse, model *Model) error { + getRoleResponse, err := authorizationUtils.TypeConverter[authorization.GetRoleResponse](resp) + if err != nil { + return err + } + + return mapGetCustomRoleResponse(ctx, getRoleResponse, model) +} + +func mapUpdateCustomRoleResponse(ctx context.Context, resp *authorization.UpdateRoleResponse, model *Model) error { + getRoleResponse, err := authorizationUtils.TypeConverter[authorization.GetRoleResponse](resp) + if err != nil { + return err + } + + return mapGetCustomRoleResponse(ctx, getRoleResponse, model) +} + +// toCreatePayload builds an addRolePayload from provider's model. +func toCreatePayload(ctx context.Context, model *Model) (*authorization.AddRolePayload, error) { + if model == nil { + return nil, fmt.Errorf("nil model") + } + + permissions := make([]authorization.PermissionRequest, 0) + for _, permission := range model.Permissions.Elements() { + if permission.IsNull() || permission.IsUnknown() { + return nil, errors.New("permission is unknown or null") + } + + permission, err := conversion.ToString(ctx, permission) + if err != nil { + return nil, fmt.Errorf("converting permission list entry to string: %w", err) + } + + permissions = append(permissions, authorization.PermissionRequest{Name: &permission}) + } + + return &authorization.AddRolePayload{ + Name: model.Name.ValueStringPointer(), + Description: model.Description.ValueStringPointer(), + Permissions: &permissions, + }, nil +} + +// toUpdatePayload builds an updateRolePayload from provider's model. +func toUpdatePayload(ctx context.Context, model *Model) (*authorization.UpdateRolePayload, error) { + if model == nil { + return nil, fmt.Errorf("nil model") + } + + permissions := make([]authorization.PermissionRequest, 0) + for _, permission := range model.Permissions.Elements() { + if permission.IsNull() || permission.IsUnknown() { + return nil, errors.New("permission is unknown or null") + } + + permission, err := conversion.ToString(ctx, permission) + if err != nil { + return nil, fmt.Errorf("converting permission list entry to string: %w", err) + } + + permissions = append(permissions, authorization.PermissionRequest{Name: &permission}) + } + + return &authorization.UpdateRolePayload{ + Name: model.Name.ValueStringPointer(), + Description: model.Description.ValueStringPointer(), + Permissions: &permissions, + }, nil +} + +func (r *customRoleResource) annotateLogger(ctx context.Context, model *Model) context.Context { + ctx = tflog.SetField(ctx, "resource_id", model.ResourceId.ValueString()) + ctx = tflog.SetField(ctx, "role_id", model.RoleId.ValueString()) + ctx = tflog.SetField(ctx, "name", model.Name.ValueString()) + + return ctx +} diff --git a/stackit/internal/services/authorization/customrole/resource_test.go b/stackit/internal/services/authorization/customrole/resource_test.go new file mode 100644 index 000000000..b8c4f3cbd --- /dev/null +++ b/stackit/internal/services/authorization/customrole/resource_test.go @@ -0,0 +1,232 @@ +package customrole + +import ( + "context" + "fmt" + "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/stackitcloud/stackit-sdk-go/core/utils" + "github.com/stackitcloud/stackit-sdk-go/services/authorization" +) + +var ( + testRoleId = uuid.New().String() + testProjectId = uuid.New().String() +) + +func TestMapFields(t *testing.T) { + tests := []struct { + description string + input *authorization.GetRoleResponse + expected *Model + isValid bool + }{ + { + description: "full_input", + input: &authorization.GetRoleResponse{ + ResourceId: &testProjectId, + ResourceType: utils.Ptr("project"), + Role: utils.Ptr(authorization.Role{ + Id: &testRoleId, + Name: utils.Ptr("role-name"), + Description: utils.Ptr("Some description"), + Permissions: utils.Ptr([]authorization.Permission{ + { + Name: utils.Ptr("iam.subject.get"), + Description: utils.Ptr("Can read subjects."), + }, + }), + }), + }, + expected: &Model{ + Id: types.StringValue(fmt.Sprintf("%s,%s", testProjectId, testRoleId)), + RoleId: types.StringValue(testRoleId), + ResourceId: types.StringValue(testProjectId), + Name: types.StringValue("role-name"), + Description: types.StringValue("Some description"), + Permissions: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("iam.subject.get"), + }), + }, + isValid: true, + }, + { + description: "partial_input", + input: &authorization.GetRoleResponse{ + ResourceId: &testProjectId, + ResourceType: utils.Ptr("project"), + Role: utils.Ptr(authorization.Role{ + Id: &testRoleId, + Permissions: utils.Ptr([]authorization.Permission{ + { + Name: utils.Ptr("iam.subject.get"), + }, + }), + }), + }, + expected: &Model{ + Id: types.StringValue(fmt.Sprintf("%s,%s", testProjectId, testRoleId)), + RoleId: types.StringValue(testRoleId), + ResourceId: types.StringValue(testProjectId), + Permissions: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("iam.subject.get"), + }), + }, + isValid: true, + }, + { + description: "partial_input_without_permissions", + input: &authorization.GetRoleResponse{ + ResourceId: &testProjectId, + ResourceType: utils.Ptr("project"), + Role: utils.Ptr(authorization.Role{ + Id: &testRoleId, + Permissions: utils.Ptr([]authorization.Permission{}), + }), + }, + expected: &Model{ + Id: types.StringValue(fmt.Sprintf("%s,%s", testProjectId, testRoleId)), + RoleId: types.StringValue(testRoleId), + ResourceId: types.StringValue(testProjectId), + Permissions: types.ListNull(types.StringType), + }, + isValid: true, + }, + { + description: "nil_instance", + input: nil, + expected: nil, + isValid: false, + }, + { + description: "empty_instance", + input: &authorization.GetRoleResponse{}, + expected: nil, + isValid: false, + }, + { + description: "missing_role", + input: &authorization.GetRoleResponse{ + ResourceId: &testProjectId, + ResourceType: utils.Ptr("project"), + }, + expected: nil, + isValid: false, + }, + { + description: "missing_permissions", + input: &authorization.GetRoleResponse{ + ResourceId: &testProjectId, + ResourceType: utils.Ptr("project"), + Role: utils.Ptr(authorization.Role{ + Id: &testRoleId, + }), + }, + expected: nil, + isValid: false, + }, + { + description: "missing_role_id", + input: &authorization.GetRoleResponse{ + ResourceId: &testProjectId, + ResourceType: utils.Ptr("project"), + Role: utils.Ptr(authorization.Role{ + Permissions: utils.Ptr([]authorization.Permission{}), + }), + }, + expected: nil, + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + state := &Model{} + err := mapGetCustomRoleResponse(context.Background(), tt.input, state) + + if tt.isValid && err != nil { + t.Fatalf("expected success, got error: %v", err) + } + + if !tt.isValid && err == nil { + t.Fatalf("expected error, got nil") + } + + if tt.isValid { + if diff := cmp.Diff(tt.expected, state); diff != "" { + t.Errorf("unexpected diff (-want +got):\n%s", diff) + } + } + }) + } +} + +func TestToCreatePayload(t *testing.T) { + tests := []struct { + description string + input *Model + expected authorization.AddRolePayload + expectError bool + }{ + { + description: "all values", + input: &Model{ + Name: types.StringValue("role-name"), + Description: types.StringValue("Some description"), + Permissions: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("iam.subject.get"), + }), + }, + expected: authorization.AddRolePayload{ + Name: utils.Ptr("role-name"), + Description: utils.Ptr("Some description"), + Permissions: utils.Ptr([]authorization.PermissionRequest{ + { + Name: utils.Ptr("iam.subject.get"), + }, + }), + }, + }, + { + description: "empty values still valid", + input: &Model{}, + expected: authorization.AddRolePayload{ + Permissions: utils.Ptr([]authorization.PermissionRequest{}), + }, + expectError: false, + }, + { + description: "nil input model", + input: nil, + expected: authorization.AddRolePayload{}, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + output, err := toCreatePayload(context.Background(), tt.input) + + if tt.expectError && err == nil { + t.Fatalf("expected error but got none") + } + + if !tt.expectError && err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if tt.expectError && output == nil { + // skip diff when error was expected + return + } + + if diff := cmp.Diff(&tt.expected, output); diff != "" { + t.Fatalf("unexpected payload (-want +got):\n%s", diff) + } + }) + } +} diff --git a/stackit/internal/services/authorization/roleassignments/resource.go b/stackit/internal/services/authorization/roleassignments/resource.go index cd29fdb0a..a51ac4354 100644 --- a/stackit/internal/services/authorization/roleassignments/resource.go +++ b/stackit/internal/services/authorization/roleassignments/resource.go @@ -2,16 +2,10 @@ package roleassignments import ( "context" - "encoding/json" "errors" "fmt" "strings" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - authorizationUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/authorization/utils" - "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" @@ -21,12 +15,15 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/stackit-sdk-go/services/authorization" + "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/features" + authorizationUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/authorization/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) -// List of permission assignments targets in form [TF resource name]:[api name] +// List of permission assignments targets in form [TF resource name]:[api name]. var roleTargets = []string{ "project", "organization", @@ -39,10 +36,10 @@ var ( _ resource.ResourceWithImportState = &roleAssignmentResource{} errRoleAssignmentNotFound = errors.New("response members did not contain expected role assignment") - errRoleAssignmentDuplicateFound = errors.New("found a duplicate role assignment.") + errRoleAssignmentDuplicateFound = errors.New("found a duplicate role assignment") ) -// Provider's internal model +// Provider's internal model. type Model struct { Id types.String `tfsdk:"id"` // needed by TF ResourceId types.String `tfsdk:"resource_id"` @@ -60,6 +57,7 @@ func NewRoleAssignmentResources() []func() resource.Resource { } }) } + return resources } @@ -82,14 +80,17 @@ func (r *roleAssignmentResource) Configure(ctx context.Context, req resource.Con } features.CheckExperimentEnabled(ctx, &providerData, features.IamExperiment, fmt.Sprintf("stackit_authorization_%s_role_assignment", r.apiName), core.Resource, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } apiClient := authorizationUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.authorizationClient = apiClient tflog.Info(ctx, fmt.Sprintf("Resource Manager %s Role Assignment client configured", r.apiName)) } @@ -144,10 +145,11 @@ func (r *roleAssignmentResource) Schema(_ context.Context, _ resource.SchemaRequ } // Create creates the resource and sets the initial Terraform state. -func (r *roleAssignmentResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *roleAssignmentResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -167,6 +169,7 @@ func (r *roleAssignmentResource) Create(ctx context.Context, req resource.Create core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", fmt.Sprintf("Creating API payload: %v", err)) return } + createResp, err := r.authorizationClient.AddMembers(ctx, model.ResourceId.ValueString()).AddMembersPayload(*payload).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, fmt.Sprintf("Error creating %s role assignment", r.apiName), fmt.Sprintf("Calling API: %v", err)) @@ -181,19 +184,23 @@ func (r *roleAssignmentResource) Create(ctx context.Context, req resource.Create core.LogAndAddError(ctx, &resp.Diagnostics, fmt.Sprintf("Error creating %s role assignment", r.apiName), fmt.Sprintf("Processing API payload: %v", err)) return } + diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, fmt.Sprintf("%s role assignment created", r.apiName)) } // Read refreshes the Terraform state with the latest data. -func (r *roleAssignmentResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *roleAssignmentResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -220,22 +227,25 @@ func (r *roleAssignmentResource) Read(ctx context.Context, req resource.ReadRequ // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, fmt.Sprintf("%s role assignment read successful", r.apiName)) } // Update updates the resource and sets the updated Terraform state on success. -func (r *roleAssignmentResource) Update(_ context.Context, _ resource.UpdateRequest, _ *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *roleAssignmentResource) Update(_ context.Context, _ resource.UpdateRequest, _ *resource.UpdateResponse) { //nolint:gocritic // function signature required by Terraform // does nothing since resource updates should always trigger resource replacement } // Delete deletes the resource and removes the Terraform state on success. -func (r *roleAssignmentResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform +func (r *roleAssignmentResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -263,7 +273,7 @@ func (r *roleAssignmentResource) Delete(ctx context.Context, req resource.Delete } // ImportState imports a resource into the Terraform state on success. -// The expected format of the project role assignment resource import identifier is: resource_id,role,subject +// The expected format of the project role assignment resource import identifier is: resource_id,role,subject. func (r *roleAssignmentResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { idParts := strings.Split(req.ID, core.Separator) if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { @@ -271,6 +281,7 @@ func (r *roleAssignmentResource) ImportState(ctx context.Context, req resource.I fmt.Sprintf("Error importing %s role assignment", r.apiName), fmt.Sprintf("Expected import identifier with format [resource_id],[role],[subject], got %q", req.ID), ) + return } @@ -285,9 +296,11 @@ func mapListMembersResponse(resp *authorization.ListMembersResponse, model *Mode if resp == nil { return fmt.Errorf("response input is nil") } + if resp.Members == nil { return fmt.Errorf("response members are nil") } + if model == nil { return fmt.Errorf("model input is nil") } @@ -299,35 +312,24 @@ func mapListMembersResponse(resp *authorization.ListMembersResponse, model *Mode if *m.Role == model.Role.ValueString() && *m.Subject == model.Subject.ValueString() { model.Role = types.StringPointerValue(m.Role) model.Subject = types.StringPointerValue(m.Subject) + return nil } } + return errRoleAssignmentNotFound } func mapMembersResponse(resp *authorization.MembersResponse, model *Model) error { - listMembersResponse, err := typeConverter[authorization.ListMembersResponse](resp) + listMembersResponse, err := authorizationUtils.TypeConverter[authorization.ListMembersResponse](resp) if err != nil { return err } - return mapListMembersResponse(listMembersResponse, model) -} -// Helper to convert objects with equal JSON tags -func typeConverter[R any](data any) (*R, error) { - var result R - b, err := json.Marshal(&data) - if err != nil { - return nil, err - } - err = json.Unmarshal(b, &result) - if err != nil { - return nil, err - } - return &result, err + return mapListMembersResponse(listMembersResponse, model) } -// Build Createproject role assignmentPayload from provider's model +// Build Createproject role assignmentPayload from provider's model. func (r *roleAssignmentResource) toCreatePayload(model *Model) (*authorization.AddMembersPayload, error) { if model == nil { return nil, fmt.Errorf("nil model") @@ -347,10 +349,11 @@ func (r *roleAssignmentResource) annotateLogger(ctx context.Context, model *Mode ctx = tflog.SetField(ctx, "subject", model.Subject.ValueString()) ctx = tflog.SetField(ctx, "role", model.Role.ValueString()) ctx = tflog.SetField(ctx, "resource_type", r.apiName) + return ctx } -// returns an error if duplicate role assignment exists +// returns an error if duplicate role assignment exists. func (r *roleAssignmentResource) checkDuplicate(ctx context.Context, model Model) error { //nolint:gocritic // A read only copy is required since an api response is parsed into the model and this check should not affect the model parameter listResp, err := r.authorizationClient.ListMembers(ctx, r.apiName, model.ResourceId.ValueString()).Subject(model.Subject.ValueString()).Execute() if err != nil { @@ -359,12 +362,13 @@ func (r *roleAssignmentResource) checkDuplicate(ctx context.Context, model Model // Map response body to schema err = mapListMembersResponse(listResp, &model) - if err != nil { if errors.Is(err, errRoleAssignmentNotFound) { return nil } + return err } + return errRoleAssignmentDuplicateFound } diff --git a/stackit/internal/services/authorization/testfiles/custom-role.tf b/stackit/internal/services/authorization/testfiles/custom-role.tf new file mode 100644 index 000000000..33fc59e84 --- /dev/null +++ b/stackit/internal/services/authorization/testfiles/custom-role.tf @@ -0,0 +1,12 @@ + +variable "project_id" {} +variable "role_name" {} +variable "role_description" {} +variable "role_permissions_0" {} + +resource "stackit_authorization_project_custom_role" "custom-role" { + resource_id = var.project_id + name = var.role_name + description = var.role_description + permissions = [var.role_permissions_0] +} diff --git a/stackit/internal/services/authorization/utils/util.go b/stackit/internal/services/authorization/utils/util.go index 99694780a..7258b2633 100644 --- a/stackit/internal/services/authorization/utils/util.go +++ b/stackit/internal/services/authorization/utils/util.go @@ -2,6 +2,7 @@ package utils import ( "context" + "encoding/json" "fmt" "github.com/hashicorp/terraform-plugin-framework/diag" @@ -27,3 +28,20 @@ func ConfigureClient(ctx context.Context, providerData *core.ProviderData, diags return apiClient } + +// TypeConverter Helper to convert objects with equal JSON tags. +func TypeConverter[R any](data any) (*R, error) { + var result R + + b, err := json.Marshal(&data) + if err != nil { + return nil, err + } + + err = json.Unmarshal(b, &result) + if err != nil { + return nil, err + } + + return &result, err +} diff --git a/stackit/internal/testutil/testutil.go b/stackit/internal/testutil/testutil.go index ea15ce31b..59ec57db2 100644 --- a/stackit/internal/testutil/testutil.go +++ b/stackit/internal/testutil/testutil.go @@ -60,7 +60,7 @@ var ( LogMeCustomEndpoint = os.Getenv("TF_ACC_LOGME_CUSTOM_ENDPOINT") MariaDBCustomEndpoint = os.Getenv("TF_ACC_MARIADB_CUSTOM_ENDPOINT") ModelServingCustomEndpoint = os.Getenv("TF_ACC_MODELSERVING_CUSTOM_ENDPOINT") - AuthorizationCustomEndpoint = os.Getenv("TF_ACC_authorization_custom_endpoint") + AuthorizationCustomEndpoint = os.Getenv("TF_ACC_AUTHORIZATION_CUSTOM_ENDPOINT") MongoDBFlexCustomEndpoint = os.Getenv("TF_ACC_MONGODBFLEX_CUSTOM_ENDPOINT") OpenSearchCustomEndpoint = os.Getenv("TF_ACC_OPENSEARCH_CUSTOM_ENDPOINT") ObservabilityCustomEndpoint = os.Getenv("TF_ACC_OBSERVABILITY_CUSTOM_ENDPOINT") @@ -75,6 +75,7 @@ var ( ServerBackupCustomEndpoint = os.Getenv("TF_ACC_SERVER_BACKUP_CUSTOM_ENDPOINT") ServerUpdateCustomEndpoint = os.Getenv("TF_ACC_SERVER_UPDATE_CUSTOM_ENDPOINT") ServiceAccountCustomEndpoint = os.Getenv("TF_ACC_SERVICE_ACCOUNT_CUSTOM_ENDPOINT") + TokenCustomEndpoint = os.Getenv("TF_ACC_TOKEN_CUSTOM_ENDPOINT") SKECustomEndpoint = os.Getenv("TF_ACC_SKE_CUSTOM_ENDPOINT") ) @@ -438,7 +439,7 @@ func SKEProviderConfig() string { } func AuthorizationProviderConfig() string { - if AuthorizationCustomEndpoint == "" { + if AuthorizationCustomEndpoint == "" || ServiceAccountCustomEndpoint == "" || ResourceManagerCustomEndpoint == "" || TokenCustomEndpoint == "" { return ` provider "stackit" { default_region = "eu01" @@ -448,9 +449,15 @@ func AuthorizationProviderConfig() string { return fmt.Sprintf(` provider "stackit" { authorization_custom_endpoint = "%s" + service_account_custom_endpoint = "%s" + resourcemanager_custom_endpoint = "%s" + token_custom_endpoint = "%s" experiments = ["iam"] }`, AuthorizationCustomEndpoint, + ServiceAccountCustomEndpoint, + ResourceManagerCustomEndpoint, + TokenCustomEndpoint, ) } diff --git a/stackit/provider.go b/stackit/provider.go index 3a2795ad3..8e9bb5718 100644 --- a/stackit/provider.go +++ b/stackit/provider.go @@ -18,6 +18,7 @@ import ( "github.com/stackitcloud/stackit-sdk-go/core/config" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features" + customRole "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/authorization/customrole" roleAssignements "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/authorization/roleassignments" cdnCustomDomain "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/cdn/customdomain" cdn "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/cdn/distribution" @@ -477,7 +478,7 @@ func (p *Provider) Configure(ctx context.Context, req provider.ConfigureRequest, // DataSources defines the data sources implemented in the provider. func (p *Provider) DataSources(_ context.Context) []func() datasource.DataSource { - return []func() datasource.DataSource{ + dataSources := []func() datasource.DataSource{ alertGroup.NewAlertGroupDataSource, cdn.NewDistributionDataSource, cdnCustomDomain.NewCustomDomainDataSource, @@ -545,6 +546,9 @@ func (p *Provider) DataSources(_ context.Context) []func() datasource.DataSource serviceAccount.NewServiceAccountDataSource, skeCluster.NewClusterDataSource, } + dataSources = append(dataSources, customRole.NewCustomRoleDataSources()...) + + return dataSources } // Resources defines the resources implemented in the provider. @@ -619,6 +623,7 @@ func (p *Provider) Resources(_ context.Context) []func() resource.Resource { skeKubeconfig.NewKubeconfigResource, } resources = append(resources, roleAssignements.NewRoleAssignmentResources()...) + resources = append(resources, customRole.NewCustomRoleResources()...) return resources }