diff --git a/docs/data-sources/server_update_schedule.md b/docs/data-sources/server_update_schedule.md index 00b678db..62a10db0 100644 --- a/docs/data-sources/server_update_schedule.md +++ b/docs/data-sources/server_update_schedule.md @@ -32,10 +32,14 @@ data "stackit_server_update_schedule" "example" { - `server_id` (String) Server ID for the update schedule. - `update_schedule_id` (Number) Update schedule ID. +### Optional + +- `region` (String) The resource region. If not defined, the provider region is used. + ### Read-Only - `enabled` (Boolean) Is the update schedule enabled or disabled. -- `id` (String) Terraform's internal resource identifier. It is structured as "`project_id`,`server_id`,`update_schedule_id`". +- `id` (String) Terraform's internal resource identifier. It is structured as "`project_id`,`region`,`server_id`,`update_schedule_id`". - `maintenance_window` (Number) Maintenance window [1..24]. - `name` (String) The schedule name. - `rrule` (String) Update schedule described in `rrule` (recurrence rule) format. diff --git a/docs/data-sources/server_update_schedules.md b/docs/data-sources/server_update_schedules.md index be485026..cf34faae 100644 --- a/docs/data-sources/server_update_schedules.md +++ b/docs/data-sources/server_update_schedules.md @@ -30,9 +30,13 @@ data "stackit_server_update_schedules" "example" { - `project_id` (String) STACKIT Project ID (UUID) to which the server is associated. - `server_id` (String) Server ID (UUID) to which the update schedule is associated. +### Optional + +- `region` (String) The resource region. If not defined, the provider region is used. + ### Read-Only -- `id` (String) Terraform's internal data source identifier. It is structured as "`project_id`,`server_id`". +- `id` (String) Terraform's internal data source identifier. It is structured as "`project_id`,`region`,`server_id`". - `items` (Attributes List) (see [below for nested schema](#nestedatt--items)) diff --git a/docs/resources/server_update_schedule.md b/docs/resources/server_update_schedule.md index e824ab01..8b4fa218 100644 --- a/docs/resources/server_update_schedule.md +++ b/docs/resources/server_update_schedule.md @@ -38,7 +38,11 @@ resource "stackit_server_update_schedule" "example" { - `rrule` (String) Update schedule described in `rrule` (recurrence rule) format. - `server_id` (String) Server ID for the update schedule. +### 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`,`server_id`,`update_schedule_id`". +- `id` (String) Terraform's internal resource identifier. It is structured as "`project_id`,`region`,`server_id`,`update_schedule_id`". - `update_schedule_id` (Number) Update schedule ID. diff --git a/go.mod b/go.mod index d4d787c4..a4013eaa 100644 --- a/go.mod +++ b/go.mod @@ -29,7 +29,7 @@ require ( github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.13.0 github.com/stackitcloud/stackit-sdk-go/services/secretsmanager v0.11.0 github.com/stackitcloud/stackit-sdk-go/services/serverbackup v1.0.1 - github.com/stackitcloud/stackit-sdk-go/services/serverupdate v0.5.0 + github.com/stackitcloud/stackit-sdk-go/services/serverupdate v1.0.0 github.com/stackitcloud/stackit-sdk-go/services/serviceaccount v0.6.0 github.com/stackitcloud/stackit-sdk-go/services/serviceenablement v1.0.0 github.com/stackitcloud/stackit-sdk-go/services/ske v0.22.0 diff --git a/go.sum b/go.sum index 2d8d559b..5e27742d 100644 --- a/go.sum +++ b/go.sum @@ -188,8 +188,8 @@ github.com/stackitcloud/stackit-sdk-go/services/secretsmanager v0.11.0 h1:PwfpDF github.com/stackitcloud/stackit-sdk-go/services/secretsmanager v0.11.0/go.mod h1:Hb21FmYP95q0fzOb9jk4/9CIxTsHzrSYDQZh6e82XUg= github.com/stackitcloud/stackit-sdk-go/services/serverbackup v1.0.1 h1:qujhShugc1290NQlPoNqsembqzot8aTToAdSsJg5WrM= github.com/stackitcloud/stackit-sdk-go/services/serverbackup v1.0.1/go.mod h1:e1fsQL24gTPXcMWptuslNscawmXv/PLUAFuw+sOofbc= -github.com/stackitcloud/stackit-sdk-go/services/serverupdate v0.5.0 h1:TMUxDh8XGgWUpnWo7GsawVq2ICDsy/r8dMlfC26MR5g= -github.com/stackitcloud/stackit-sdk-go/services/serverupdate v0.5.0/go.mod h1:giHnHz3kHeLY8Av9MZLsyJlaTXYz+BuGqdP/SKB5Vo0= +github.com/stackitcloud/stackit-sdk-go/services/serverupdate v1.0.0 h1:a8logPoRcMCgwa9rCtuzWF6DLiuCIdJgcacZKThFsks= +github.com/stackitcloud/stackit-sdk-go/services/serverupdate v1.0.0/go.mod h1:zDdYYQVHGlju9cnMISX/Ty73Yh/qYcZGcJSOYWRZCbw= github.com/stackitcloud/stackit-sdk-go/services/serviceaccount v0.6.0 h1:y+XzJcntHJ7M+IWWvAUkiVFA8op+jZxwHs3ktW2aLoA= github.com/stackitcloud/stackit-sdk-go/services/serviceaccount v0.6.0/go.mod h1:J/Wa67cbDI1wAyxib9PiEbNqGfIoFUH+DSLueVazQx8= github.com/stackitcloud/stackit-sdk-go/services/serviceenablement v1.0.0 h1:Xxd5KUSWRt7FytnNWClLEa0n9GM6e5xAKo835ODSpAM= diff --git a/stackit/internal/services/serverupdate/schedule/resource.go b/stackit/internal/services/serverupdate/schedule/resource.go index e62a8467..43bec451 100644 --- a/stackit/internal/services/serverupdate/schedule/resource.go +++ b/stackit/internal/services/serverupdate/schedule/resource.go @@ -18,6 +18,7 @@ import ( "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/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" @@ -39,6 +40,7 @@ var ( _ resource.Resource = &scheduleResource{} _ resource.ResourceWithConfigure = &scheduleResource{} _ resource.ResourceWithImportState = &scheduleResource{} + _ resource.ResourceWithModifyPlan = &scheduleResource{} ) type Model struct { @@ -50,6 +52,7 @@ type Model struct { Rrule types.String `tfsdk:"rrule"` Enabled types.Bool `tfsdk:"enabled"` MaintenanceWindow types.Int64 `tfsdk:"maintenance_window"` + Region types.String `tfsdk:"region"` } // NewScheduleResource is a helper function to simplify the provider implementation. @@ -59,7 +62,38 @@ func NewScheduleResource() resource.Resource { // scheduleResource is the resource implementation. type scheduleResource struct { - client *serverupdate.APIClient + client *serverupdate.APIClient + providerData core.ProviderData +} + +// ModifyPlan implements resource.ResourceWithModifyPlan. +// Use the modifier to set the effective region in the current plan. +func (r *scheduleResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform + 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 + } } // Metadata returns the resource type name. @@ -74,14 +108,15 @@ func (r *scheduleResource) Configure(ctx context.Context, req resource.Configure return } - providerData, ok := req.ProviderData.(core.ProviderData) + var ok bool + r.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 } if !resourceBetaCheckDone { - features.CheckBetaResourcesEnabled(ctx, &providerData, &resp.Diagnostics, "stackit_server_update_schedule", "resource") + features.CheckBetaResourcesEnabled(ctx, &r.providerData, &resp.Diagnostics, "stackit_server_update_schedule", "resource") if resp.Diagnostics.HasError() { return } @@ -90,16 +125,15 @@ func (r *scheduleResource) Configure(ctx context.Context, req resource.Configure var apiClient *serverupdate.APIClient var err error - if providerData.ServerUpdateCustomEndpoint != "" { - ctx = tflog.SetField(ctx, "server_update_custom_endpoint", providerData.ServerUpdateCustomEndpoint) + if r.providerData.ServerUpdateCustomEndpoint != "" { + ctx = tflog.SetField(ctx, "server_update_custom_endpoint", r.providerData.ServerUpdateCustomEndpoint) apiClient, err = serverupdate.NewAPIClient( - config.WithCustomAuth(providerData.RoundTripper), - config.WithEndpoint(providerData.ServerUpdateCustomEndpoint), + config.WithCustomAuth(r.providerData.RoundTripper), + config.WithEndpoint(r.providerData.ServerUpdateCustomEndpoint), ) } else { apiClient, err = serverupdate.NewAPIClient( - config.WithCustomAuth(providerData.RoundTripper), - config.WithRegion(providerData.GetRegion()), + config.WithCustomAuth(r.providerData.RoundTripper), ) } @@ -119,7 +153,7 @@ func (r *scheduleResource) Schema(_ context.Context, _ resource.SchemaRequest, r MarkdownDescription: features.AddBetaDescription("Server update schedule resource schema. Must have a `region` specified in the provider configuration."), Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ - Description: "Terraform's internal resource identifier. It is structured as \"`project_id`,`server_id`,`update_schedule_id`\".", + Description: "Terraform's internal resource identifier. It is structured as \"`project_id`,`region`,`server_id`,`update_schedule_id`\".", Computed: true, PlanModifiers: []planmodifier.String{ stringplanmodifier.UseStateForUnknown(), @@ -194,6 +228,15 @@ func (r *scheduleResource) Schema(_ context.Context, _ resource.SchemaRequest, r int64validator.AtMost(24), }, }, + "region": schema.StringAttribute{ + Optional: true, + // must be computed to allow for storing the override value from the provider + Computed: true, + Description: "The resource region. If not defined, the provider region is used.", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, }, } } @@ -208,11 +251,13 @@ func (r *scheduleResource) Create(ctx context.Context, req resource.CreateReques } projectId := model.ProjectId.ValueString() serverId := model.ServerId.ValueString() + region := model.Region.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) ctx = tflog.SetField(ctx, "server_id", serverId) + ctx = tflog.SetField(ctx, "region", region) // Enable updates if not already enabled - err := enableUpdatesService(ctx, &model, r.client) + err := enableUpdatesService(ctx, &model, r.client, region) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating server update schedule", fmt.Sprintf("Enabling server update project before creation: %v", err)) return @@ -224,7 +269,7 @@ func (r *scheduleResource) Create(ctx context.Context, req resource.CreateReques core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating server update schedule", fmt.Sprintf("Creating API payload: %v", err)) return } - scheduleResp, err := r.client.CreateUpdateSchedule(ctx, projectId, serverId).CreateUpdateSchedulePayload(*payload).Execute() + scheduleResp, err := r.client.CreateUpdateSchedule(ctx, projectId, serverId, region).CreateUpdateSchedulePayload(*payload).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating server update schedule", fmt.Sprintf("Calling API: %v", err)) return @@ -232,7 +277,7 @@ func (r *scheduleResource) Create(ctx context.Context, req resource.CreateReques ctx = tflog.SetField(ctx, "update_schedule_id", *scheduleResp.Id) // Map response body to schema - err = mapFields(scheduleResp, &model) + err = mapFields(scheduleResp, &model, region) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating server update schedule", fmt.Sprintf("Processing API payload: %v", err)) return @@ -256,11 +301,16 @@ func (r *scheduleResource) Read(ctx context.Context, req resource.ReadRequest, r projectId := model.ProjectId.ValueString() serverId := model.ServerId.ValueString() updateScheduleId := model.UpdateScheduleId.ValueInt64() + region := model.Region.ValueString() + if region == "" { + region = r.providerData.GetRegion() + } ctx = tflog.SetField(ctx, "project_id", projectId) ctx = tflog.SetField(ctx, "server_id", serverId) + ctx = tflog.SetField(ctx, "region", region) ctx = tflog.SetField(ctx, "update_schedule_id", updateScheduleId) - scheduleResp, err := r.client.GetUpdateSchedule(ctx, projectId, serverId, strconv.FormatInt(updateScheduleId, 10)).Execute() + scheduleResp, err := r.client.GetUpdateSchedule(ctx, projectId, serverId, strconv.FormatInt(updateScheduleId, 10), region).Execute() if err != nil { oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped if ok && oapiErr.StatusCode == http.StatusNotFound { @@ -272,7 +322,7 @@ func (r *scheduleResource) Read(ctx context.Context, req resource.ReadRequest, r } // Map response body to schema - err = mapFields(scheduleResp, &model) + err = mapFields(scheduleResp, &model, region) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading update schedule", fmt.Sprintf("Processing API payload: %v", err)) return @@ -298,8 +348,10 @@ func (r *scheduleResource) Update(ctx context.Context, req resource.UpdateReques projectId := model.ProjectId.ValueString() serverId := model.ServerId.ValueString() updateScheduleId := model.UpdateScheduleId.ValueInt64() + region := model.Region.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) ctx = tflog.SetField(ctx, "server_id", serverId) + ctx = tflog.SetField(ctx, "region", region) ctx = tflog.SetField(ctx, "update_schedule_id", updateScheduleId) // Update schedule @@ -309,14 +361,14 @@ func (r *scheduleResource) Update(ctx context.Context, req resource.UpdateReques return } - scheduleResp, err := r.client.UpdateUpdateSchedule(ctx, projectId, serverId, strconv.FormatInt(updateScheduleId, 10)).UpdateUpdateSchedulePayload(*payload).Execute() + scheduleResp, err := r.client.UpdateUpdateSchedule(ctx, projectId, serverId, strconv.FormatInt(updateScheduleId, 10), region).UpdateUpdateSchedulePayload(*payload).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating server update schedule", fmt.Sprintf("Calling API: %v", err)) return } // Map response body to schema - err = mapFields(scheduleResp, &model) + err = mapFields(scheduleResp, &model, region) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating server update schedule", fmt.Sprintf("Processing API payload: %v", err)) return @@ -340,11 +392,13 @@ func (r *scheduleResource) Delete(ctx context.Context, req resource.DeleteReques projectId := model.ProjectId.ValueString() serverId := model.ServerId.ValueString() updateScheduleId := model.UpdateScheduleId.ValueInt64() + region := model.Region.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) ctx = tflog.SetField(ctx, "server_id", serverId) + ctx = tflog.SetField(ctx, "region", region) ctx = tflog.SetField(ctx, "update_schedule_id", updateScheduleId) - err := r.client.DeleteUpdateSchedule(ctx, projectId, serverId, strconv.FormatInt(updateScheduleId, 10)).Execute() + err := r.client.DeleteUpdateSchedule(ctx, projectId, serverId, strconv.FormatInt(updateScheduleId, 10), region).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting server update schedule", fmt.Sprintf("Calling API: %v", err)) return @@ -356,15 +410,15 @@ func (r *scheduleResource) Delete(ctx context.Context, req resource.DeleteReques // The expected format of the resource import identifier is: // project_id,server_id,schedule_id func (r *scheduleResource) 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] == "" { + if len(idParts) != 4 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" || idParts[3] == "" { core.LogAndAddError(ctx, &resp.Diagnostics, "Error importing server update schedule", - fmt.Sprintf("Expected import identifier with format [project_id],[server_id],[update_schedule_id], got %q", req.ID), + fmt.Sprintf("Expected import identifier with format [project_id],[region],[server_id],[update_schedule_id], got %q", req.ID), ) return } - intId, err := strconv.ParseInt(idParts[2], 10, 64) + intId, err := strconv.ParseInt(idParts[3], 10, 64) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error importing server update schedule", @@ -374,12 +428,13 @@ func (r *scheduleResource) ImportState(ctx context.Context, req resource.ImportS } resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("server_id"), idParts[1])...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("region"), idParts[1])...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("server_id"), idParts[2])...) resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("update_schedule_id"), intId)...) tflog.Info(ctx, "Server update schedule state imported.") } -func mapFields(schedule *serverupdate.UpdateSchedule, model *Model) error { +func mapFields(schedule *serverupdate.UpdateSchedule, model *Model, region string) error { if schedule == nil { return fmt.Errorf("response input is nil") } @@ -393,6 +448,7 @@ func mapFields(schedule *serverupdate.UpdateSchedule, model *Model) error { model.UpdateScheduleId = types.Int64PointerValue(schedule.Id) idParts := []string{ model.ProjectId.ValueString(), + region, model.ServerId.ValueString(), strconv.FormatInt(model.UpdateScheduleId.ValueInt64(), 10), } @@ -403,17 +459,18 @@ func mapFields(schedule *serverupdate.UpdateSchedule, model *Model) error { model.Rrule = types.StringPointerValue(schedule.Rrule) model.Enabled = types.BoolPointerValue(schedule.Enabled) model.MaintenanceWindow = types.Int64PointerValue(schedule.MaintenanceWindow) + model.Region = types.StringValue(region) return nil } // If already enabled, just continues -func enableUpdatesService(ctx context.Context, model *Model, client *serverupdate.APIClient) error { +func enableUpdatesService(ctx context.Context, model *Model, client *serverupdate.APIClient, region string) error { projectId := model.ProjectId.ValueString() serverId := model.ServerId.ValueString() - enableServicePayload := serverupdate.EnableServicePayload{} + payload := serverupdate.EnableServiceResourcePayload{} tflog.Debug(ctx, "Enabling server update service") - err := client.EnableService(ctx, projectId, serverId).EnableServicePayload(enableServicePayload).Execute() + err := client.EnableServiceResource(ctx, projectId, serverId, region).EnableServiceResourcePayload(payload).Execute() if err != nil { if strings.Contains(err.Error(), "Tried to activate already active service") { tflog.Debug(ctx, "Service for server update already enabled") diff --git a/stackit/internal/services/serverupdate/schedule/resource_test.go b/stackit/internal/services/serverupdate/schedule/resource_test.go index b463d242..43cb2895 100644 --- a/stackit/internal/services/serverupdate/schedule/resource_test.go +++ b/stackit/internal/services/serverupdate/schedule/resource_test.go @@ -10,9 +10,11 @@ import ( ) func TestMapFields(t *testing.T) { + const testRegion = "region" tests := []struct { description string input *sdk.UpdateSchedule + region string expected Model isValid bool }{ @@ -21,11 +23,13 @@ func TestMapFields(t *testing.T) { &sdk.UpdateSchedule{ Id: utils.Ptr(int64(5)), }, + testRegion, Model{ - ID: types.StringValue("project_uid,server_uid,5"), + ID: types.StringValue("project_uid,region,server_uid,5"), ProjectId: types.StringValue("project_uid"), ServerId: types.StringValue("server_uid"), UpdateScheduleId: types.Int64Value(5), + Region: types.StringValue(testRegion), }, true, }, @@ -38,27 +42,31 @@ func TestMapFields(t *testing.T) { Rrule: utils.Ptr("DTSTART;TZID=Europe/Sofia:20200803T023000 RRULE:FREQ=DAILY;INTERVAL=1"), MaintenanceWindow: utils.Ptr(int64(1)), }, + testRegion, Model{ ServerId: types.StringValue("server_uid"), ProjectId: types.StringValue("project_uid"), UpdateScheduleId: types.Int64Value(5), - ID: types.StringValue("project_uid,server_uid,5"), + ID: types.StringValue("project_uid,region,server_uid,5"), Name: types.StringValue("update_schedule_name_1"), Rrule: types.StringValue("DTSTART;TZID=Europe/Sofia:20200803T023000 RRULE:FREQ=DAILY;INTERVAL=1"), Enabled: types.BoolValue(true), MaintenanceWindow: types.Int64Value(1), + Region: types.StringValue(testRegion), }, true, }, { "nil_response", nil, + testRegion, Model{}, false, }, { "no_resource_id", &sdk.UpdateSchedule{}, + testRegion, Model{}, false, }, @@ -69,7 +77,7 @@ func TestMapFields(t *testing.T) { ProjectId: tt.expected.ProjectId, ServerId: tt.expected.ServerId, } - err := mapFields(tt.input, state) + err := mapFields(tt.input, state, tt.region) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } diff --git a/stackit/internal/services/serverupdate/schedule/schedule_datasource.go b/stackit/internal/services/serverupdate/schedule/schedule_datasource.go index b1e76163..99e0b41a 100644 --- a/stackit/internal/services/serverupdate/schedule/schedule_datasource.go +++ b/stackit/internal/services/serverupdate/schedule/schedule_datasource.go @@ -10,6 +10,7 @@ import ( "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/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features" @@ -37,7 +38,8 @@ func NewScheduleDataSource() datasource.DataSource { // scheduleDataSource is the data source implementation. type scheduleDataSource struct { - client *serverupdate.APIClient + client *serverupdate.APIClient + providerData core.ProviderData } // Metadata returns the data source type name. @@ -52,14 +54,15 @@ func (r *scheduleDataSource) Configure(ctx context.Context, req datasource.Confi return } - providerData, ok := req.ProviderData.(core.ProviderData) + var ok bool + r.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 } if !scheduleDataSourceBetaCheckDone { - features.CheckBetaResourcesEnabled(ctx, &providerData, &resp.Diagnostics, "stackit_server_update_schedule", "data source") + features.CheckBetaResourcesEnabled(ctx, &r.providerData, &resp.Diagnostics, "stackit_server_update_schedule", "data source") if resp.Diagnostics.HasError() { return } @@ -68,16 +71,15 @@ func (r *scheduleDataSource) Configure(ctx context.Context, req datasource.Confi var apiClient *serverupdate.APIClient var err error - if providerData.ServerUpdateCustomEndpoint != "" { - ctx = tflog.SetField(ctx, "server_update_custom_endpoint", providerData.ServerUpdateCustomEndpoint) + if r.providerData.ServerUpdateCustomEndpoint != "" { + ctx = tflog.SetField(ctx, "server_update_custom_endpoint", r.providerData.ServerUpdateCustomEndpoint) apiClient, err = serverupdate.NewAPIClient( - config.WithCustomAuth(providerData.RoundTripper), - config.WithEndpoint(providerData.ServerUpdateCustomEndpoint), + config.WithCustomAuth(r.providerData.RoundTripper), + config.WithEndpoint(r.providerData.ServerUpdateCustomEndpoint), ) } else { apiClient, err = serverupdate.NewAPIClient( - config.WithCustomAuth(providerData.RoundTripper), - config.WithRegion(providerData.GetRegion()), + config.WithCustomAuth(r.providerData.RoundTripper), ) } @@ -97,7 +99,7 @@ func (r *scheduleDataSource) Schema(_ context.Context, _ datasource.SchemaReques MarkdownDescription: features.AddBetaDescription("Server update schedule datasource schema. Must have a `region` specified in the provider configuration."), Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ - Description: "Terraform's internal resource identifier. It is structured as \"`project_id`,`server_id`,`update_schedule_id`\".", + Description: "Terraform's internal resource identifier. It is structured as \"`project_id`,`region`,`server_id`,`update_schedule_id`\".", Computed: true, }, "name": schema.StringAttribute{ @@ -136,6 +138,11 @@ func (r *scheduleDataSource) Schema(_ context.Context, _ datasource.SchemaReques Description: "Maintenance window [1..24].", Computed: true, }, + "region": schema.StringAttribute{ + // the region cannot be found, so it has to be passed + Optional: true, + Description: "The resource region. If not defined, the provider region is used.", + }, }, } } @@ -151,11 +158,18 @@ func (r *scheduleDataSource) Read(ctx context.Context, req datasource.ReadReques projectId := model.ProjectId.ValueString() serverId := model.ServerId.ValueString() updateScheduleId := model.UpdateScheduleId.ValueInt64() + var region string + if utils.IsUndefined(model.Region) { + region = r.providerData.GetRegion() + } else { + region = model.Region.ValueString() + } ctx = tflog.SetField(ctx, "project_id", projectId) ctx = tflog.SetField(ctx, "server_id", serverId) ctx = tflog.SetField(ctx, "update_schedule_id", updateScheduleId) + ctx = tflog.SetField(ctx, "region", region) - scheduleResp, err := r.client.GetUpdateSchedule(ctx, projectId, serverId, strconv.FormatInt(updateScheduleId, 10)).Execute() + scheduleResp, err := r.client.GetUpdateSchedule(ctx, projectId, serverId, strconv.FormatInt(updateScheduleId, 10), region).Execute() if err != nil { oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped if ok && oapiErr.StatusCode == http.StatusNotFound { @@ -166,7 +180,7 @@ func (r *scheduleDataSource) Read(ctx context.Context, req datasource.ReadReques } // Map response body to schema - err = mapFields(scheduleResp, &model) + err = mapFields(scheduleResp, &model, region) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading server update schedule", fmt.Sprintf("Processing API payload: %v", err)) return diff --git a/stackit/internal/services/serverupdate/schedule/schedules_datasource.go b/stackit/internal/services/serverupdate/schedule/schedules_datasource.go index 99a52087..ee3ef78b 100644 --- a/stackit/internal/services/serverupdate/schedule/schedules_datasource.go +++ b/stackit/internal/services/serverupdate/schedule/schedules_datasource.go @@ -12,6 +12,7 @@ import ( "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" @@ -37,7 +38,8 @@ func NewSchedulesDataSource() datasource.DataSource { // schedulesDataSource is the data source implementation. type schedulesDataSource struct { - client *serverupdate.APIClient + client *serverupdate.APIClient + providerData core.ProviderData } // Metadata returns the data source type name. @@ -52,14 +54,15 @@ func (r *schedulesDataSource) Configure(ctx context.Context, req datasource.Conf return } - providerData, ok := req.ProviderData.(core.ProviderData) + var ok bool + r.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 } if !schedulesDataSourceBetaCheckDone { - features.CheckBetaResourcesEnabled(ctx, &providerData, &resp.Diagnostics, "stackit_server_update_schedules", "data source") + features.CheckBetaResourcesEnabled(ctx, &r.providerData, &resp.Diagnostics, "stackit_server_update_schedules", "data source") if resp.Diagnostics.HasError() { return } @@ -68,16 +71,15 @@ func (r *schedulesDataSource) Configure(ctx context.Context, req datasource.Conf var apiClient *serverupdate.APIClient var err error - if providerData.ServerUpdateCustomEndpoint != "" { - ctx = tflog.SetField(ctx, "server_update_custom_endpoint", providerData.ServerUpdateCustomEndpoint) + if r.providerData.ServerUpdateCustomEndpoint != "" { + ctx = tflog.SetField(ctx, "server_update_custom_endpoint", r.providerData.ServerUpdateCustomEndpoint) apiClient, err = serverupdate.NewAPIClient( - config.WithCustomAuth(providerData.RoundTripper), - config.WithEndpoint(providerData.ServerUpdateCustomEndpoint), + config.WithCustomAuth(r.providerData.RoundTripper), + config.WithEndpoint(r.providerData.ServerUpdateCustomEndpoint), ) } else { apiClient, err = serverupdate.NewAPIClient( - config.WithCustomAuth(providerData.RoundTripper), - config.WithRegion(providerData.GetRegion()), + config.WithCustomAuth(r.providerData.RoundTripper), ) } @@ -97,7 +99,7 @@ func (r *schedulesDataSource) Schema(_ context.Context, _ datasource.SchemaReque MarkdownDescription: features.AddBetaDescription("Server update schedules datasource schema. Must have a `region` specified in the provider configuration."), Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ - Description: "Terraform's internal data source identifier. It is structured as \"`project_id`,`server_id`\".", + Description: "Terraform's internal data source identifier. It is structured as \"`project_id`,`region`,`server_id`\".", Computed: true, }, "project_id": schema.StringAttribute{ @@ -142,6 +144,11 @@ func (r *schedulesDataSource) Schema(_ context.Context, _ datasource.SchemaReque }, }, }, + "region": schema.StringAttribute{ + // the region cannot be found, so it has to be passed + Optional: true, + Description: "The resource region. If not defined, the provider region is used.", + }, }, } } @@ -152,6 +159,7 @@ type schedulesDataSourceModel struct { ProjectId types.String `tfsdk:"project_id"` ServerId types.String `tfsdk:"server_id"` Items []schedulesDatasourceItemModel `tfsdk:"items"` + Region types.String `tfsdk:"region"` } // schedulesDatasourceItemModel maps schedule schema data. @@ -173,10 +181,16 @@ func (r *schedulesDataSource) Read(ctx context.Context, req datasource.ReadReque } projectId := model.ProjectId.ValueString() serverId := model.ServerId.ValueString() + var region string + if utils.IsUndefined(model.Region) { + region = r.providerData.GetRegion() + } else { + region = model.Region.ValueString() + } ctx = tflog.SetField(ctx, "project_id", projectId) ctx = tflog.SetField(ctx, "server_id", serverId) - schedules, err := r.client.ListUpdateSchedules(ctx, projectId, serverId).Execute() + schedules, err := r.client.ListUpdateSchedules(ctx, projectId, serverId, region).Execute() if err != nil { oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped if ok && oapiErr.StatusCode == http.StatusNotFound { @@ -187,7 +201,7 @@ func (r *schedulesDataSource) Read(ctx context.Context, req datasource.ReadReque } // Map response body to schema - err = mapSchedulesDatasourceFields(ctx, schedules, &model) + err = mapSchedulesDatasourceFields(ctx, schedules, &model, region) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading server update schedules", fmt.Sprintf("Processing API payload: %v", err)) return @@ -202,7 +216,7 @@ func (r *schedulesDataSource) Read(ctx context.Context, req datasource.ReadReque tflog.Info(ctx, "Server update schedules read") } -func mapSchedulesDatasourceFields(ctx context.Context, schedules *serverupdate.GetUpdateSchedulesResponse, model *schedulesDataSourceModel) error { +func mapSchedulesDatasourceFields(ctx context.Context, schedules *serverupdate.GetUpdateSchedulesResponse, model *schedulesDataSourceModel, region string) error { if schedules == nil { return fmt.Errorf("response input is nil") } @@ -214,10 +228,11 @@ func mapSchedulesDatasourceFields(ctx context.Context, schedules *serverupdate.G projectId := model.ProjectId.ValueString() serverId := model.ServerId.ValueString() - idParts := []string{projectId, serverId} + idParts := []string{projectId, region, serverId} model.ID = types.StringValue( strings.Join(idParts, core.Separator), ) + model.Region = types.StringValue(region) for _, schedule := range *schedules.Items { scheduleState := schedulesDatasourceItemModel{ diff --git a/stackit/internal/services/serverupdate/schedule/schedules_datasource_test.go b/stackit/internal/services/serverupdate/schedule/schedules_datasource_test.go index 2d1c8156..2619daf0 100644 --- a/stackit/internal/services/serverupdate/schedule/schedules_datasource_test.go +++ b/stackit/internal/services/serverupdate/schedule/schedules_datasource_test.go @@ -11,9 +11,11 @@ import ( ) func TestMapSchedulesDataSourceFields(t *testing.T) { + const testRegion = "region" tests := []struct { description string input *sdk.GetUpdateSchedulesResponse + region string expected schedulesDataSourceModel isValid bool }{ @@ -22,11 +24,13 @@ func TestMapSchedulesDataSourceFields(t *testing.T) { &sdk.GetUpdateSchedulesResponse{ Items: &[]sdk.UpdateSchedule{}, }, + testRegion, schedulesDataSourceModel{ - ID: types.StringValue("project_uid,server_uid"), + ID: types.StringValue("project_uid,region,server_uid"), ProjectId: types.StringValue("project_uid"), ServerId: types.StringValue("server_uid"), Items: nil, + Region: types.StringValue(testRegion), }, true, }, @@ -43,8 +47,9 @@ func TestMapSchedulesDataSourceFields(t *testing.T) { }, }, }, + testRegion, schedulesDataSourceModel{ - ID: types.StringValue("project_uid,server_uid"), + ID: types.StringValue("project_uid,region,server_uid"), ServerId: types.StringValue("server_uid"), ProjectId: types.StringValue("project_uid"), Items: []schedulesDatasourceItemModel{ @@ -56,12 +61,14 @@ func TestMapSchedulesDataSourceFields(t *testing.T) { MaintenanceWindow: types.Int64Value(1), }, }, + Region: types.StringValue(testRegion), }, true, }, { "nil_response", nil, + testRegion, schedulesDataSourceModel{}, false, }, @@ -73,7 +80,7 @@ func TestMapSchedulesDataSourceFields(t *testing.T) { ServerId: tt.expected.ServerId, } ctx := context.TODO() - err := mapSchedulesDatasourceFields(ctx, tt.input, state) + err := mapSchedulesDatasourceFields(ctx, tt.input, state, tt.region) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } diff --git a/stackit/internal/services/serverupdate/serverupdate_acc_test.go b/stackit/internal/services/serverupdate/serverupdate_acc_test.go index f12639b2..f1a41273 100644 --- a/stackit/internal/services/serverupdate/serverupdate_acc_test.go +++ b/stackit/internal/services/serverupdate/serverupdate_acc_test.go @@ -26,7 +26,11 @@ var serverUpdateScheduleResource = map[string]string{ "maintenance_window": "1", } -func resourceConfig(maintenanceWindow int64) string { +func resourceConfig(maintenanceWindow int64, region *string) string { + var regionConfig string + if region != nil { + regionConfig = fmt.Sprintf(`region = %q`, *region) + } return fmt.Sprintf(` %s @@ -37,6 +41,7 @@ func resourceConfig(maintenanceWindow int64) string { rrule = "%s" enabled = true maintenance_window = %d + %s } `, testutil.ServerUpdateProviderConfig(), @@ -45,6 +50,7 @@ func resourceConfig(maintenanceWindow int64) string { serverUpdateScheduleResource["name"], serverUpdateScheduleResource["rrule"], maintenanceWindow, + regionConfig, ) } @@ -53,6 +59,7 @@ func TestAccServerUpdateScheduleResource(t *testing.T) { fmt.Println("TF_ACC_SERVER_ID not set, skipping test") return } + testRegion := utils.Ptr("eu01") var invalidMaintenanceWindow int64 = 0 var validMaintenanceWindow int64 = 15 var updatedMaintenanceWindow int64 = 8 @@ -62,12 +69,12 @@ func TestAccServerUpdateScheduleResource(t *testing.T) { Steps: []resource.TestStep{ // Creation fail { - Config: resourceConfig(invalidMaintenanceWindow), + Config: resourceConfig(invalidMaintenanceWindow, testRegion), ExpectError: regexp.MustCompile(`.*maintenance_window value must be at least 1*`), }, // Creation { - Config: resourceConfig(validMaintenanceWindow), + Config: resourceConfig(validMaintenanceWindow, testRegion), Check: resource.ComposeAggregateTestCheckFunc( // Update schedule data resource.TestCheckResourceAttr("stackit_server_update_schedule.test_schedule", "project_id", serverUpdateScheduleResource["project_id"]), @@ -77,6 +84,7 @@ func TestAccServerUpdateScheduleResource(t *testing.T) { resource.TestCheckResourceAttr("stackit_server_update_schedule.test_schedule", "name", serverUpdateScheduleResource["name"]), resource.TestCheckResourceAttr("stackit_server_update_schedule.test_schedule", "rrule", serverUpdateScheduleResource["rrule"]), resource.TestCheckResourceAttr("stackit_server_update_schedule.test_schedule", "enabled", strconv.FormatBool(true)), + resource.TestCheckResourceAttr("stackit_server_update_schedule.test_schedule", "region", *testRegion), ), }, // data source @@ -94,7 +102,7 @@ func TestAccServerUpdateScheduleResource(t *testing.T) { server_id = stackit_server_update_schedule.test_schedule.server_id update_schedule_id = stackit_server_update_schedule.test_schedule.update_schedule_id }`, - resourceConfig(validMaintenanceWindow), + resourceConfig(validMaintenanceWindow, testRegion), ), Check: resource.ComposeAggregateTestCheckFunc( // Server update schedule data @@ -124,14 +132,14 @@ func TestAccServerUpdateScheduleResource(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute update_schedule_id") } - return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.ServerId, scheduleId), nil + return fmt.Sprintf("%s,%s,%s,%s", testutil.ProjectId, testutil.Region, testutil.ServerId, scheduleId), nil }, ImportState: true, ImportStateVerify: true, }, // Update { - Config: resourceConfig(updatedMaintenanceWindow), + Config: resourceConfig(updatedMaintenanceWindow, nil), Check: resource.ComposeAggregateTestCheckFunc( // Update schedule data resource.TestCheckResourceAttr("stackit_server_update_schedule.test_schedule", "project_id", serverUpdateScheduleResource["project_id"]), @@ -154,9 +162,7 @@ func testAccCheckServerUpdateScheduleDestroy(s *terraform.State) error { var client *serverupdate.APIClient var err error if testutil.ServerUpdateCustomEndpoint == "" { - client, err = serverupdate.NewAPIClient( - config.WithRegion("eu01"), - ) + client, err = serverupdate.NewAPIClient() } else { client, err = serverupdate.NewAPIClient( config.WithEndpoint(testutil.ServerUpdateCustomEndpoint), @@ -176,7 +182,7 @@ func testAccCheckServerUpdateScheduleDestroy(s *terraform.State) error { schedulesToDestroy = append(schedulesToDestroy, scheduleId) } - schedulesResp, err := client.ListUpdateSchedules(ctx, testutil.ProjectId, testutil.ServerId).Execute() + schedulesResp, err := client.ListUpdateSchedules(ctx, testutil.ProjectId, testutil.ServerId, testutil.Region).Execute() if err != nil { return fmt.Errorf("getting schedulesResp: %w", err) } @@ -188,7 +194,7 @@ func testAccCheckServerUpdateScheduleDestroy(s *terraform.State) error { } scheduleId := strconv.FormatInt(*schedules[i].Id, 10) if utils.Contains(schedulesToDestroy, scheduleId) { - err := client.DeleteUpdateScheduleExecute(ctx, testutil.ProjectId, testutil.ServerId, scheduleId) + err := client.DeleteUpdateScheduleExecute(ctx, testutil.ProjectId, testutil.ServerId, scheduleId, testutil.Region) if err != nil { return fmt.Errorf("destroying server update schedule %s during CheckDestroy: %w", scheduleId, err) } diff --git a/stackit/internal/testutil/testutil.go b/stackit/internal/testutil/testutil.go index 70347c0b..763d9f3a 100644 --- a/stackit/internal/testutil/testutil.go +++ b/stackit/internal/testutil/testutil.go @@ -360,7 +360,8 @@ func ServerUpdateProviderConfig() string { if ServerUpdateCustomEndpoint == "" { return ` provider "stackit" { - region = "eu01" + default_region = "eu01" + enable_beta_resources = true }` } return fmt.Sprintf(`