From 4e8d12068d5b4a28a41347501ea3ec2e0fc4e322 Mon Sep 17 00:00:00 2001 From: Henrique Santos Date: Thu, 19 Oct 2023 09:42:01 +0100 Subject: [PATCH 01/14] Implement secrets manager user --- .../secretsmanager/user/datasource.go | 214 ++++++++++ .../secretsmanager/user/datasource_test.go | 88 ++++ .../services/secretsmanager/user/resource.go | 404 ++++++++++++++++++ .../secretsmanager/user/resource_test.go | 271 ++++++++++++ 4 files changed, 977 insertions(+) create mode 100644 stackit/internal/services/secretsmanager/user/datasource.go create mode 100644 stackit/internal/services/secretsmanager/user/datasource_test.go create mode 100644 stackit/internal/services/secretsmanager/user/resource.go create mode 100644 stackit/internal/services/secretsmanager/user/resource_test.go diff --git a/stackit/internal/services/secretsmanager/user/datasource.go b/stackit/internal/services/secretsmanager/user/datasource.go new file mode 100644 index 000000000..ab62b7b28 --- /dev/null +++ b/stackit/internal/services/secretsmanager/user/datasource.go @@ -0,0 +1,214 @@ +package secretsmanager + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" + + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/stackitcloud/stackit-sdk-go/core/config" + "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ datasource.DataSource = &userDataSource{} +) + +type DataSourceModel struct { + Id types.String `tfsdk:"id"` // needed by TF + UserId types.String `tfsdk:"user_id"` + InstanceId types.String `tfsdk:"instance_id"` + ProjectId types.String `tfsdk:"project_id"` + Description types.String `tfsdk:"description"` + WriteEnabled types.Bool `tfsdk:"write_enabled"` + Username types.String `tfsdk:"name"` +} + +// NewUserDataSource is a helper function to simplify the provider implementation. +func NewUserDataSource() datasource.DataSource { + return &userDataSource{} +} + +// userDataSource is the data source implementation. +type userDataSource struct { + client *secretsmanager.APIClient +} + +// Metadata returns the data source type name. +func (r *userDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_secretsmanager_user" +} + +// Configure adds the provider configured client to the data source. +func (r *userDataSource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + // Prevent panic if the provider has not been configured. + if req.ProviderData == nil { + return + } + + providerData, ok := req.ProviderData.(core.ProviderData) + if !ok { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData)) + return + } + + var apiClient *secretsmanager.APIClient + var err error + if providerData.SecretsManagerCustomEndpoint != "" { + apiClient, err = secretsmanager.NewAPIClient( + config.WithCustomAuth(providerData.RoundTripper), + config.WithEndpoint(providerData.SecretsManagerCustomEndpoint), + ) + } else { + apiClient, err = secretsmanager.NewAPIClient( + config.WithCustomAuth(providerData.RoundTripper), + config.WithRegion(providerData.Region), + ) + } + + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the data source configuration", err)) + return + } + + r.client = apiClient + tflog.Info(ctx, "Secrets Manager user client configured") +} + +// Schema defines the schema for the data source. +func (r *userDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + descriptions := map[string]string{ + "main": "Secrets Manager user data source schema. Must have a `region` specified in the provider configuration.", + "id": "Terraform's internal data source identifier. It is structured as \"`project_id`,`instance_id`,`user_id`\".", + "user_id": "The user's ID.", + "instance_id": "ID of the Secrets Manager instance.", + "project_id": "STACKIT Project ID to which the instance is associated.", + "description": "A user chosen description to differentiate between multiple users. Can't be changed after creation.", + "write_enabled": "If true, the user has writeaccess to the secrets engine.", + "username": "An auto-generated user name.", + } + + resp.Schema = schema.Schema{ + Description: descriptions["main"], + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: descriptions["id"], + Computed: true, + }, + "user_id": schema.StringAttribute{ + Description: descriptions["user_id"], + Required: true, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "instance_id": schema.StringAttribute{ + Description: descriptions["instance_id"], + Required: true, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "project_id": schema.StringAttribute{ + Description: descriptions["project_id"], + Required: true, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "description": schema.StringAttribute{ + Description: descriptions["description"], + Computed: true, + }, + "write_enabled": schema.BoolAttribute{ + Description: descriptions["write_enabled"], + Computed: true, + }, + "username": schema.StringAttribute{ + Description: descriptions["username"], + Computed: true, + }, + }, + } +} + +// Read refreshes the Terraform state with the latest data. +func (r *userDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform + var model DataSourceModel + diags := req.Config.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + projectId := model.ProjectId.ValueString() + instanceId := model.InstanceId.ValueString() + userId := model.UserId.ValueString() + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "instance_id", instanceId) + ctx = tflog.SetField(ctx, "user_id", userId) + + userResp, err := r.client.GetUser(ctx, projectId, instanceId, userId).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading user", fmt.Sprintf("Calling API: %v", err)) + return + } + + // Map response body to schema and populate Computed attribute values + err = mapDataSourceFields(userResp, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading user", fmt.Sprintf("Processing API payload: %v", err)) + return + } + + // Set refreshed state + diags = resp.State.Set(ctx, model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "Secrets Manager user read") +} + +func mapDataSourceFields(user *secretsmanager.User, model *DataSourceModel) error { + if user == nil { + return fmt.Errorf("response input is nil") + } + if model == nil { + return fmt.Errorf("model input is nil") + } + + var userId string + if model.UserId.ValueString() != "" { + userId = model.UserId.ValueString() + } else if user.Id != nil { + userId = *user.Id + } else { + return fmt.Errorf("user id not present") + } + + idParts := []string{ + model.ProjectId.ValueString(), + model.InstanceId.ValueString(), + userId, + } + model.Id = types.StringValue( + strings.Join(idParts, core.Separator), + ) + model.UserId = types.StringValue(userId) + model.Description = types.StringPointerValue(user.Description) + model.WriteEnabled = types.BoolPointerValue(user.Write) + model.Username = types.StringPointerValue(user.Username) + return nil +} diff --git a/stackit/internal/services/secretsmanager/user/datasource_test.go b/stackit/internal/services/secretsmanager/user/datasource_test.go new file mode 100644 index 000000000..7ecfaba4b --- /dev/null +++ b/stackit/internal/services/secretsmanager/user/datasource_test.go @@ -0,0 +1,88 @@ +package secretsmanager + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/stackitcloud/stackit-sdk-go/core/utils" + "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager" +) + +func TestMapDataSourceFields(t *testing.T) { + tests := []struct { + description string + input *secretsmanager.User + expected DataSourceModel + isValid bool + }{ + { + "default_values", + &secretsmanager.User{ + Id: utils.Ptr("uid"), + }, + DataSourceModel{ + Id: types.StringValue("pid,iid,uid"), + UserId: types.StringValue("uid"), + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + Description: types.StringNull(), + WriteEnabled: types.BoolNull(), + Username: types.StringNull(), + }, + true, + }, + { + "simple_values", + &secretsmanager.User{ + Id: utils.Ptr("uid"), + Description: utils.Ptr("description"), + Write: utils.Ptr(false), + Username: utils.Ptr("username"), + }, + DataSourceModel{ + Id: types.StringValue("pid,iid,uid"), + UserId: types.StringValue("uid"), + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + Description: types.StringValue("description"), + WriteEnabled: types.BoolValue(false), + Username: types.StringValue("username"), + }, + true, + }, + { + "nil_response", + nil, + DataSourceModel{}, + false, + }, + { + "no_resource_id", + &secretsmanager.User{}, + DataSourceModel{}, + false, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + state := &DataSourceModel{ + ProjectId: tt.expected.ProjectId, + InstanceId: tt.expected.InstanceId, + } + err := mapDataSourceFields(tt.input, 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) + } + } + }) + } +} diff --git a/stackit/internal/services/secretsmanager/user/resource.go b/stackit/internal/services/secretsmanager/user/resource.go new file mode 100644 index 000000000..2cce6aa9c --- /dev/null +++ b/stackit/internal/services/secretsmanager/user/resource.go @@ -0,0 +1,404 @@ +package secretsmanager + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + "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/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/stackitcloud/stackit-sdk-go/core/config" + "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ resource.Resource = &userResource{} + _ resource.ResourceWithConfigure = &userResource{} + _ resource.ResourceWithImportState = &userResource{} +) + +type Model struct { + Id types.String `tfsdk:"id"` // needed by TF + UserId types.String `tfsdk:"user_id"` + InstanceId types.String `tfsdk:"instance_id"` + ProjectId types.String `tfsdk:"project_id"` + Description types.String `tfsdk:"description"` + WriteEnabled types.Bool `tfsdk:"write_enabled"` + Username types.String `tfsdk:"name"` + Password types.String `tfsdk:"password"` +} + +// NewUserResource is a helper function to simplify the provider implementation. +func NewUserResource() resource.Resource { + return &userResource{} +} + +// userResource is the resource implementation. +type userResource struct { + client *secretsmanager.APIClient +} + +// Metadata returns the resource type name. +func (r *userResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_secretsmanager_user" +} + +// Configure adds the provider configured client to the resource. +func (r *userResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + // Prevent panic if the provider has not been configured. + if req.ProviderData == nil { + return + } + + providerData, ok := req.ProviderData.(core.ProviderData) + if !ok { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData)) + return + } + + var apiClient *secretsmanager.APIClient + var err error + if providerData.SecretsManagerCustomEndpoint != "" { + apiClient, err = secretsmanager.NewAPIClient( + config.WithCustomAuth(providerData.RoundTripper), + config.WithEndpoint(providerData.SecretsManagerCustomEndpoint), + ) + } else { + apiClient, err = secretsmanager.NewAPIClient( + config.WithCustomAuth(providerData.RoundTripper), + config.WithRegion(providerData.Region), + ) + } + + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration", err)) + return + } + + r.client = apiClient + tflog.Info(ctx, "Secrets Manager user client configured") +} + +// Schema defines the schema for the resource. +func (r *userResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + descriptions := map[string]string{ + "main": "Secrets Manager user resource schema. Must have a `region` specified in the provider configuration.", + "id": "Terraform's internal resource identifier. It is structured as \"`project_id`,`instance_id`,`user_id`\".", + "user_id": "The user's ID.", + "instance_id": "ID of the Secrets Manager instance.", + "project_id": "STACKIT Project ID to which the instance is associated.", + "description": "A user chosen description to differentiate between multiple users. Can't be changed after creation.", + "write_enabled": "If true, the user has writeaccess to the secrets engine.", + "username": "An auto-generated user name.", + "password": "An auto-generated password.", + } + + 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(), + }, + }, + "user_id": schema.StringAttribute{ + Description: descriptions["user_id"], + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "instance_id": schema.StringAttribute{ + Description: descriptions["instance_id"], + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "project_id": schema.StringAttribute{ + Description: descriptions["project_id"], + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "description": schema.StringAttribute{ + Description: descriptions["description"], + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "write_enabled": schema.BoolAttribute{ + Description: descriptions["write_enabled"], + Required: true, + }, + "username": schema.StringAttribute{ + Description: descriptions["username"], + Computed: true, + }, + "password": schema.StringAttribute{ + Description: descriptions["password"], + Computed: true, + Sensitive: true, + }, + }, + } +} + +// Create creates the resource and sets the initial Terraform state. +func (r *userResource) 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 + } + projectId := model.ProjectId.ValueString() + instanceId := model.InstanceId.ValueString() + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "instance_id", instanceId) + + // Generate API request body from model + payload, err := toCreatePayload(&model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating user", fmt.Sprintf("Creating API payload: %v", err)) + return + } + // Create new user + userResp, err := r.client.CreateUser(ctx, projectId, instanceId).CreateUserPayload(*payload).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating user", fmt.Sprintf("Calling API: %v", err)) + return + } + if userResp.Id == nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating user", "Got empty user id") + return + } + userId := *userResp.Id + ctx = tflog.SetField(ctx, "user_id", userId) + + // Map response body to schema + err = mapFields(userResp, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating user", 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, "Secrets Manager user created") +} + +// Read refreshes the Terraform state with the latest data. +func (r *userResource) 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 + } + projectId := model.ProjectId.ValueString() + instanceId := model.InstanceId.ValueString() + userId := model.UserId.ValueString() + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "instance_id", instanceId) + ctx = tflog.SetField(ctx, "user_id", userId) + + userResp, err := r.client.GetUser(ctx, projectId, instanceId, userId).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading user", fmt.Sprintf("Calling API: %v", err)) + return + } + + // Map response body to schema + err = mapFields(userResp, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading user", fmt.Sprintf("Processing API payload: %v", err)) + return + } + + // Set refreshed state + diags = resp.State.Set(ctx, model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "Secrets Manager user read") +} + +// Update updates the resource and sets the updated Terraform state on success. +func (r *userResource) 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 + } + projectId := model.ProjectId.ValueString() + instanceId := model.InstanceId.ValueString() + userId := model.UserId.ValueString() + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "instance_id", instanceId) + ctx = tflog.SetField(ctx, "user_id", userId) + + // Generate API request body from model + payload, err := toUpdatePayload(&model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating user", fmt.Sprintf("Creating API payload: %v", err)) + return + } + // Update existing user + err = r.client.UpdateUser(ctx, projectId, instanceId, userId).UpdateUserPayload(*payload).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating user", err.Error()) + return + } + user, err := r.client.GetUser(ctx, projectId, instanceId, userId).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating user", fmt.Sprintf("Calling API to get user's current state: %v", err)) + return + } + + // Map response body to schema + err = mapFields(user, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating user", 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, "Secrets Manager user updated") +} + +// Delete deletes the resource and removes the Terraform state on success. +func (r *userResource) 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 + } + + projectId := model.ProjectId.ValueString() + instanceId := model.InstanceId.ValueString() + userId := model.UserId.ValueString() + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "instance_id", instanceId) + ctx = tflog.SetField(ctx, "user_id", userId) + + // Delete existing user + err := r.client.DeleteUser(ctx, projectId, instanceId, userId).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting user", fmt.Sprintf("Calling API: %v", err)) + } + + tflog.Info(ctx, "Secrets Manager user deleted") +} + +// ImportState imports a resource into the Terraform state on success. +// The expected format of the resource import identifier is: project_id,instance_id,user_id +func (r *userResource) 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] == "" { + core.LogAndAddError(ctx, &resp.Diagnostics, + "Error importing credential", + fmt.Sprintf("Expected import identifier with format [project_id],[instance_id],[user_id], got %q", req.ID), + ) + return + } + + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("instance_id"), idParts[1])...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("user_id"), idParts[2])...) + core.LogAndAddWarning(ctx, &resp.Diagnostics, + "Secrets Manager user imported with empty password", + "The user password is not imported as it is only available upon creation of a new user. The password field will be empty.", + ) + tflog.Info(ctx, "Secrets Manager user state imported") +} + +func toCreatePayload(model *Model) (*secretsmanager.CreateUserPayload, error) { + if model == nil { + return nil, fmt.Errorf("nil model") + } + return &secretsmanager.CreateUserPayload{ + Description: model.Description.ValueStringPointer(), + Write: model.WriteEnabled.ValueBoolPointer(), + }, nil +} + +func toUpdatePayload(model *Model) (*secretsmanager.UpdateUserPayload, error) { + if model == nil { + return nil, fmt.Errorf("nil model") + } + return &secretsmanager.UpdateUserPayload{ + Write: model.WriteEnabled.ValueBoolPointer(), + }, nil +} + +func mapFields(user *secretsmanager.User, model *Model) error { + if user == nil { + return fmt.Errorf("response input is nil") + } + if model == nil { + return fmt.Errorf("model input is nil") + } + + var userId string + if model.UserId.ValueString() != "" { + userId = model.UserId.ValueString() + } else if user.Id != nil { + userId = *user.Id + } else { + return fmt.Errorf("user id not present") + } + + idParts := []string{ + model.ProjectId.ValueString(), + model.InstanceId.ValueString(), + userId, + } + model.Id = types.StringValue( + strings.Join(idParts, core.Separator), + ) + model.UserId = types.StringValue(userId) + model.Description = types.StringPointerValue(user.Description) + model.WriteEnabled = types.BoolPointerValue(user.Write) + model.Username = types.StringPointerValue(user.Username) + // Password only sent in creation response, responses after that have it as "" + if user.Password != nil && *user.Password != "" { + model.Password = types.StringPointerValue(user.Password) + } + return nil +} diff --git a/stackit/internal/services/secretsmanager/user/resource_test.go b/stackit/internal/services/secretsmanager/user/resource_test.go new file mode 100644 index 000000000..2555685c4 --- /dev/null +++ b/stackit/internal/services/secretsmanager/user/resource_test.go @@ -0,0 +1,271 @@ +package secretsmanager + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/stackitcloud/stackit-sdk-go/core/utils" + "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager" +) + +func TestMapFields(t *testing.T) { + tests := []struct { + description string + input *secretsmanager.User + modelPassword *string + expected Model + isValid bool + }{ + { + "default_values", + &secretsmanager.User{ + Id: utils.Ptr("uid"), + }, + nil, + Model{ + Id: types.StringValue("pid,iid,uid"), + UserId: types.StringValue("uid"), + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + Description: types.StringNull(), + WriteEnabled: types.BoolNull(), + Username: types.StringNull(), + Password: types.StringNull(), + }, + true, + }, + { + "simple_values", + &secretsmanager.User{ + Id: utils.Ptr("uid"), + Description: utils.Ptr("description"), + Write: utils.Ptr(false), + Username: utils.Ptr("username"), + Password: utils.Ptr("password"), + }, + nil, + Model{ + Id: types.StringValue("pid,iid,uid"), + UserId: types.StringValue("uid"), + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + Description: types.StringValue("description"), + WriteEnabled: types.BoolValue(false), + Username: types.StringValue("username"), + Password: types.StringValue("password"), + }, + true, + }, + { + "nil_response", + nil, + nil, + Model{}, + false, + }, + { + "no_resource_id", + &secretsmanager.User{}, + nil, + Model{}, + false, + }, + { + "no_password_in_response_1", + &secretsmanager.User{ + Id: utils.Ptr("uid"), + Description: utils.Ptr("description"), + Write: utils.Ptr(false), + Username: utils.Ptr("username"), + }, + utils.Ptr("password"), + Model{ + Id: types.StringValue("pid,iid,uid"), + UserId: types.StringValue("uid"), + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + Description: types.StringValue("description"), + WriteEnabled: types.BoolValue(false), + Username: types.StringValue("username"), + Password: types.StringValue("password"), + }, + true, + }, + { + "no_password_in_response_2", + &secretsmanager.User{ + Id: utils.Ptr("uid"), + Description: utils.Ptr("description"), + Write: utils.Ptr(false), + Username: utils.Ptr("username"), + Password: utils.Ptr(""), + }, + utils.Ptr("password"), + Model{ + Id: types.StringValue("pid,iid,uid"), + UserId: types.StringValue("uid"), + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + Description: types.StringValue("description"), + WriteEnabled: types.BoolValue(false), + Username: types.StringValue("username"), + Password: types.StringValue("password"), + }, + true, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + state := &Model{ + ProjectId: tt.expected.ProjectId, + InstanceId: tt.expected.InstanceId, + } + if tt.modelPassword != nil { + state.Password = types.StringPointerValue(tt.modelPassword) + } + err := mapFields(tt.input, 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 + expected *secretsmanager.CreateUserPayload + isValid bool + }{ + { + "default_values", + &Model{}, + &secretsmanager.CreateUserPayload{ + Description: nil, + Write: nil, + }, + true, + }, + { + "simple_values", + &Model{ + Description: types.StringValue("description"), + WriteEnabled: types.BoolValue(false), + }, + &secretsmanager.CreateUserPayload{ + Description: utils.Ptr("description"), + Write: utils.Ptr(false), + }, + true, + }, + { + "null_fields", + &Model{ + Description: types.StringNull(), + WriteEnabled: types.BoolNull(), + }, + &secretsmanager.CreateUserPayload{ + Description: nil, + Write: nil, + }, + true, + }, + { + "empty_fields", + &Model{ + Description: types.StringValue(""), + WriteEnabled: types.BoolNull(), + }, + &secretsmanager.CreateUserPayload{ + Description: utils.Ptr(""), + Write: nil, + }, + true, + }, + { + "nil_model", + nil, + nil, + false, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + output, err := toCreatePayload(tt.input) + 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(output, tt.expected) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + } + }) + } +} + +func TestToUpdatePayload(t *testing.T) { + tests := []struct { + description string + input *Model + expected *secretsmanager.UpdateUserPayload + isValid bool + }{ + { + "default_values", + &Model{}, + &secretsmanager.UpdateUserPayload{ + Write: nil, + }, + true, + }, + { + "simple_values", + &Model{ + WriteEnabled: types.BoolValue(false), + }, + &secretsmanager.UpdateUserPayload{ + Write: utils.Ptr(false), + }, + true, + }, + { + "nil_model", + nil, + nil, + false, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + output, err := toUpdatePayload(tt.input) + 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(output, tt.expected) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + } + }) + } +} From cb4da5d643cfef2d3f51babc8684b3bd62bb6c33 Mon Sep 17 00:00:00 2001 From: Henrique Santos Date: Thu, 19 Oct 2023 10:02:52 +0100 Subject: [PATCH 02/14] Add user tests --- .../secretsmanager/secretsmanager_acc_test.go | 90 +++++++++++++++---- 1 file changed, 73 insertions(+), 17 deletions(-) diff --git a/stackit/internal/services/secretsmanager/secretsmanager_acc_test.go b/stackit/internal/services/secretsmanager/secretsmanager_acc_test.go index 6b9ff5409..9455d879e 100644 --- a/stackit/internal/services/secretsmanager/secretsmanager_acc_test.go +++ b/stackit/internal/services/secretsmanager/secretsmanager_acc_test.go @@ -25,7 +25,14 @@ var instanceResource = map[string]string{ "acl-1-updated": "111.222.111.222/22", } -func resourceConfig(acls *string) string { +// User resource data +var userResource = map[string]string{ + "description": testutil.ResourceNameWithDateTime("secretsmanager"), + "write_enabled": "false", + "write_enable_updated": "true", +} + +func resourceConfig(acls *string, writeEnabled string) string { if acls == nil { return fmt.Sprintf(` %s @@ -34,10 +41,19 @@ func resourceConfig(acls *string) string { project_id = "%s" name = "%s" } + + resource "stackit_secretsmanager_user" "user" { + project_id = stackit_postgresflex_instance.instance.project_id + instance_id = stackit_postgresflex_instance.instance.instance_id + description = "%s" + write_enabled = %s + } `, testutil.SecretsManagerProviderConfig(), instanceResource["project_id"], instanceResource["name"], + userResource["description"], + writeEnabled, ) } @@ -49,11 +65,20 @@ func resourceConfig(acls *string) string { name = "%s" acls = %s } + + resource "stackit_secretsmanager_user" "user" { + project_id = stackit_postgresflex_instance.instance.project_id + instance_id = stackit_postgresflex_instance.instance.instance_id + description = "%s" + write_enabled = %s + } `, testutil.SecretsManagerProviderConfig(), instanceResource["project_id"], instanceResource["name"], *acls, + userResource["description"], + writeEnabled, ) } @@ -65,11 +90,14 @@ func TestAccSecretsManager(t *testing.T) { // Creation { - Config: resourceConfig(utils.Ptr(fmt.Sprintf( - "[%q, %q]", - instanceResource["acl-0"], - instanceResource["acl-1"], - ))), + Config: resourceConfig( + utils.Ptr(fmt.Sprintf( + "[%q, %q]", + instanceResource["acl-0"], + instanceResource["acl-1"], + )), + userResource["write_enabled"], + ), Check: resource.ComposeAggregateTestCheckFunc( // Instance data resource.TestCheckResourceAttr("stackit_secretsmanager_instance.instance", "project_id", instanceResource["project_id"]), @@ -88,11 +116,14 @@ func TestAccSecretsManager(t *testing.T) { project_id = stackit_secretsmanager_instance.instance.project_id instance_id = stackit_secretsmanager_instance.instance.instance_id }`, - resourceConfig(utils.Ptr(fmt.Sprintf( - "[%q, %q]", - instanceResource["acl-0"], - instanceResource["acl-1"], - ))), + resourceConfig( + utils.Ptr(fmt.Sprintf( + "[%q, %q]", + instanceResource["acl-0"], + instanceResource["acl-1"], + )), + userResource["write_enabled"], + ), ), Check: resource.ComposeAggregateTestCheckFunc( // Instance data @@ -123,13 +154,38 @@ func TestAccSecretsManager(t *testing.T) { ImportState: true, ImportStateVerify: true, }, + { + ResourceName: "stackit_secretsmanager_user.user", + ImportStateIdFunc: func(s *terraform.State) (string, error) { + r, ok := s.RootModule().Resources["stackit_secretsmanager_user.user"] + if !ok { + return "", fmt.Errorf("couldn't find resource stackit_secretsmanager_user.user") + } + instanceId, ok := r.Primary.Attributes["instance_id"] + if !ok { + return "", fmt.Errorf("couldn't find attribute instance_id") + } + userId, ok := r.Primary.Attributes["user_id"] + if !ok { + return "", fmt.Errorf("couldn't find attribute user_id") + } + + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, instanceId, userId), nil + }, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"password"}, + }, // Update { - Config: resourceConfig(utils.Ptr(fmt.Sprintf( - "[%q, %q]", - instanceResource["acl-0"], - instanceResource["acl-1-updated"], - ))), + Config: resourceConfig( + utils.Ptr(fmt.Sprintf( + "[%q, %q]", + instanceResource["acl-0"], + instanceResource["acl-1-updated"], + )), + userResource["write_enabled_updated"], + ), Check: resource.ComposeAggregateTestCheckFunc( // Instance data resource.TestCheckResourceAttr("stackit_secretsmanager_instance.instance", "project_id", instanceResource["project_id"]), @@ -142,7 +198,7 @@ func TestAccSecretsManager(t *testing.T) { }, // Update, no ACLs { - Config: resourceConfig(nil), + Config: resourceConfig(nil, userResource["write_enabled_updated"]), Check: resource.ComposeAggregateTestCheckFunc( // Instance data resource.TestCheckResourceAttr("stackit_secretsmanager_instance.instance", "project_id", instanceResource["project_id"]), From aabd945a2d0b51bc2906fe6807bef50388d5b4f8 Mon Sep 17 00:00:00 2001 From: Henrique Santos Date: Thu, 19 Oct 2023 10:05:12 +0100 Subject: [PATCH 03/14] Add secrets manager user --- stackit/provider.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/stackit/provider.go b/stackit/provider.go index 537ca5466..efb4eab00 100644 --- a/stackit/provider.go +++ b/stackit/provider.go @@ -35,6 +35,7 @@ import ( redisInstance "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/redis/instance" resourceManagerProject "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/resourcemanager/project" secretsManagerInstance "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/secretsmanager/instance" + secretsManagerUser "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/secretsmanager/user" skeCluster "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/ske/cluster" skeProject "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/ske/project" @@ -350,6 +351,7 @@ func (p *Provider) DataSources(_ context.Context) []func() datasource.DataSource argusScrapeConfig.NewScrapeConfigDataSource, resourceManagerProject.NewProjectDataSource, secretsManagerInstance.NewInstanceDataSource, + secretsManagerUser.NewUserDataSource, skeProject.NewProjectDataSource, skeCluster.NewClusterDataSource, postgresFlexInstance.NewInstanceDataSource, @@ -384,6 +386,7 @@ func (p *Provider) Resources(_ context.Context) []func() resource.Resource { resourceManagerProject.NewProjectResource, argusCredential.NewCredentialResource, secretsManagerInstance.NewInstanceResource, + secretsManagerUser.NewUserResource, skeProject.NewProjectResource, skeCluster.NewClusterResource, postgresFlexInstance.NewInstanceResource, From 8d6b18c020597f7c5528997530732aa7979ead58 Mon Sep 17 00:00:00 2001 From: Henrique Santos Date: Thu, 19 Oct 2023 10:05:17 +0100 Subject: [PATCH 04/14] Fix typo --- .../services/secretsmanager/secretsmanager_acc_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/stackit/internal/services/secretsmanager/secretsmanager_acc_test.go b/stackit/internal/services/secretsmanager/secretsmanager_acc_test.go index 9455d879e..28b8a3167 100644 --- a/stackit/internal/services/secretsmanager/secretsmanager_acc_test.go +++ b/stackit/internal/services/secretsmanager/secretsmanager_acc_test.go @@ -43,8 +43,8 @@ func resourceConfig(acls *string, writeEnabled string) string { } resource "stackit_secretsmanager_user" "user" { - project_id = stackit_postgresflex_instance.instance.project_id - instance_id = stackit_postgresflex_instance.instance.instance_id + project_id = stackit_secretsmanager_instance.instance.project_id + instance_id = stackit_secretsmanager_instance.instance.instance_id description = "%s" write_enabled = %s } @@ -67,8 +67,8 @@ func resourceConfig(acls *string, writeEnabled string) string { } resource "stackit_secretsmanager_user" "user" { - project_id = stackit_postgresflex_instance.instance.project_id - instance_id = stackit_postgresflex_instance.instance.instance_id + project_id = stackit_secretsmanager_instance.instance.project_id + instance_id = stackit_secretsmanager_instance.instance.instance_id description = "%s" write_enabled = %s } From fb7f68b6e222aebf7cdd2bacc70810ff8bc1413e Mon Sep 17 00:00:00 2001 From: Henrique Santos Date: Thu, 19 Oct 2023 10:17:57 +0100 Subject: [PATCH 05/14] Change ACL to set --- .../services/secretsmanager/instance/resource.go | 15 +++++++-------- .../secretsmanager/instance/resource_test.go | 4 ++-- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/stackit/internal/services/secretsmanager/instance/resource.go b/stackit/internal/services/secretsmanager/instance/resource.go index 99d8a0c23..e689eb0e9 100644 --- a/stackit/internal/services/secretsmanager/instance/resource.go +++ b/stackit/internal/services/secretsmanager/instance/resource.go @@ -5,7 +5,7 @@ import ( "fmt" "strings" - "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/schema/validator" @@ -36,7 +36,7 @@ type Model struct { InstanceId types.String `tfsdk:"instance_id"` ProjectId types.String `tfsdk:"project_id"` Name types.String `tfsdk:"name"` - ACLs types.List `tfsdk:"acls"` + ACLs types.Set `tfsdk:"acls"` } // NewInstanceResource is a helper function to simplify the provider implementation. @@ -143,13 +143,12 @@ func (r *instanceResource) Schema(_ context.Context, _ resource.SchemaRequest, r stringvalidator.LengthAtLeast(1), }, }, - "acls": schema.ListAttribute{ + "acls": schema.SetAttribute{ Description: descriptions["acls"], ElementType: types.StringType, Optional: true, - Validators: []validator.List{ - listvalidator.UniqueValues(), - listvalidator.ValueStringsAre( + Validators: []validator.Set{ + setvalidator.ValueStringsAre( validate.CIDR(), ), }, @@ -397,7 +396,7 @@ func mapACLs(aclList *secretsmanager.AclList, model *Model) error { return fmt.Errorf("nil ACL list") } if aclList.Acls == nil || len(*aclList.Acls) == 0 { - model.ACLs = types.ListNull(types.StringType) + model.ACLs = types.SetNull(types.StringType) return nil } @@ -405,7 +404,7 @@ func mapACLs(aclList *secretsmanager.AclList, model *Model) error { for _, acl := range *aclList.Acls { acls = append(acls, types.StringValue(*acl.Cidr)) } - aclsList, diags := types.ListValue(types.StringType, acls) + aclsList, diags := types.SetValue(types.StringType, acls) if diags.HasError() { return fmt.Errorf("mapping ACLs: %w", core.DiagsToError(diags)) } diff --git a/stackit/internal/services/secretsmanager/instance/resource_test.go b/stackit/internal/services/secretsmanager/instance/resource_test.go index eeb3dbf77..6b3284189 100644 --- a/stackit/internal/services/secretsmanager/instance/resource_test.go +++ b/stackit/internal/services/secretsmanager/instance/resource_test.go @@ -36,7 +36,7 @@ func TestMapFields(t *testing.T) { InstanceId: types.StringValue("iid"), ProjectId: types.StringValue("pid"), Name: types.StringNull(), - ACLs: types.ListNull(types.StringType), + ACLs: types.SetNull(types.StringType), }, true, }, @@ -66,7 +66,7 @@ func TestMapFields(t *testing.T) { InstanceId: types.StringValue("iid"), ProjectId: types.StringValue("pid"), Name: types.StringValue("name"), - ACLs: types.ListValueMust(types.StringType, []attr.Value{ + ACLs: types.SetValueMust(types.StringType, []attr.Value{ types.StringValue("cidr-1"), types.StringValue("cidr-2"), types.StringValue("cidr-3"), From c1482edcab3baf4127cb34d8e5283d8edda4ae4e Mon Sep 17 00:00:00 2001 From: Henrique Santos Date: Thu, 19 Oct 2023 10:19:39 +0100 Subject: [PATCH 06/14] Fix field name --- stackit/internal/services/secretsmanager/user/datasource.go | 2 +- stackit/internal/services/secretsmanager/user/resource.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/stackit/internal/services/secretsmanager/user/datasource.go b/stackit/internal/services/secretsmanager/user/datasource.go index ab62b7b28..511e2d4ad 100644 --- a/stackit/internal/services/secretsmanager/user/datasource.go +++ b/stackit/internal/services/secretsmanager/user/datasource.go @@ -30,7 +30,7 @@ type DataSourceModel struct { ProjectId types.String `tfsdk:"project_id"` Description types.String `tfsdk:"description"` WriteEnabled types.Bool `tfsdk:"write_enabled"` - Username types.String `tfsdk:"name"` + Username types.String `tfsdk:"username"` } // NewUserDataSource is a helper function to simplify the provider implementation. diff --git a/stackit/internal/services/secretsmanager/user/resource.go b/stackit/internal/services/secretsmanager/user/resource.go index 2cce6aa9c..3fbafc579 100644 --- a/stackit/internal/services/secretsmanager/user/resource.go +++ b/stackit/internal/services/secretsmanager/user/resource.go @@ -34,7 +34,7 @@ type Model struct { ProjectId types.String `tfsdk:"project_id"` Description types.String `tfsdk:"description"` WriteEnabled types.Bool `tfsdk:"write_enabled"` - Username types.String `tfsdk:"name"` + Username types.String `tfsdk:"username"` Password types.String `tfsdk:"password"` } From c1e2df01156e64b3840c946437eb43a83b689687 Mon Sep 17 00:00:00 2001 From: Henrique Santos Date: Thu, 19 Oct 2023 10:21:44 +0100 Subject: [PATCH 07/14] Change ACLs to set --- stackit/internal/services/secretsmanager/instance/datasource.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stackit/internal/services/secretsmanager/instance/datasource.go b/stackit/internal/services/secretsmanager/instance/datasource.go index 3636a22ae..8178621e9 100644 --- a/stackit/internal/services/secretsmanager/instance/datasource.go +++ b/stackit/internal/services/secretsmanager/instance/datasource.go @@ -110,7 +110,7 @@ func (r *instanceDataSource) Schema(_ context.Context, _ datasource.SchemaReques Description: descriptions["name"], Computed: true, }, - "acls": schema.ListAttribute{ + "acls": schema.SetAttribute{ Description: descriptions["acls"], ElementType: types.StringType, Computed: true, From f451b94cd731dc7905e365cc8bcc567eeeb0e6e4 Mon Sep 17 00:00:00 2001 From: Henrique Santos Date: Thu, 19 Oct 2023 10:23:12 +0100 Subject: [PATCH 08/14] Fix typo --- .../services/secretsmanager/secretsmanager_acc_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/stackit/internal/services/secretsmanager/secretsmanager_acc_test.go b/stackit/internal/services/secretsmanager/secretsmanager_acc_test.go index 28b8a3167..a74900c35 100644 --- a/stackit/internal/services/secretsmanager/secretsmanager_acc_test.go +++ b/stackit/internal/services/secretsmanager/secretsmanager_acc_test.go @@ -27,9 +27,9 @@ var instanceResource = map[string]string{ // User resource data var userResource = map[string]string{ - "description": testutil.ResourceNameWithDateTime("secretsmanager"), - "write_enabled": "false", - "write_enable_updated": "true", + "description": testutil.ResourceNameWithDateTime("secretsmanager"), + "write_enabled": "false", + "write_enabled_updated": "true", } func resourceConfig(acls *string, writeEnabled string) string { From ddba953fd2b4d3eb8545a0faf08c0303eeaf4e0c Mon Sep 17 00:00:00 2001 From: Henrique Santos Date: Thu, 19 Oct 2023 10:46:54 +0100 Subject: [PATCH 09/14] Fix formatting --- .../services/secretsmanager/secretsmanager_acc_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/stackit/internal/services/secretsmanager/secretsmanager_acc_test.go b/stackit/internal/services/secretsmanager/secretsmanager_acc_test.go index a74900c35..5070018f6 100644 --- a/stackit/internal/services/secretsmanager/secretsmanager_acc_test.go +++ b/stackit/internal/services/secretsmanager/secretsmanager_acc_test.go @@ -108,7 +108,8 @@ func TestAccSecretsManager(t *testing.T) { resource.TestCheckResourceAttr("stackit_secretsmanager_instance.instance", "acls.1", instanceResource["acl-1"]), ), }, - { // Data source + // Data source + { Config: fmt.Sprintf(` %s From 0fb30f0a78ea1255f0515df95fc2ab5e4e7b2b1d Mon Sep 17 00:00:00 2001 From: Henrique Santos Date: Thu, 19 Oct 2023 10:47:03 +0100 Subject: [PATCH 10/14] Fix update not using existing password --- stackit/internal/services/secretsmanager/user/resource.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/stackit/internal/services/secretsmanager/user/resource.go b/stackit/internal/services/secretsmanager/user/resource.go index 3fbafc579..e6aee0f10 100644 --- a/stackit/internal/services/secretsmanager/user/resource.go +++ b/stackit/internal/services/secretsmanager/user/resource.go @@ -287,6 +287,12 @@ func (r *userResource) Update(ctx context.Context, req resource.UpdateRequest, r return } + // Get existing state + diags = req.State.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } // Map response body to schema err = mapFields(user, &model) if err != nil { From 8084afe35dac4c0c48e4c50e8d98cb474522c9a5 Mon Sep 17 00:00:00 2001 From: Henrique Santos Date: Thu, 19 Oct 2023 10:50:43 +0100 Subject: [PATCH 11/14] Add repeating ACLs to test case --- .../services/secretsmanager/secretsmanager_acc_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/stackit/internal/services/secretsmanager/secretsmanager_acc_test.go b/stackit/internal/services/secretsmanager/secretsmanager_acc_test.go index 5070018f6..3f51645c1 100644 --- a/stackit/internal/services/secretsmanager/secretsmanager_acc_test.go +++ b/stackit/internal/services/secretsmanager/secretsmanager_acc_test.go @@ -92,9 +92,10 @@ func TestAccSecretsManager(t *testing.T) { { Config: resourceConfig( utils.Ptr(fmt.Sprintf( - "[%q, %q]", + "[%q, %q, %q]", instanceResource["acl-0"], instanceResource["acl-1"], + instanceResource["acl-1"], )), userResource["write_enabled"], ), From 31d9f42bda01f1ef601aff61bd31ef35c8ac3fed Mon Sep 17 00:00:00 2001 From: Henrique Santos Date: Thu, 19 Oct 2023 11:30:19 +0100 Subject: [PATCH 12/14] Fix signature --- stackit/internal/services/secretsmanager/user/datasource.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/stackit/internal/services/secretsmanager/user/datasource.go b/stackit/internal/services/secretsmanager/user/datasource.go index 511e2d4ad..f3d6c6027 100644 --- a/stackit/internal/services/secretsmanager/user/datasource.go +++ b/stackit/internal/services/secretsmanager/user/datasource.go @@ -6,7 +6,6 @@ import ( "strings" "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" @@ -49,7 +48,7 @@ func (r *userDataSource) Metadata(_ context.Context, req datasource.MetadataRequ } // Configure adds the provider configured client to the data source. -func (r *userDataSource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { +func (r *userDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { // Prevent panic if the provider has not been configured. if req.ProviderData == nil { return From 00d6f58aaf31ea914fb8b53ce20c202e63ba65ec Mon Sep 17 00:00:00 2001 From: Henrique Santos Date: Thu, 19 Oct 2023 11:34:08 +0100 Subject: [PATCH 13/14] Add user checks --- .../secretsmanager/secretsmanager_acc_test.go | 78 ++++++++++++++++++- 1 file changed, 75 insertions(+), 3 deletions(-) diff --git a/stackit/internal/services/secretsmanager/secretsmanager_acc_test.go b/stackit/internal/services/secretsmanager/secretsmanager_acc_test.go index 3f51645c1..6144d937f 100644 --- a/stackit/internal/services/secretsmanager/secretsmanager_acc_test.go +++ b/stackit/internal/services/secretsmanager/secretsmanager_acc_test.go @@ -100,13 +100,28 @@ func TestAccSecretsManager(t *testing.T) { userResource["write_enabled"], ), Check: resource.ComposeAggregateTestCheckFunc( - // Instance data + // Instance resource.TestCheckResourceAttr("stackit_secretsmanager_instance.instance", "project_id", instanceResource["project_id"]), resource.TestCheckResourceAttrSet("stackit_secretsmanager_instance.instance", "instance_id"), resource.TestCheckResourceAttr("stackit_secretsmanager_instance.instance", "name", instanceResource["name"]), resource.TestCheckResourceAttr("stackit_secretsmanager_instance.instance", "acls.#", "2"), resource.TestCheckResourceAttr("stackit_secretsmanager_instance.instance", "acls.0", instanceResource["acl-0"]), resource.TestCheckResourceAttr("stackit_secretsmanager_instance.instance", "acls.1", instanceResource["acl-1"]), + + // User + resource.TestCheckResourceAttrPair( + "stackit_secretsmanager_user.user", "project_id", + "stackit_secretsmanager_instance.instance", "project_id", + ), + resource.TestCheckResourceAttrPair( + "stackit_secretsmanager_user.user", "instance_id", + "stackit_secretsmanager_instance.instance", "instance_id", + ), + resource.TestCheckResourceAttrSet("stackit_secretsmanager_user.user", "user_id"), + resource.TestCheckResourceAttr("stackit_secretsmanager_user.user", "description", userResource["description"]), + resource.TestCheckResourceAttr("stackit_secretsmanager_user.user", "write_enabled", userResource["write_enabled"]), + resource.TestCheckResourceAttrSet("stackit_secretsmanager_user.user", "username"), + resource.TestCheckResourceAttrSet("stackit_secretsmanager_user.user", "password"), ), }, // Data source @@ -117,6 +132,12 @@ func TestAccSecretsManager(t *testing.T) { data "stackit_secretsmanager_instance" "instance" { project_id = stackit_secretsmanager_instance.instance.project_id instance_id = stackit_secretsmanager_instance.instance.instance_id + } + + data "stackit_secretsmanager_user" "user" { + project_id = stackit_secretsmanager_user.user.project_id + instance_id = stackit_secretsmanager_user.user.instance_id + user_id = stackit_secretsmanager_user.user.user_id }`, resourceConfig( utils.Ptr(fmt.Sprintf( @@ -128,7 +149,7 @@ func TestAccSecretsManager(t *testing.T) { ), ), Check: resource.ComposeAggregateTestCheckFunc( - // Instance data + // Instance resource.TestCheckResourceAttr("data.stackit_secretsmanager_instance.instance", "project_id", instanceResource["project_id"]), resource.TestCheckResourceAttrPair( "stackit_secretsmanager_instance.instance", "instance_id", @@ -137,6 +158,26 @@ func TestAccSecretsManager(t *testing.T) { resource.TestCheckResourceAttr("data.stackit_secretsmanager_instance.instance", "name", instanceResource["name"]), resource.TestCheckResourceAttr("data.stackit_secretsmanager_instance.instance", "acls.0", instanceResource["acl-0"]), resource.TestCheckResourceAttr("data.stackit_secretsmanager_instance.instance", "acls.1", instanceResource["acl-1"]), + + // User + resource.TestCheckResourceAttrPair( + "stackit_secretsmanager_user.user", "project_id", + "data.stackit_secretsmanager_user.user", "project_id", + ), + resource.TestCheckResourceAttrPair( + "stackit_secretsmanager_user.user", "instance_id", + "data.stackit_secretsmanager_user.user", "instance_id", + ), + resource.TestCheckResourceAttrPair( + "stackit_secretsmanager_user.user", "user_id", + "data.stackit_secretsmanager_user.user", "user_id", + ), + resource.TestCheckResourceAttr("data.stackit_secretsmanager_user.user", "description", userResource["description"]), + resource.TestCheckResourceAttr("data.stackit_secretsmanager_user.user", "write_enabled", userResource["write_enabled"]), + resource.TestCheckResourceAttrPair( + "stackit_secretsmanager_user.user", "username", + "data.stackit_secretsmanager_user.user", "username", + ), ), }, // Import @@ -177,6 +218,7 @@ func TestAccSecretsManager(t *testing.T) { ImportState: true, ImportStateVerify: true, ImportStateVerifyIgnore: []string{"password"}, + Check: resource.TestCheckNoResourceAttr("stackit_secretsmanager_user.user", "password"), }, // Update { @@ -189,13 +231,28 @@ func TestAccSecretsManager(t *testing.T) { userResource["write_enabled_updated"], ), Check: resource.ComposeAggregateTestCheckFunc( - // Instance data + // Instance resource.TestCheckResourceAttr("stackit_secretsmanager_instance.instance", "project_id", instanceResource["project_id"]), resource.TestCheckResourceAttrSet("stackit_secretsmanager_instance.instance", "instance_id"), resource.TestCheckResourceAttr("stackit_secretsmanager_instance.instance", "name", instanceResource["name"]), resource.TestCheckResourceAttr("stackit_secretsmanager_instance.instance", "acls.#", "2"), resource.TestCheckResourceAttr("stackit_secretsmanager_instance.instance", "acls.0", instanceResource["acl-0"]), resource.TestCheckResourceAttr("stackit_secretsmanager_instance.instance", "acls.1", instanceResource["acl-1-updated"]), + + // User + resource.TestCheckResourceAttrPair( + "stackit_secretsmanager_user.user", "project_id", + "stackit_secretsmanager_instance.instance", "project_id", + ), + resource.TestCheckResourceAttrPair( + "stackit_secretsmanager_user.user", "instance_id", + "stackit_secretsmanager_instance.instance", "instance_id", + ), + resource.TestCheckResourceAttrSet("stackit_secretsmanager_user.user", "user_id"), + resource.TestCheckResourceAttr("stackit_secretsmanager_user.user", "description", userResource["description"]), + resource.TestCheckResourceAttr("stackit_secretsmanager_user.user", "write_enabled", userResource["write_enabled_updated"]), + resource.TestCheckResourceAttrSet("stackit_secretsmanager_user.user", "username"), + resource.TestCheckResourceAttrSet("stackit_secretsmanager_user.user", "password"), ), }, // Update, no ACLs @@ -207,6 +264,21 @@ func TestAccSecretsManager(t *testing.T) { resource.TestCheckResourceAttrSet("stackit_secretsmanager_instance.instance", "instance_id"), resource.TestCheckResourceAttr("stackit_secretsmanager_instance.instance", "name", instanceResource["name"]), resource.TestCheckResourceAttr("stackit_secretsmanager_instance.instance", "acls.#", "0"), + + // User + resource.TestCheckResourceAttrPair( + "stackit_secretsmanager_user.user", "project_id", + "stackit_secretsmanager_instance.instance", "project_id", + ), + resource.TestCheckResourceAttrPair( + "stackit_secretsmanager_user.user", "instance_id", + "stackit_secretsmanager_instance.instance", "instance_id", + ), + resource.TestCheckResourceAttrSet("stackit_secretsmanager_user.user", "user_id"), + resource.TestCheckResourceAttr("stackit_secretsmanager_user.user", "description", userResource["description"]), + resource.TestCheckResourceAttr("stackit_secretsmanager_user.user", "write_enabled", userResource["write_enabled_updated"]), + resource.TestCheckResourceAttrSet("stackit_secretsmanager_user.user", "username"), + resource.TestCheckResourceAttrSet("stackit_secretsmanager_user.user", "password"), ), }, // Deletion is done by the framework implicitly From 10fb519a9b2edeecc5772213b3da5e93503bb92b Mon Sep 17 00:00:00 2001 From: Henrique Santos Date: Thu, 19 Oct 2023 11:38:54 +0100 Subject: [PATCH 14/14] Reorder list --- stackit/provider.go | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/stackit/provider.go b/stackit/provider.go index efb4eab00..7f3aee538 100644 --- a/stackit/provider.go +++ b/stackit/provider.go @@ -328,10 +328,10 @@ 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{ + argusInstance.NewInstanceDataSource, + argusScrapeConfig.NewScrapeConfigDataSource, dnsZone.NewZoneDataSource, dnsRecordSet.NewRecordSetDataSource, - postgresInstance.NewInstanceDataSource, - postgresCredential.NewCredentialDataSource, logMeInstance.NewInstanceDataSource, logMeCredential.NewCredentialDataSource, mariaDBInstance.NewInstanceDataSource, @@ -343,29 +343,30 @@ func (p *Provider) DataSources(_ context.Context) []func() datasource.DataSource objecStorageCredential.NewCredentialDataSource, openSearchInstance.NewInstanceDataSource, openSearchCredential.NewCredentialDataSource, + postgresFlexInstance.NewInstanceDataSource, + postgresFlexUser.NewUserDataSource, + postgresInstance.NewInstanceDataSource, + postgresCredential.NewCredentialDataSource, rabbitMQInstance.NewInstanceDataSource, rabbitMQCredential.NewCredentialDataSource, redisInstance.NewInstanceDataSource, redisCredential.NewCredentialDataSource, - argusInstance.NewInstanceDataSource, - argusScrapeConfig.NewScrapeConfigDataSource, resourceManagerProject.NewProjectDataSource, secretsManagerInstance.NewInstanceDataSource, secretsManagerUser.NewUserDataSource, skeProject.NewProjectDataSource, skeCluster.NewClusterDataSource, - postgresFlexInstance.NewInstanceDataSource, - postgresFlexUser.NewUserDataSource, } } // Resources defines the resources implemented in the provider. func (p *Provider) Resources(_ context.Context) []func() resource.Resource { return []func() resource.Resource{ + argusCredential.NewCredentialResource, + argusInstance.NewInstanceResource, + argusScrapeConfig.NewScrapeConfigResource, dnsZone.NewZoneResource, dnsRecordSet.NewRecordSetResource, - postgresInstance.NewInstanceResource, - postgresCredential.NewCredentialResource, logMeInstance.NewInstanceResource, logMeCredential.NewCredentialResource, mariaDBInstance.NewInstanceResource, @@ -377,19 +378,18 @@ func (p *Provider) Resources(_ context.Context) []func() resource.Resource { objecStorageCredential.NewCredentialResource, openSearchInstance.NewInstanceResource, openSearchCredential.NewCredentialResource, + postgresFlexInstance.NewInstanceResource, + postgresFlexUser.NewUserResource, + postgresInstance.NewInstanceResource, + postgresCredential.NewCredentialResource, rabbitMQInstance.NewInstanceResource, rabbitMQCredential.NewCredentialResource, redisInstance.NewInstanceResource, redisCredential.NewCredentialResource, - argusInstance.NewInstanceResource, - argusScrapeConfig.NewScrapeConfigResource, resourceManagerProject.NewProjectResource, - argusCredential.NewCredentialResource, secretsManagerInstance.NewInstanceResource, secretsManagerUser.NewUserResource, skeProject.NewProjectResource, skeCluster.NewClusterResource, - postgresFlexInstance.NewInstanceResource, - postgresFlexUser.NewUserResource, } }