From b53fca26ec1c4c7aebdcbfca16ea3af65713946b Mon Sep 17 00:00:00 2001 From: Jonas Schlecht Date: Wed, 1 Jul 2026 09:08:02 +0200 Subject: [PATCH 1/2] feat(objectstorage): add default retention for buckets Relates to STACKITTPR-727 --- .../objectstorage_default_retention.md | 31 ++ .../objectstorage_default_retention.md | 31 ++ .../default-retention/datasource.go | 162 +++++++ .../default-retention/resource.go | 412 ++++++++++++++++++ .../default-retention/resource_test.go | 84 ++++ .../objectstorage/objectstorage_acc_test.go | 37 +- .../objectstorage/testfiles/resource-min.tf | 13 +- stackit/provider.go | 3 + 8 files changed, 771 insertions(+), 2 deletions(-) create mode 100644 docs/data-sources/objectstorage_default_retention.md create mode 100644 docs/resources/objectstorage_default_retention.md create mode 100644 stackit/internal/services/objectstorage/default-retention/datasource.go create mode 100644 stackit/internal/services/objectstorage/default-retention/resource.go create mode 100644 stackit/internal/services/objectstorage/default-retention/resource_test.go diff --git a/docs/data-sources/objectstorage_default_retention.md b/docs/data-sources/objectstorage_default_retention.md new file mode 100644 index 000000000..f8217fa5f --- /dev/null +++ b/docs/data-sources/objectstorage_default_retention.md @@ -0,0 +1,31 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "stackit_objectstorage_default_retention Data Source - stackit" +subcategory: "" +description: |- + ObjectStorage default-retention resource schema. Must have a region specified in the provider configuration. +--- + +# stackit_objectstorage_default_retention (Data Source) + +ObjectStorage default-retention resource schema. Must have a `region` specified in the provider configuration. + + + + +## Schema + +### Required + +- `bucket_name` (String) The associated bucket's name. It must be DNS conform. +- `days` (Number) The number retention period in days. +- `mode` (String) The retention mode for default retention on a bucket. +- `project_id` (String) STACKIT Project ID to which the default-retention is associated. + +### Optional + +- `region` (String) The resource region. If not defined, the provider region is used. + +### Read-Only + +- `id` (String) Terraform's internal resource identifier. It is structured as "`project_id`,`region`,`bucket_name`". diff --git a/docs/resources/objectstorage_default_retention.md b/docs/resources/objectstorage_default_retention.md new file mode 100644 index 000000000..93f0b404b --- /dev/null +++ b/docs/resources/objectstorage_default_retention.md @@ -0,0 +1,31 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "stackit_objectstorage_default_retention Resource - stackit" +subcategory: "" +description: |- + ObjectStorage default-retention resource schema. Must have a region specified in the provider configuration. +--- + +# stackit_objectstorage_default_retention (Resource) + +ObjectStorage default-retention resource schema. Must have a `region` specified in the provider configuration. + + + + +## Schema + +### Required + +- `bucket_name` (String) The associated bucket's name. It must be DNS conform. +- `days` (Number) The number retention period in days. +- `mode` (String) The retention mode for default retention on a bucket. +- `project_id` (String) STACKIT Project ID to which the default-retention is associated. + +### Optional + +- `region` (String) The resource region. If not defined, the provider region is used. + +### Read-Only + +- `id` (String) Terraform's internal resource identifier. It is structured as "`project_id`,`region`,`bucket_name`". diff --git a/stackit/internal/services/objectstorage/default-retention/datasource.go b/stackit/internal/services/objectstorage/default-retention/datasource.go new file mode 100644 index 000000000..2e2a06b38 --- /dev/null +++ b/stackit/internal/services/objectstorage/default-retention/datasource.go @@ -0,0 +1,162 @@ +package objectstorage + +import ( + "context" + "errors" + "fmt" + "net/http" + + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/stackitcloud/stackit-sdk-go/core/oapierror" + sdkUtils "github.com/stackitcloud/stackit-sdk-go/core/utils" + defaultretention "github.com/stackitcloud/stackit-sdk-go/services/objectstorage/v2api" + objectstorage "github.com/stackitcloud/stackit-sdk-go/services/objectstorage/v2api" + + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + objectstorageUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/objectstorage/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" +) + +var ( + _ datasource.DataSourceWithConfigure = &defaultRetentionDataSource{} +) + +func NewDefaultRetentionDataSource() datasource.DataSource { + return &defaultRetentionDataSource{} +} + +type defaultRetentionDataSource struct { + client *objectstorage.APIClient + providerData core.ProviderData +} + +// Configure implements [datasource.DataSourceWithConfigure]. +func (r *defaultRetentionDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + var ok bool + r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + if !ok { + return + } + + apiClient := objectstorageUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + r.client = apiClient + tflog.Info(ctx, "Application Load Balancer client configured") +} + +// Schema implements [datasource.DataSource]. +func (d *defaultRetentionDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { + descriptions := map[string]string{ + "main": "ObjectStorage default-retention resource schema. Must have a `region` specified in the provider configuration.", + "id": "Terraform's internal resource identifier. It is structured as \"`project_id`,`region`,`bucket_name`\".", + "bucket_name": "The associated bucket's name. It must be DNS conform.", + "project_id": "STACKIT Project ID to which the default-retention is associated.", + "region": "The resource region. If not defined, the provider region is used.", + "days": "The number retention period in days.", + "mode": "The retention mode for default retention on a bucket.", + } + + resp.Schema = schema.Schema{ + Description: descriptions["main"], + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: descriptions["id"], + Computed: true, + }, + "bucket_name": schema.StringAttribute{ + Description: descriptions["bucket_name"], + Required: true, + Validators: []validator.String{ + validate.NoSeparator(), + }, + }, + "project_id": schema.StringAttribute{ + Description: descriptions["project_id"], + Required: true, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "region": schema.StringAttribute{ + Optional: true, + // must be computed to allow for storing the override value from the provider + Computed: true, + Description: descriptions["region"], + }, + "days": schema.Int32Attribute{ + Required: true, + Description: descriptions["days"], + }, + "mode": schema.StringAttribute{ + Required: true, + Description: descriptions["mode"], + Validators: []validator.String{ + stringvalidator.OneOf(sdkUtils.EnumSliceToStringSlice(defaultretention.AllowedRetentionModeEnumValues)...), + validate.NoSeparator(), + }, + }, + }, + } + +} + +// Metadata implements [datasource.DataSource]. +func (d *defaultRetentionDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_objectstorage_default_retention" +} + +// Read implements [datasource.DataSource]. +func (d *defaultRetentionDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var model model + diags := req.Config.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + ctx = core.InitProviderContext(ctx) + + projectId := model.ProjectId.ValueString() + bucketName := model.BucketName.ValueString() + region := d.providerData.GetRegionWithOverride(model.Region) + + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "bucket_name", bucketName) + ctx = tflog.SetField(ctx, "region", region) + + //Read default-retention + result, err := d.client.DefaultAPI.GetDefaultRetention(ctx, projectId, region, bucketName).Execute() + if err != nil { + var oapiErr *oapierror.GenericOpenAPIError + if errors.As(err, &oapiErr) && oapiErr.StatusCode == http.StatusNotFound { + resp.State.RemoveResource(ctx) + return + } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading default-retention", fmt.Sprintf("Calling API: %v", err)) + return + } + + ctx = core.LogResponse(ctx) + + err = mapFields(result, &model, region) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading default-retention", 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, "ObjectStorage default-retention read") + +} diff --git a/stackit/internal/services/objectstorage/default-retention/resource.go b/stackit/internal/services/objectstorage/default-retention/resource.go new file mode 100644 index 000000000..2a3ecaf67 --- /dev/null +++ b/stackit/internal/services/objectstorage/default-retention/resource.go @@ -0,0 +1,412 @@ +package objectstorage + +import ( + "context" + "errors" + "fmt" + "net/http" + "strings" + + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int32planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/stackitcloud/stackit-sdk-go/core/oapierror" + sdkUtils "github.com/stackitcloud/stackit-sdk-go/core/utils" + defaultretention "github.com/stackitcloud/stackit-sdk-go/services/objectstorage/v2api" + + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + objectstorageUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/objectstorage/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" +) + +var ( + _ resource.Resource = &defaultRetentionResource{} + _ resource.ResourceWithConfigure = &defaultRetentionResource{} + _ resource.ResourceWithImportState = &defaultRetentionResource{} + _ resource.ResourceWithModifyPlan = &defaultRetentionResource{} +) + +type model struct { + Id types.String `tfsdk:"id"` // needed by TF + BucketName types.String `tfsdk:"bucket_name"` + ProjectId types.String `tfsdk:"project_id"` + Region types.String `tfsdk:"region"` + Days types.Int32 `tfsdk:"days"` + Mode types.String `tfsdk:"mode"` +} + +// NewDefaultRetentionResource is a helper function to simplify the provider implementation. +func NewDefaultRetentionResource() resource.Resource { + return &defaultRetentionResource{} +} + +// defaultRetentionResource is the resource implementation. +type defaultRetentionResource struct { + client *defaultretention.APIClient + providerData core.ProviderData +} + +// ModifyPlan implements [resource.ResourceWithModifyPlan]. +func (r *defaultRetentionResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic + var configModel model + // skip initial empty configuration to avoid follow-up errors + if req.Config.Raw.IsNull() { + return + } + resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) + if resp.Diagnostics.HasError() { + return + } + + var planModel model + resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) + if resp.Diagnostics.HasError() { + return + } + + utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) + if resp.Diagnostics.HasError() { + return + } +} + +// ImportState implements [resource.ResourceWithImportState]. +func (r *defaultRetentionResource) 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 default-retention", + fmt.Sprintf("Expected import identifier with format [project_id],[region],[bucketName], got %q", req.ID), + ) + return + } + + ctx = utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{ + "project_id": idParts[0], + "region": idParts[1], + "bucket_name": idParts[2], + }) + tflog.Info(ctx, "ObjectStorage default-retention state imported") + +} + +// Configure implements [resource.ResourceWithConfigure]. +func (r *defaultRetentionResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + var ok bool + r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + if !ok { + return + } + + apiClient := objectstorageUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + r.client = apiClient + tflog.Info(ctx, "ObjectStorage bucket client configured") + +} + +// Schema implements [resource.Resource]. +func (r *defaultRetentionResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + descriptions := map[string]string{ + "main": "ObjectStorage default-retention resource schema. Must have a `region` specified in the provider configuration.", + "id": "Terraform's internal resource identifier. It is structured as \"`project_id`,`region`,`bucket_name`\".", + "bucket_name": "The associated bucket's name. It must be DNS conform.", + "project_id": "STACKIT Project ID to which the default-retention is associated.", + "region": "The resource region. If not defined, the provider region is used.", + "days": "The number retention period in days.", + "mode": "The retention mode for default retention on a bucket.", + } + + 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(), + }, + }, + "bucket_name": schema.StringAttribute{ + Description: descriptions["bucket_name"], + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + stringplanmodifier.UseStateForUnknown(), + }, + Validators: []validator.String{ + validate.NoSeparator(), + }, + }, + "project_id": schema.StringAttribute{ + Description: descriptions["project_id"], + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + stringplanmodifier.UseStateForUnknown(), + }, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "region": schema.StringAttribute{ + Optional: true, + // must be computed to allow for storing the override value from the provider + Computed: true, + Description: descriptions["region"], + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "days": schema.Int32Attribute{ + Required: true, + Description: descriptions["days"], + PlanModifiers: []planmodifier.Int32{ + int32planmodifier.RequiresReplace(), + }, + }, + "mode": schema.StringAttribute{ + Required: true, + Description: descriptions["mode"], + Validators: []validator.String{ + stringvalidator.OneOf(sdkUtils.EnumSliceToStringSlice(defaultretention.AllowedRetentionModeEnumValues)...), + validate.NoSeparator(), + }, + }, + }, + } + +} + +// Metadata implements [resource.Resource]. +func (r *defaultRetentionResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_objectstorage_default_retention" +} + +// Create implements [resource.Resource]. +func (r *defaultRetentionResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var model model + diags := req.Plan.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + ctx = core.InitProviderContext(ctx) + + projectId := model.ProjectId.ValueString() + bucketName := model.BucketName.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) + + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "bucket_name", bucketName) + ctx = tflog.SetField(ctx, "region", region) + + //Create default-retention + apiRequest, err := toSetDefaultRetentionRequest(ctx, r.client.DefaultAPI, &model, projectId, bucketName, region) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error setting default-retention", fmt.Sprintf("Parsing model: %v", err)) + } + result, err := apiRequest.Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error setting default-retention", fmt.Sprintf("Calling API: %v", err)) + return + } + + ctx = core.LogResponse(ctx) + + err = mapFields(result, &model, region) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error setting default-retention", 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, "ObjectStorage default-retention created") +} + +// Delete implements [resource.Resource]. +func (r *defaultRetentionResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var model model + diags := req.State.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + ctx = core.InitProviderContext(ctx) + + projectId := model.ProjectId.ValueString() + bucketName := model.BucketName.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) + + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "bucket_name", bucketName) + ctx = tflog.SetField(ctx, "region", region) + + //Delete default-retention + _, err := r.client.DefaultAPI.DeleteDefaultRetention(ctx, projectId, region, bucketName).Execute() + if err != nil { + if oapiErr, ok := errors.AsType[*oapierror.GenericOpenAPIError](err); ok { + if oapiErr.StatusCode == http.StatusNotFound { + resp.State.RemoveResource(ctx) + return + } + if oapiErr.StatusCode == http.StatusConflict { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting default-retention", "Encountered conflict") + return + } + } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting default-retention", fmt.Sprintf("Calling API: %v", err)) + return + } + + ctx = core.LogResponse(ctx) + + tflog.Info(ctx, "ObjectStorage default-retention deleted") + +} + +// Read implements [resource.Resource]. +func (r *defaultRetentionResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var model model + diags := req.State.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + ctx = core.InitProviderContext(ctx) + + projectId := model.ProjectId.ValueString() + bucketName := model.BucketName.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) + + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "bucket_name", bucketName) + ctx = tflog.SetField(ctx, "region", region) + + //Read default-retention + result, err := r.client.DefaultAPI.GetDefaultRetention(ctx, projectId, region, bucketName).Execute() + if err != nil { + var oapiErr *oapierror.GenericOpenAPIError + if errors.As(err, &oapiErr) && oapiErr.StatusCode == http.StatusNotFound { + resp.State.RemoveResource(ctx) + return + } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading default-retention", fmt.Sprintf("Calling API: %v", err)) + return + } + + ctx = core.LogResponse(ctx) + + err = mapFields(result, &model, region) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading default-retention", 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, "ObjectStorage default-retention read") + +} + +// Update implements [resource.Resource]. +func (r *defaultRetentionResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var model model + diags := req.Plan.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + ctx = core.InitProviderContext(ctx) + + projectId := model.ProjectId.ValueString() + bucketName := model.BucketName.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) + + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "bucket_name", bucketName) + ctx = tflog.SetField(ctx, "region", region) + + //Update default-retention + apiRequest, err := toSetDefaultRetentionRequest(ctx, r.client.DefaultAPI, &model, projectId, bucketName, region) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error setting default-retention", fmt.Sprintf("Parsing model: %v", err)) + } + result, err := apiRequest.Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error setting default-retention", fmt.Sprintf("Calling API: %v", err)) + return + } + + ctx = core.LogResponse(ctx) + + err = mapFields(result, &model, region) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error setting default-retention", 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, "ObjectStorage default-retention set") + +} + +func toSetDefaultRetentionRequest(ctx context.Context, client defaultretention.DefaultAPI, m *model, projectId, bucketName, region string) (defaultretention.ApiSetDefaultRetentionRequest, error) { + days := m.Days.ValueInt32() + stringMode := m.Mode.ValueString() + mode, err := defaultretention.NewRetentionModeFromValue(stringMode) + if err != nil { + return defaultretention.ApiSetDefaultRetentionRequest{}, fmt.Errorf("Could not parse provided retention mode to enum: %w", err) + } + apiRequest := client.SetDefaultRetention(ctx, projectId, region, bucketName).SetDefaultRetentionPayload(defaultretention.SetDefaultRetentionPayload{ + Days: days, + Mode: *mode, + }) + return apiRequest, nil +} + +func mapFields(res *defaultretention.DefaultRetentionResponse, m *model, region string) error { + if res == nil { + return fmt.Errorf("response input is nil") + } + if m == nil { + return fmt.Errorf("model input is nil") + } + + m.BucketName = types.StringValue(res.Bucket) + m.Id = utils.BuildInternalTerraformId(m.ProjectId.ValueString(), region, m.BucketName.ValueString()) + m.Region = types.StringValue(region) + m.Days = types.Int32Value(res.Days) + m.Mode = types.StringValue(string(res.Mode)) + return nil +} diff --git a/stackit/internal/services/objectstorage/default-retention/resource_test.go b/stackit/internal/services/objectstorage/default-retention/resource_test.go new file mode 100644 index 000000000..cd3608bf3 --- /dev/null +++ b/stackit/internal/services/objectstorage/default-retention/resource_test.go @@ -0,0 +1,84 @@ +package objectstorage + +import ( + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/types" + defaultretention "github.com/stackitcloud/stackit-sdk-go/services/objectstorage/v2api" +) + +type mockSettings struct { + returnError bool +} + +func newAPIMock(settings *mockSettings) defaultretention.DefaultAPI { + return &defaultretention.DefaultAPIServiceMock{ + SetDefaultRetentionExecuteMock: new(func(r defaultretention.ApiSetDefaultRetentionRequest) (*defaultretention.DefaultRetentionResponse, error) { + if settings.returnError { + return nil, fmt.Errorf("set default retention failed") + } + return &defaultretention.DefaultRetentionResponse{}, nil + }), + } +} + +func TestMapFields(t *testing.T) { + const testRegion = "eu01" + const testProjectId = "97bed312-5705-4246-9621-03e3a06af0af" + const testBucketName = "bucket1" + id := fmt.Sprintf("%s,%s,%s", testProjectId, testRegion, testBucketName) + tests := []struct { + description string + input *defaultretention.DefaultRetentionResponse + expected model + isValid bool + }{ + { + "simple_values", + &defaultretention.DefaultRetentionResponse{ + Days: 2, + Bucket: testBucketName, + Mode: defaultretention.RETENTIONMODE_COMPLIANCE, + Project: testProjectId, + }, + model{ + Id: types.StringValue(id), + Days: types.Int32Value(2), + BucketName: types.StringValue(testBucketName), + Mode: types.StringValue(string(defaultretention.RETENTIONMODE_COMPLIANCE)), + ProjectId: types.StringValue(testProjectId), + Region: types.StringValue(testRegion), + }, + true, + }, + { + "nil_response", + nil, + model{}, + false, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + + model := &model{ + ProjectId: tt.expected.ProjectId, + } + err := mapFields(tt.input, model, "eu01") + 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(model, &tt.expected) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + } + }) + } +} diff --git a/stackit/internal/services/objectstorage/objectstorage_acc_test.go b/stackit/internal/services/objectstorage/objectstorage_acc_test.go index 98896e0c8..e28d18d86 100644 --- a/stackit/internal/services/objectstorage/objectstorage_acc_test.go +++ b/stackit/internal/services/objectstorage/objectstorage_acc_test.go @@ -32,6 +32,9 @@ var testConfigVarsMin = config.Variables{ "objectstorage_bucket_name_with_lock": config.StringVariable(fmt.Sprintf("tf-acc-test-%s", acctest.RandStringFromCharSet(20, acctest.CharSetAlpha))), "object_lock": config.BoolVariable(true), + + "retention_days": config.IntegerVariable(2), + "retention_mode": config.StringVariable(string(objectstorage.RETENTIONMODE_GOVERNANCE)), } func TestAccObjectStorageResourceMin(t *testing.T) { @@ -96,6 +99,12 @@ func TestAccObjectStorageResourceMin(t *testing.T) { resource.TestCheckResourceAttrSet("stackit_objectstorage_bucket.bucket_object_lock", "url_path_style"), resource.TestCheckResourceAttrSet("stackit_objectstorage_bucket.bucket_object_lock", "url_virtual_hosted_style"), resource.TestCheckResourceAttr("stackit_objectstorage_bucket.bucket_object_lock", "object_lock", testutil.ConvertConfigVariable(testConfigVarsMin["object_lock"])), + + // default retention of object storage + resource.TestCheckResourceAttr("stackit_objectstorage_default_retention.retention", "bucket_name", testutil.ConvertConfigVariable(testConfigVarsMin["objectstorage_bucket_name_with_lock"])), + resource.TestCheckResourceAttr("stackit_objectstorage_default_retention.retention", "project_id", testutil.ConvertConfigVariable(testConfigVarsMin["project_id"])), + resource.TestCheckResourceAttr("stackit_objectstorage_default_retention.retention", "days", testutil.ConvertConfigVariable(testConfigVarsMin["retention_days"])), + resource.TestCheckResourceAttr("stackit_objectstorage_default_retention.retention", "mode", testutil.ConvertConfigVariable(testConfigVarsMin["retention_mode"])), ), }, // Data source @@ -131,7 +140,15 @@ func TestAccObjectStorageResourceMin(t *testing.T) { data "stackit_objectstorage_bucket" "bucket_object_lock" { project_id = stackit_objectstorage_bucket.bucket_object_lock.project_id name = stackit_objectstorage_bucket.bucket_object_lock.name - }`, + } + + data "stackit_objectstorage_default_retention" "retention" { + bucket_name = stackit_objectstorage_bucket.bucket_object_lock.name + project_id = var.project_id + days = var.retention_days + mode = var.retention_mode + } + `, testutil.NewConfigBuilder().BuildProviderConfig()+resourceMinConfig, ), Check: resource.ComposeAggregateTestCheckFunc( @@ -235,6 +252,24 @@ func TestAccObjectStorageResourceMin(t *testing.T) { "stackit_objectstorage_bucket.bucket_object_lock", "object_lock", "data.stackit_objectstorage_bucket.bucket_object_lock", "object_lock", ), + + // default retention of object storage + resource.TestCheckResourceAttrPair( + "stackit_objectstorage_default_retention.retention", "bucket_name", + "data.stackit_objectstorage_default_retention.retention", "bucket_name", + ), + resource.TestCheckResourceAttrPair( + "stackit_objectstorage_default_retention.retention", "project_id", + "data.stackit_objectstorage_default_retention.retention", "project_id", + ), + resource.TestCheckResourceAttrPair( + "stackit_objectstorage_default_retention.retention", "days", + "data.stackit_objectstorage_default_retention.retention", "days", + ), + resource.TestCheckResourceAttrPair( + "stackit_objectstorage_default_retention.retention", "mode", + "data.stackit_objectstorage_default_retention.retention", "mode", + ), ), }, // Import diff --git a/stackit/internal/services/objectstorage/testfiles/resource-min.tf b/stackit/internal/services/objectstorage/testfiles/resource-min.tf index cecb80b99..af7d7f92f 100644 --- a/stackit/internal/services/objectstorage/testfiles/resource-min.tf +++ b/stackit/internal/services/objectstorage/testfiles/resource-min.tf @@ -7,6 +7,9 @@ variable "expiration_timestamp" {} variable "objectstorage_bucket_name_with_lock" {} variable "object_lock" {} +variable "retention_days" {} +variable "retention_mode" {} + resource "stackit_objectstorage_bucket" "bucket" { project_id = var.project_id name = var.objectstorage_bucket_name @@ -37,4 +40,12 @@ resource "stackit_objectstorage_bucket" "bucket_object_lock" { project_id = var.project_id name = var.objectstorage_bucket_name_with_lock object_lock = var.object_lock -} \ No newline at end of file +} + +resource "stackit_objectstorage_default_retention" "retention" { + bucket_name = stackit_objectstorage_bucket.bucket_object_lock.name + project_id = stackit_objectstorage_bucket.bucket_object_lock.project_id + days = var.retention_days + mode = var.retention_mode +} + diff --git a/stackit/provider.go b/stackit/provider.go index c3a086c41..ad5881ea0 100644 --- a/stackit/provider.go +++ b/stackit/provider.go @@ -83,6 +83,7 @@ import ( compliancelock "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/objectstorage/compliance-lock" objecStorageCredential "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/objectstorage/credential" objecStorageCredentialsGroup "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/objectstorage/credentialsgroup" + objectstorageDefaultRetention "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/objectstorage/default-retention" alertGroup "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/observability/alertgroup" observabilityCredential "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/observability/credential" observabilityInstance "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/observability/instance" @@ -715,6 +716,7 @@ func (p *Provider) DataSources(_ context.Context) []func() datasource.DataSource objectStorageBucket.NewBucketDataSource, objecStorageCredentialsGroup.NewCredentialsGroupDataSource, objecStorageCredential.NewCredentialDataSource, + objectstorageDefaultRetention.NewDefaultRetentionDataSource, observabilityInstance.NewInstanceDataSource, observabilityScrapeConfig.NewScrapeConfigDataSource, openSearchInstance.NewInstanceDataSource, @@ -822,6 +824,7 @@ func (p *Provider) Resources(_ context.Context) []func() resource.Resource { objectStorageBucket.NewBucketResource, objecStorageCredentialsGroup.NewCredentialsGroupResource, objecStorageCredential.NewCredentialResource, + objectstorageDefaultRetention.NewDefaultRetentionResource, observabilityCredential.NewCredentialResource, observabilityInstance.NewInstanceResource, observabilityScrapeConfig.NewScrapeConfigResource, From cd918793b60d9d3ccd39e6e30ce426af3a411f9f Mon Sep 17 00:00:00 2001 From: Jonas Schlecht Date: Wed, 1 Jul 2026 09:22:12 +0200 Subject: [PATCH 2/2] refactor(objectstorage): linter changes Relates to STACKITTPR-727 --- .../default-retention/datasource.go | 13 +++--- .../default-retention/resource.go | 42 ++++++++----------- .../default-retention/resource_test.go | 16 ------- 3 files changed, 23 insertions(+), 48 deletions(-) diff --git a/stackit/internal/services/objectstorage/default-retention/datasource.go b/stackit/internal/services/objectstorage/default-retention/datasource.go index 2e2a06b38..a58fc5ec0 100644 --- a/stackit/internal/services/objectstorage/default-retention/datasource.go +++ b/stackit/internal/services/objectstorage/default-retention/datasource.go @@ -13,7 +13,6 @@ import ( "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/stackit-sdk-go/core/oapierror" sdkUtils "github.com/stackitcloud/stackit-sdk-go/core/utils" - defaultretention "github.com/stackitcloud/stackit-sdk-go/services/objectstorage/v2api" objectstorage "github.com/stackitcloud/stackit-sdk-go/services/objectstorage/v2api" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" @@ -52,7 +51,7 @@ func (r *defaultRetentionDataSource) Configure(ctx context.Context, req datasour } // Schema implements [datasource.DataSource]. -func (d *defaultRetentionDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { +func (d *defaultRetentionDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { // nolint:gocritic descriptions := map[string]string{ "main": "ObjectStorage default-retention resource schema. Must have a `region` specified in the provider configuration.", "id": "Terraform's internal resource identifier. It is structured as \"`project_id`,`region`,`bucket_name`\".", @@ -99,22 +98,21 @@ func (d *defaultRetentionDataSource) Schema(ctx context.Context, req datasource. Required: true, Description: descriptions["mode"], Validators: []validator.String{ - stringvalidator.OneOf(sdkUtils.EnumSliceToStringSlice(defaultretention.AllowedRetentionModeEnumValues)...), + stringvalidator.OneOf(sdkUtils.EnumSliceToStringSlice(objectstorage.AllowedRetentionModeEnumValues)...), validate.NoSeparator(), }, }, }, } - } // Metadata implements [datasource.DataSource]. -func (d *defaultRetentionDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { +func (d *defaultRetentionDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { resp.TypeName = req.ProviderTypeName + "_objectstorage_default_retention" } // Read implements [datasource.DataSource]. -func (d *defaultRetentionDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { +func (d *defaultRetentionDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic var model model diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) @@ -131,7 +129,7 @@ func (d *defaultRetentionDataSource) Read(ctx context.Context, req datasource.Re ctx = tflog.SetField(ctx, "bucket_name", bucketName) ctx = tflog.SetField(ctx, "region", region) - //Read default-retention + // Read default-retention result, err := d.client.DefaultAPI.GetDefaultRetention(ctx, projectId, region, bucketName).Execute() if err != nil { var oapiErr *oapierror.GenericOpenAPIError @@ -158,5 +156,4 @@ func (d *defaultRetentionDataSource) Read(ctx context.Context, req datasource.Re return } tflog.Info(ctx, "ObjectStorage default-retention read") - } diff --git a/stackit/internal/services/objectstorage/default-retention/resource.go b/stackit/internal/services/objectstorage/default-retention/resource.go index 2a3ecaf67..1d0565f1c 100644 --- a/stackit/internal/services/objectstorage/default-retention/resource.go +++ b/stackit/internal/services/objectstorage/default-retention/resource.go @@ -18,7 +18,7 @@ import ( "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/stackit-sdk-go/core/oapierror" sdkUtils "github.com/stackitcloud/stackit-sdk-go/core/utils" - defaultretention "github.com/stackitcloud/stackit-sdk-go/services/objectstorage/v2api" + objectstorage "github.com/stackitcloud/stackit-sdk-go/services/objectstorage/v2api" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" @@ -50,7 +50,7 @@ func NewDefaultRetentionResource() resource.Resource { // defaultRetentionResource is the resource implementation. type defaultRetentionResource struct { - client *defaultretention.APIClient + client *objectstorage.APIClient providerData core.ProviderData } @@ -100,7 +100,6 @@ func (r *defaultRetentionResource) ImportState(ctx context.Context, req resource "bucket_name": idParts[2], }) tflog.Info(ctx, "ObjectStorage default-retention state imported") - } // Configure implements [resource.ResourceWithConfigure]. @@ -117,11 +116,10 @@ func (r *defaultRetentionResource) Configure(ctx context.Context, req resource.C } r.client = apiClient tflog.Info(ctx, "ObjectStorage bucket client configured") - } // Schema implements [resource.Resource]. -func (r *defaultRetentionResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { +func (r *defaultRetentionResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { descriptions := map[string]string{ "main": "ObjectStorage default-retention resource schema. Must have a `region` specified in the provider configuration.", "id": "Terraform's internal resource identifier. It is structured as \"`project_id`,`region`,`bucket_name`\".", @@ -185,22 +183,21 @@ func (r *defaultRetentionResource) Schema(ctx context.Context, req resource.Sche Required: true, Description: descriptions["mode"], Validators: []validator.String{ - stringvalidator.OneOf(sdkUtils.EnumSliceToStringSlice(defaultretention.AllowedRetentionModeEnumValues)...), + stringvalidator.OneOf(sdkUtils.EnumSliceToStringSlice(objectstorage.AllowedRetentionModeEnumValues)...), validate.NoSeparator(), }, }, }, } - } // Metadata implements [resource.Resource]. -func (r *defaultRetentionResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { +func (r *defaultRetentionResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = req.ProviderTypeName + "_objectstorage_default_retention" } // Create implements [resource.Resource]. -func (r *defaultRetentionResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { +func (r *defaultRetentionResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic var model model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) @@ -217,7 +214,7 @@ func (r *defaultRetentionResource) Create(ctx context.Context, req resource.Crea ctx = tflog.SetField(ctx, "bucket_name", bucketName) ctx = tflog.SetField(ctx, "region", region) - //Create default-retention + // Create default-retention apiRequest, err := toSetDefaultRetentionRequest(ctx, r.client.DefaultAPI, &model, projectId, bucketName, region) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error setting default-retention", fmt.Sprintf("Parsing model: %v", err)) @@ -246,7 +243,7 @@ func (r *defaultRetentionResource) Create(ctx context.Context, req resource.Crea } // Delete implements [resource.Resource]. -func (r *defaultRetentionResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { +func (r *defaultRetentionResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic var model model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) @@ -263,7 +260,7 @@ func (r *defaultRetentionResource) Delete(ctx context.Context, req resource.Dele ctx = tflog.SetField(ctx, "bucket_name", bucketName) ctx = tflog.SetField(ctx, "region", region) - //Delete default-retention + // Delete default-retention _, err := r.client.DefaultAPI.DeleteDefaultRetention(ctx, projectId, region, bucketName).Execute() if err != nil { if oapiErr, ok := errors.AsType[*oapierror.GenericOpenAPIError](err); ok { @@ -283,11 +280,10 @@ func (r *defaultRetentionResource) Delete(ctx context.Context, req resource.Dele ctx = core.LogResponse(ctx) tflog.Info(ctx, "ObjectStorage default-retention deleted") - } // Read implements [resource.Resource]. -func (r *defaultRetentionResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { +func (r *defaultRetentionResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic var model model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) @@ -304,7 +300,7 @@ func (r *defaultRetentionResource) Read(ctx context.Context, req resource.ReadRe ctx = tflog.SetField(ctx, "bucket_name", bucketName) ctx = tflog.SetField(ctx, "region", region) - //Read default-retention + // Read default-retention result, err := r.client.DefaultAPI.GetDefaultRetention(ctx, projectId, region, bucketName).Execute() if err != nil { var oapiErr *oapierror.GenericOpenAPIError @@ -331,11 +327,10 @@ func (r *defaultRetentionResource) Read(ctx context.Context, req resource.ReadRe return } tflog.Info(ctx, "ObjectStorage default-retention read") - } // Update implements [resource.Resource]. -func (r *defaultRetentionResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { +func (r *defaultRetentionResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic var model model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) @@ -352,7 +347,7 @@ func (r *defaultRetentionResource) Update(ctx context.Context, req resource.Upda ctx = tflog.SetField(ctx, "bucket_name", bucketName) ctx = tflog.SetField(ctx, "region", region) - //Update default-retention + // Update default-retention apiRequest, err := toSetDefaultRetentionRequest(ctx, r.client.DefaultAPI, &model, projectId, bucketName, region) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error setting default-retention", fmt.Sprintf("Parsing model: %v", err)) @@ -378,24 +373,23 @@ func (r *defaultRetentionResource) Update(ctx context.Context, req resource.Upda return } tflog.Info(ctx, "ObjectStorage default-retention set") - } -func toSetDefaultRetentionRequest(ctx context.Context, client defaultretention.DefaultAPI, m *model, projectId, bucketName, region string) (defaultretention.ApiSetDefaultRetentionRequest, error) { +func toSetDefaultRetentionRequest(ctx context.Context, client objectstorage.DefaultAPI, m *model, projectId, bucketName, region string) (objectstorage.ApiSetDefaultRetentionRequest, error) { days := m.Days.ValueInt32() stringMode := m.Mode.ValueString() - mode, err := defaultretention.NewRetentionModeFromValue(stringMode) + mode, err := objectstorage.NewRetentionModeFromValue(stringMode) if err != nil { - return defaultretention.ApiSetDefaultRetentionRequest{}, fmt.Errorf("Could not parse provided retention mode to enum: %w", err) + return objectstorage.ApiSetDefaultRetentionRequest{}, fmt.Errorf("could not parse provided retention mode to enum: %w", err) } - apiRequest := client.SetDefaultRetention(ctx, projectId, region, bucketName).SetDefaultRetentionPayload(defaultretention.SetDefaultRetentionPayload{ + apiRequest := client.SetDefaultRetention(ctx, projectId, region, bucketName).SetDefaultRetentionPayload(objectstorage.SetDefaultRetentionPayload{ Days: days, Mode: *mode, }) return apiRequest, nil } -func mapFields(res *defaultretention.DefaultRetentionResponse, m *model, region string) error { +func mapFields(res *objectstorage.DefaultRetentionResponse, m *model, region string) error { if res == nil { return fmt.Errorf("response input is nil") } diff --git a/stackit/internal/services/objectstorage/default-retention/resource_test.go b/stackit/internal/services/objectstorage/default-retention/resource_test.go index cd3608bf3..075c9c3cc 100644 --- a/stackit/internal/services/objectstorage/default-retention/resource_test.go +++ b/stackit/internal/services/objectstorage/default-retention/resource_test.go @@ -9,21 +9,6 @@ import ( defaultretention "github.com/stackitcloud/stackit-sdk-go/services/objectstorage/v2api" ) -type mockSettings struct { - returnError bool -} - -func newAPIMock(settings *mockSettings) defaultretention.DefaultAPI { - return &defaultretention.DefaultAPIServiceMock{ - SetDefaultRetentionExecuteMock: new(func(r defaultretention.ApiSetDefaultRetentionRequest) (*defaultretention.DefaultRetentionResponse, error) { - if settings.returnError { - return nil, fmt.Errorf("set default retention failed") - } - return &defaultretention.DefaultRetentionResponse{}, nil - }), - } -} - func TestMapFields(t *testing.T) { const testRegion = "eu01" const testProjectId = "97bed312-5705-4246-9621-03e3a06af0af" @@ -62,7 +47,6 @@ func TestMapFields(t *testing.T) { } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - model := &model{ ProjectId: tt.expected.ProjectId, }