diff --git a/go.mod b/go.mod index f9fdb597..ceca4732 100644 --- a/go.mod +++ b/go.mod @@ -22,6 +22,7 @@ require ( github.com/stackitcloud/stackit-sdk-go/services/rabbitmq v0.4.0 github.com/stackitcloud/stackit-sdk-go/services/redis v0.4.0 github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.3.0 + github.com/stackitcloud/stackit-sdk-go/services/secretsmanager v0.2.0 github.com/stackitcloud/stackit-sdk-go/services/ske v0.3.0 golang.org/x/mod v0.13.0 ) diff --git a/go.sum b/go.sum index c2509b6a..21a16b1d 100644 --- a/go.sum +++ b/go.sum @@ -149,6 +149,8 @@ github.com/stackitcloud/stackit-sdk-go/services/redis v0.4.0 h1:K+C+VBCNRH90aVkl github.com/stackitcloud/stackit-sdk-go/services/redis v0.4.0/go.mod h1:OzuVC+kPkhAoGxdUkIRD+CbFGWG0Ye6kfZOv7octFN4= github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.3.0 h1:VcMBLwARKFiEnc28ZCiRx9v6RAhIzofxAUah03Vxg/g= github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.3.0/go.mod h1:P9ZpI/G/LG0+enyEdr7zpsaikD5J085Np6IWK6dzBEk= +github.com/stackitcloud/stackit-sdk-go/services/secretsmanager v0.2.0 h1:i8yCH0bkdQKDTyAi5Qy9/PSLruoQ/Dbh22zBjhOWmzs= +github.com/stackitcloud/stackit-sdk-go/services/secretsmanager v0.2.0/go.mod h1:NyKeNilgXEpo1akNUODAz5IpBRDSc3suHyDDwYLf9JY= github.com/stackitcloud/stackit-sdk-go/services/ske v0.3.0 h1:umS4cjV2jhtBUEYr/7QWpe95tDgANng1Wma4rF9k/Hk= github.com/stackitcloud/stackit-sdk-go/services/ske v0.3.0/go.mod h1:pVb/CaolkqaicGbHpXWUOXk5+s731TpXbx6RMyYjV1Q= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= diff --git a/stackit/internal/core/core.go b/stackit/internal/core/core.go index 67a5b526..89d4c8cb 100644 --- a/stackit/internal/core/core.go +++ b/stackit/internal/core/core.go @@ -26,6 +26,7 @@ type ProviderData struct { ObjectStorageCustomEndpoint string OpenSearchCustomEndpoint string RedisCustomEndpoint string + SecretsManagerCustomEndpoint string ArgusCustomEndpoint string SKECustomEndpoint string ResourceManagerCustomEndpoint string diff --git a/stackit/internal/services/secretsmanager/instance/datasource.go b/stackit/internal/services/secretsmanager/instance/datasource.go new file mode 100644 index 00000000..ce3ffd83 --- /dev/null +++ b/stackit/internal/services/secretsmanager/instance/datasource.go @@ -0,0 +1,147 @@ +package secretsmanager + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "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/stackitcloud/stackit-sdk-go/core/config" + "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ datasource.DataSource = &instanceDataSource{} +) + +// NewInstanceDataSource is a helper function to simplify the provider implementation. +func NewInstanceDataSource() datasource.DataSource { + return &instanceDataSource{} +} + +// instanceDataSource is the data source implementation. +type instanceDataSource struct { + client *secretsmanager.APIClient +} + +// Metadata returns the data source type name. +func (r *instanceDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_secretsmanager_instance" +} + +// Configure adds the provider configured client to the data source. +func (r *instanceDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.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", err)) + return + } + + r.client = apiClient + tflog.Info(ctx, "Secrets Manager instance client configured") +} + +// Schema defines the schema for the data source. +func (r *instanceDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + descriptions := map[string]string{ + "main": "Secrets Manager instance data source schema.", + "id": "Terraform's internal resource ID. It is structured as \"`project_id`,`instance_id`\".", + "instance_id": "ID of the Secrets Manager instance.", + "project_id": "STACKIT project ID to which the instance is associated.", + "name": "Instance name.", + } + + resp.Schema = schema.Schema{ + Description: descriptions["main"], + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: descriptions["id"], + Computed: true, + }, + "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(), + }, + }, + "name": schema.StringAttribute{ + Description: descriptions["name"], + Computed: true, + }, + }, + } +} + +// Read refreshes the Terraform state with the latest data. +func (r *instanceDataSource) 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 + } + projectId := model.ProjectId.ValueString() + instanceId := model.InstanceId.ValueString() + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "instance_id", instanceId) + + instanceResp, err := r.client.GetInstance(ctx, projectId, instanceId).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Calling API: %v", err)) + return + } + + err = mapFields(instanceResp, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", 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 instance read") +} diff --git a/stackit/internal/services/secretsmanager/instance/resource.go b/stackit/internal/services/secretsmanager/instance/resource.go new file mode 100644 index 00000000..89c2e2be --- /dev/null +++ b/stackit/internal/services/secretsmanager/instance/resource.go @@ -0,0 +1,306 @@ +package secretsmanager + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "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 = &instanceResource{} + _ resource.ResourceWithConfigure = &instanceResource{} + _ resource.ResourceWithImportState = &instanceResource{} +) + +type Model struct { + Id types.String `tfsdk:"id"` // needed by TF + InstanceId types.String `tfsdk:"instance_id"` + ProjectId types.String `tfsdk:"project_id"` + Name types.String `tfsdk:"name"` +} + +// NewInstanceResource is a helper function to simplify the provider implementation. +func NewInstanceResource() resource.Resource { + return &instanceResource{} +} + +// instanceResource is the resource implementation. +type instanceResource struct { + client *secretsmanager.APIClient +} + +// Metadata returns the resource type name. +func (r *instanceResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_secretsmanager_instance" +} + +// Configure adds the provider configured client to the resource. +func (r *instanceResource) 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", err)) + return + } + + r.client = apiClient + tflog.Info(ctx, "Secrets Manager instance client configured") +} + +// Schema defines the schema for the resource. +func (r *instanceResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + descriptions := map[string]string{ + "main": "Secrets Manager instance resource schema.", + "id": "Terraform's internal resource ID. It is structured as \"`project_id`,`instance_id`\".", + "instance_id": "ID of the Secrets Manager instance.", + "project_id": "STACKIT project ID to which the instance is associated.", + "name": "Instance name.", + } + + 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(), + }, + }, + "instance_id": schema.StringAttribute{ + Description: descriptions["instance_id"], + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + 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(), + }, + }, + "name": schema.StringAttribute{ + Description: descriptions["name"], + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + }, + } +} + +// Create creates the resource and sets the initial Terraform state. +func (r *instanceResource) 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() + ctx = tflog.SetField(ctx, "project_id", projectId) + + // Generate API request body from model + payload, err := toCreatePayload(&model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Creating API payload: %v", err)) + return + } + // Create new instance + createResp, err := r.client.CreateInstance(ctx, projectId).CreateInstancePayload(*payload).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Calling API: %v", err)) + return + } + instanceId := *createResp.Id + ctx = tflog.SetField(ctx, "instance_id", instanceId) + + // Map response body to schema + err = mapFields(createResp, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Processing API payload: %v", err)) + return + } + + // Set state to fully populated data + diags = resp.State.Set(ctx, model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "Secrets Manager instance created") +} + +// Read refreshes the Terraform state with the latest data. +func (r *instanceResource) 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() + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "instance_id", instanceId) + + instanceResp, err := r.client.GetInstance(ctx, projectId, instanceId).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Calling API: %v", err)) + return + } + + // Map response body to schema + err = mapFields(instanceResp, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", 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 instance read") +} + +// Update updates the resource and sets the updated Terraform state on success. +func (r *instanceResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform + // Update shouldn't be called + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", "Instance can't be updated") +} + +// Delete deletes the resource and removes the Terraform state on success. +func (r *instanceResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform + // Retrieve values from state + 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() + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "instance_id", instanceId) + + // Delete existing instance + err := r.client.DeleteInstance(ctx, projectId, instanceId).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting instance", fmt.Sprintf("Calling API: %v", err)) + return + } + tflog.Info(ctx, "Secrets Manager instance deleted") +} + +// ImportState imports a resource into the Terraform state on success. +// The expected format of the resource import identifier is: project_id,instance_id +func (r *instanceResource) 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 instance", + fmt.Sprintf("Expected import identifier with format: [project_id],[instance_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])...) + tflog.Info(ctx, "Secrets Manager instance state imported") +} + +func mapFields(instance *secretsmanager.Instance, model *Model) error { + if instance == nil { + return fmt.Errorf("response input is nil") + } + if model == nil { + return fmt.Errorf("model input is nil") + } + + var instanceId string + if model.InstanceId.ValueString() != "" { + instanceId = model.InstanceId.ValueString() + } else if instance.Id != nil { + instanceId = *instance.Id + } else { + return fmt.Errorf("instance id not present") + } + + idParts := []string{ + model.ProjectId.ValueString(), + instanceId, + } + model.Id = types.StringValue( + strings.Join(idParts, core.Separator), + ) + model.InstanceId = types.StringValue(instanceId) + model.Name = types.StringPointerValue(instance.Name) + + return nil +} + +func toCreatePayload(model *Model) (*secretsmanager.CreateInstancePayload, error) { + if model == nil { + return nil, fmt.Errorf("nil model") + } + return &secretsmanager.CreateInstancePayload{ + Name: model.Name.ValueStringPointer(), + }, nil +} diff --git a/stackit/internal/services/secretsmanager/instance/resource_test.go b/stackit/internal/services/secretsmanager/instance/resource_test.go new file mode 100644 index 00000000..3b133c02 --- /dev/null +++ b/stackit/internal/services/secretsmanager/instance/resource_test.go @@ -0,0 +1,136 @@ +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.Instance + expected Model + isValid bool + }{ + { + "default_values", + &secretsmanager.Instance{}, + Model{ + Id: types.StringValue("pid,iid"), + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + Name: types.StringNull(), + }, + true, + }, + { + "simple_values", + &secretsmanager.Instance{ + Name: utils.Ptr("name"), + }, + Model{ + Id: types.StringValue("pid,iid"), + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + Name: types.StringValue("name"), + }, + true, + }, + { + "nil_response", + nil, + Model{}, + false, + }, + { + "no_resource_id", + &secretsmanager.Instance{}, + Model{}, + false, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + state := &Model{ + ProjectId: tt.expected.ProjectId, + InstanceId: tt.expected.InstanceId, + } + 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.CreateInstancePayload + isValid bool + }{ + { + "default_values", + &Model{}, + &secretsmanager.CreateInstancePayload{}, + true, + }, + { + "simple_values", + &Model{ + Name: types.StringValue("name"), + }, + &secretsmanager.CreateInstancePayload{ + Name: utils.Ptr("name"), + }, + true, + }, + { + "null_fields_and_int_conversions", + &Model{ + Name: types.StringValue(""), + }, + &secretsmanager.CreateInstancePayload{ + Name: utils.Ptr(""), + }, + 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) + } + } + }) + } +} diff --git a/stackit/internal/services/secretsmanager/secretsmanager_acc_test.go b/stackit/internal/services/secretsmanager/secretsmanager_acc_test.go new file mode 100644 index 00000000..5b268cca --- /dev/null +++ b/stackit/internal/services/secretsmanager/secretsmanager_acc_test.go @@ -0,0 +1,140 @@ +package secretsmanager_test + +import ( + "context" + "fmt" + "strings" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/stackitcloud/stackit-sdk-go/core/config" + "github.com/stackitcloud/stackit-sdk-go/core/utils" + "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil" +) + +// Instance resource data +var instanceResource = map[string]string{ + "project_id": testutil.ProjectId, + "name": fmt.Sprintf("acc-test-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)), +} + +func resourceConfig() string { + return fmt.Sprintf(` + %s + + resource "stackit_secretsmanager_instance" "instance" { + project_id = "%s" + name = "%s" + } + `, + testutil.SecretsManagerProviderConfig(), + instanceResource["project_id"], + instanceResource["name"], + ) +} +func TestAccSecretsManager(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, + CheckDestroy: testAccCheckSecretsManagerDestroy, + Steps: []resource.TestStep{ + + // Creation + { + Config: resourceConfig(), + Check: resource.ComposeAggregateTestCheckFunc( + // Instance data + 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"]), + ), + }, + { // Data source + Config: fmt.Sprintf(` + %s + + data "stackit_secretsmanager_instance" "instance" { + project_id = stackit_secretsmanager_instance.instance.project_id + instance_id = stackit_secretsmanager_instance.instance.instance_id + }`, + resourceConfig(), + ), + Check: resource.ComposeAggregateTestCheckFunc( + // Instance data + resource.TestCheckResourceAttr("data.stackit_secretsmanager_instance.instance", "project_id", instanceResource["project_id"]), + resource.TestCheckResourceAttrPair( + "stackit_secretsmanager_instance.instance", "instance_id", + "data.stackit_secretsmanager_instance.instance", "instance_id", + ), + resource.TestCheckResourceAttr("data.stackit_secretsmanager_instance.instance", "name", instanceResource["name"]), + ), + }, + // Import + { + ResourceName: "stackit_secretsmanager_instance.instance", + ImportStateIdFunc: func(s *terraform.State) (string, error) { + r, ok := s.RootModule().Resources["stackit_secretsmanager_instance.instance"] + if !ok { + return "", fmt.Errorf("couldn't find resource stackit_secretsmanager_instance.instance") + } + instanceId, ok := r.Primary.Attributes["instance_id"] + if !ok { + return "", fmt.Errorf("couldn't find attribute instance_id") + } + return fmt.Sprintf("%s,%s", testutil.ProjectId, instanceId), nil + }, + ImportState: true, + ImportStateVerify: true, + }, + // Deletion is done by the framework implicitly + }, + }) +} + +func testAccCheckSecretsManagerDestroy(s *terraform.State) error { + ctx := context.Background() + var client *secretsmanager.APIClient + var err error + if testutil.SecretsManagerCustomEndpoint == "" { + client, err = secretsmanager.NewAPIClient() + } else { + client, err = secretsmanager.NewAPIClient( + config.WithEndpoint(testutil.SecretsManagerCustomEndpoint), + ) + } + if err != nil { + return fmt.Errorf("creating client: %w", err) + } + + instancesToDestroy := []string{} + for _, rs := range s.RootModule().Resources { + if rs.Type != "stackit_secretsmanager_instance" { + continue + } + // instance terraform ID: "[project_id],[instance_id]" + instanceId := strings.Split(rs.Primary.ID, core.Separator)[1] + instancesToDestroy = append(instancesToDestroy, instanceId) + } + + instancesResp, err := client.GetInstances(ctx, testutil.ProjectId).Execute() + if err != nil { + return fmt.Errorf("getting instancesResp: %w", err) + } + + instances := *instancesResp.Instances + for i := range instances { + if instances[i].Id == nil { + continue + } + if utils.Contains(instancesToDestroy, *instances[i].Id) { + err := client.DeleteInstanceExecute(ctx, testutil.ProjectId, *instances[i].Id) + if err != nil { + return fmt.Errorf("destroying instance %s during CheckDestroy: %w", *instances[i].Id, err) + } + } + } + return nil +} diff --git a/stackit/internal/testutil/testutil.go b/stackit/internal/testutil/testutil.go index 54b6373c..c4c3602f 100644 --- a/stackit/internal/testutil/testutil.go +++ b/stackit/internal/testutil/testutil.go @@ -46,6 +46,7 @@ var ( RabbitMQCustomEndpoint = os.Getenv("TF_ACC_RABBITMQ_CUSTOM_ENDPOINT") RedisCustomEndpoint = os.Getenv("TF_ACC_REDIS_CUSTOM_ENDPOINT") ResourceManagerCustomEndpoint = os.Getenv("TF_ACC_RESOURCEMANAGER_CUSTOM_ENDPOINT") + SecretsManagerCustomEndpoint = os.Getenv("TF_ACC_SECRETSMANAGER_CUSTOM_ENDPOINT") SKECustomEndpoint = os.Getenv("TF_ACC_SKE_CUSTOM_ENDPOINT") ) @@ -219,6 +220,21 @@ func ResourceManagerProviderConfig() string { ) } +func SecretsManagerProviderConfig() string { + if SecretsManagerCustomEndpoint == "" { + return ` + provider "stackit" { + region = "eu01" + }` + } + return fmt.Sprintf(` + provider "stackit" { + secretsmanager_custom_endpoint = "%s" + }`, + SecretsManagerCustomEndpoint, + ) +} + func SKEProviderConfig() string { if SKECustomEndpoint == "" { return ` diff --git a/stackit/provider.go b/stackit/provider.go index 77b3da50..2da1e845 100644 --- a/stackit/provider.go +++ b/stackit/provider.go @@ -32,6 +32,7 @@ import ( redisCredential "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/redis/credential" 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" skeCluster "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/ske/cluster" skeProject "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/ske/project" @@ -83,6 +84,7 @@ type providerModel struct { ObjectStorageCustomEndpoint types.String `tfsdk:"objectstorage_custom_endpoint"` OpenSearchCustomEndpoint types.String `tfsdk:"opensearch_custom_endpoint"` RedisCustomEndpoint types.String `tfsdk:"redis_custom_endpoint"` + SecretsManagerCustomEndpoint types.String `tfsdk:"secretsmanager_custom_endpoint"` ArgusCustomEndpoint types.String `tfsdk:"argus_custom_endpoint"` SKECustomEndpoint types.String `tfsdk:"ske_custom_endpoint"` ResourceManagerCustomEndpoint types.String `tfsdk:"resourcemanager_custom_endpoint"` @@ -112,6 +114,7 @@ func (p *Provider) Schema(_ context.Context, _ provider.SchemaRequest, resp *pro "argus_custom_endpoint": "Custom endpoint for the Argus service", "ske_custom_endpoint": "Custom endpoint for the Kubernetes Engine (SKE) service", "resourcemanager_custom_endpoint": "Custom endpoint for the Resource Manager service", + "secretsmanager_custom_endpoint": "Custom endpoint for the Secrets Manager service", "token_custom_endpoint": "Custom endpoint for the token API, which is used to request access tokens when using the key flow", "jwks_custom_endpoint": "Custom endpoint for the jwks API, which is used to get the json web key sets (jwks) to validate tokens when using the key flow", } @@ -186,6 +189,10 @@ func (p *Provider) Schema(_ context.Context, _ provider.SchemaRequest, resp *pro Optional: true, Description: descriptions["redis_custom_endpoint"], }, + "secretsmanager_custom_endpoint": schema.StringAttribute{ + Optional: true, + Description: descriptions["secretsmanager_custom_endpoint"], + }, "argus_custom_endpoint": schema.StringAttribute{ Optional: true, Description: descriptions["argus_custom_endpoint"], @@ -284,6 +291,9 @@ func (p *Provider) Configure(ctx context.Context, req provider.ConfigureRequest, if !(providerConfig.ResourceManagerCustomEndpoint.IsUnknown() || providerConfig.ResourceManagerCustomEndpoint.IsNull()) { providerData.ResourceManagerCustomEndpoint = providerConfig.ResourceManagerCustomEndpoint.ValueString() } + if !(providerConfig.SecretsManagerCustomEndpoint.IsUnknown() || providerConfig.SecretsManagerCustomEndpoint.IsNull()) { + providerData.SecretsManagerCustomEndpoint = providerConfig.SecretsManagerCustomEndpoint.ValueString() + } if !(providerConfig.TokenCustomEndpoint.IsUnknown() || providerConfig.TokenCustomEndpoint.IsNull()) { sdkConfig.TokenCustomUrl = providerConfig.TokenCustomEndpoint.ValueString() } @@ -326,6 +336,7 @@ func (p *Provider) DataSources(_ context.Context) []func() datasource.DataSource argusInstance.NewInstanceDataSource, argusScrapeConfig.NewScrapeConfigDataSource, resourceManagerProject.NewProjectDataSource, + secretsManagerInstance.NewInstanceDataSource, skeProject.NewProjectDataSource, skeCluster.NewClusterDataSource, postgresFlexInstance.NewInstanceDataSource, @@ -357,6 +368,7 @@ func (p *Provider) Resources(_ context.Context) []func() resource.Resource { argusScrapeConfig.NewScrapeConfigResource, resourceManagerProject.NewProjectResource, argusCredential.NewCredentialResource, + secretsManagerInstance.NewInstanceResource, skeProject.NewProjectResource, skeCluster.NewClusterResource, postgresFlexInstance.NewInstanceResource,