@@ -18,6 +18,7 @@ import (
1818 "github.com/hashicorp/terraform-plugin-framework/schema/validator"
1919 "github.com/hashicorp/terraform-plugin-framework/types"
2020 "github.com/hashicorp/terraform-plugin-log/tflog"
21+ "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils"
2122
2223 "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion"
2324 "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
3940 _ resource.Resource = & scheduleResource {}
4041 _ resource.ResourceWithConfigure = & scheduleResource {}
4142 _ resource.ResourceWithImportState = & scheduleResource {}
43+ _ resource.ResourceWithModifyPlan = & scheduleResource {}
4244)
4345
4446type Model struct {
@@ -50,6 +52,7 @@ type Model struct {
5052 Rrule types.String `tfsdk:"rrule"`
5153 Enabled types.Bool `tfsdk:"enabled"`
5254 MaintenanceWindow types.Int64 `tfsdk:"maintenance_window"`
55+ Region types.String `tfsdk:"region"`
5356}
5457
5558// NewScheduleResource is a helper function to simplify the provider implementation.
@@ -59,7 +62,38 @@ func NewScheduleResource() resource.Resource {
5962
6063// scheduleResource is the resource implementation.
6164type scheduleResource struct {
62- client * serverupdate.APIClient
65+ client * serverupdate.APIClient
66+ providerData core.ProviderData
67+ }
68+
69+ // ModifyPlan implements resource.ResourceWithModifyPlan.
70+ // Use the modifier to set the effective region in the current plan.
71+ func (r * scheduleResource ) ModifyPlan (ctx context.Context , req resource.ModifyPlanRequest , resp * resource.ModifyPlanResponse ) { // nolint:gocritic // function signature required by Terraform
72+ var configModel Model
73+ // skip initial empty configuration to avoid follow-up errors
74+ if req .Config .Raw .IsNull () {
75+ return
76+ }
77+ resp .Diagnostics .Append (req .Config .Get (ctx , & configModel )... )
78+ if resp .Diagnostics .HasError () {
79+ return
80+ }
81+
82+ var planModel Model
83+ resp .Diagnostics .Append (req .Plan .Get (ctx , & planModel )... )
84+ if resp .Diagnostics .HasError () {
85+ return
86+ }
87+
88+ utils .AdaptRegion (ctx , configModel .Region , & planModel .Region , r .providerData .GetRegion (), resp )
89+ if resp .Diagnostics .HasError () {
90+ return
91+ }
92+
93+ resp .Diagnostics .Append (resp .Plan .Set (ctx , planModel )... )
94+ if resp .Diagnostics .HasError () {
95+ return
96+ }
6397}
6498
6599// Metadata returns the resource type name.
@@ -74,14 +108,15 @@ func (r *scheduleResource) Configure(ctx context.Context, req resource.Configure
74108 return
75109 }
76110
77- providerData , ok := req .ProviderData .(core.ProviderData )
111+ var ok bool
112+ r .providerData , ok = req .ProviderData .(core.ProviderData )
78113 if ! ok {
79114 core .LogAndAddError (ctx , & resp .Diagnostics , "Error configuring API client" , fmt .Sprintf ("Expected configure type stackit.ProviderData, got %T" , req .ProviderData ))
80115 return
81116 }
82117
83118 if ! resourceBetaCheckDone {
84- features .CheckBetaResourcesEnabled (ctx , & providerData , & resp .Diagnostics , "stackit_server_update_schedule" , "resource" )
119+ features .CheckBetaResourcesEnabled (ctx , & r . providerData , & resp .Diagnostics , "stackit_server_update_schedule" , "resource" )
85120 if resp .Diagnostics .HasError () {
86121 return
87122 }
@@ -90,16 +125,15 @@ func (r *scheduleResource) Configure(ctx context.Context, req resource.Configure
90125
91126 var apiClient * serverupdate.APIClient
92127 var err error
93- if providerData .ServerUpdateCustomEndpoint != "" {
94- ctx = tflog .SetField (ctx , "server_update_custom_endpoint" , providerData .ServerUpdateCustomEndpoint )
128+ if r . providerData .ServerUpdateCustomEndpoint != "" {
129+ ctx = tflog .SetField (ctx , "server_update_custom_endpoint" , r . providerData .ServerUpdateCustomEndpoint )
95130 apiClient , err = serverupdate .NewAPIClient (
96- config .WithCustomAuth (providerData .RoundTripper ),
97- config .WithEndpoint (providerData .ServerUpdateCustomEndpoint ),
131+ config .WithCustomAuth (r . providerData .RoundTripper ),
132+ config .WithEndpoint (r . providerData .ServerUpdateCustomEndpoint ),
98133 )
99134 } else {
100135 apiClient , err = serverupdate .NewAPIClient (
101- config .WithCustomAuth (providerData .RoundTripper ),
102- config .WithRegion (providerData .GetRegion ()),
136+ config .WithCustomAuth (r .providerData .RoundTripper ),
103137 )
104138 }
105139
@@ -119,7 +153,7 @@ func (r *scheduleResource) Schema(_ context.Context, _ resource.SchemaRequest, r
119153 MarkdownDescription : features .AddBetaDescription ("Server update schedule resource schema. Must have a `region` specified in the provider configuration." ),
120154 Attributes : map [string ]schema.Attribute {
121155 "id" : schema.StringAttribute {
122- Description : "Terraform's internal resource identifier. It is structured as \" `project_id`,`server_id`,`update_schedule_id`\" ." ,
156+ Description : "Terraform's internal resource identifier. It is structured as \" `project_id`,`region`,` server_id`,`update_schedule_id`\" ." ,
123157 Computed : true ,
124158 PlanModifiers : []planmodifier.String {
125159 stringplanmodifier .UseStateForUnknown (),
@@ -194,6 +228,15 @@ func (r *scheduleResource) Schema(_ context.Context, _ resource.SchemaRequest, r
194228 int64validator .AtMost (24 ),
195229 },
196230 },
231+ "region" : schema.StringAttribute {
232+ Optional : true ,
233+ // must be computed to allow for storing the override value from the provider
234+ Computed : true ,
235+ Description : "The resource region. If not defined, the provider region is used." ,
236+ PlanModifiers : []planmodifier.String {
237+ stringplanmodifier .RequiresReplace (),
238+ },
239+ },
197240 },
198241 }
199242}
@@ -208,11 +251,13 @@ func (r *scheduleResource) Create(ctx context.Context, req resource.CreateReques
208251 }
209252 projectId := model .ProjectId .ValueString ()
210253 serverId := model .ServerId .ValueString ()
254+ region := model .Region .ValueString ()
211255 ctx = tflog .SetField (ctx , "project_id" , projectId )
212256 ctx = tflog .SetField (ctx , "server_id" , serverId )
257+ ctx = tflog .SetField (ctx , "region" , region )
213258
214259 // Enable updates if not already enabled
215- err := enableUpdatesService (ctx , & model , r .client )
260+ err := enableUpdatesService (ctx , & model , r .client , region )
216261 if err != nil {
217262 core .LogAndAddError (ctx , & resp .Diagnostics , "Error creating server update schedule" , fmt .Sprintf ("Enabling server update project before creation: %v" , err ))
218263 return
@@ -224,15 +269,15 @@ func (r *scheduleResource) Create(ctx context.Context, req resource.CreateReques
224269 core .LogAndAddError (ctx , & resp .Diagnostics , "Error creating server update schedule" , fmt .Sprintf ("Creating API payload: %v" , err ))
225270 return
226271 }
227- scheduleResp , err := r .client .CreateUpdateSchedule (ctx , projectId , serverId ).CreateUpdateSchedulePayload (* payload ).Execute ()
272+ scheduleResp , err := r .client .CreateUpdateSchedule (ctx , projectId , serverId , region ).CreateUpdateSchedulePayload (* payload ).Execute ()
228273 if err != nil {
229274 core .LogAndAddError (ctx , & resp .Diagnostics , "Error creating server update schedule" , fmt .Sprintf ("Calling API: %v" , err ))
230275 return
231276 }
232277 ctx = tflog .SetField (ctx , "update_schedule_id" , * scheduleResp .Id )
233278
234279 // Map response body to schema
235- err = mapFields (scheduleResp , & model )
280+ err = mapFields (scheduleResp , & model , region )
236281 if err != nil {
237282 core .LogAndAddError (ctx , & resp .Diagnostics , "Error creating server update schedule" , fmt .Sprintf ("Processing API payload: %v" , err ))
238283 return
@@ -256,11 +301,16 @@ func (r *scheduleResource) Read(ctx context.Context, req resource.ReadRequest, r
256301 projectId := model .ProjectId .ValueString ()
257302 serverId := model .ServerId .ValueString ()
258303 updateScheduleId := model .UpdateScheduleId .ValueInt64 ()
304+ region := model .Region .ValueString ()
305+ if region == "" {
306+ region = r .providerData .GetRegion ()
307+ }
259308 ctx = tflog .SetField (ctx , "project_id" , projectId )
260309 ctx = tflog .SetField (ctx , "server_id" , serverId )
310+ ctx = tflog .SetField (ctx , "region" , region )
261311 ctx = tflog .SetField (ctx , "update_schedule_id" , updateScheduleId )
262312
263- scheduleResp , err := r .client .GetUpdateSchedule (ctx , projectId , serverId , strconv .FormatInt (updateScheduleId , 10 )).Execute ()
313+ scheduleResp , err := r .client .GetUpdateSchedule (ctx , projectId , serverId , strconv .FormatInt (updateScheduleId , 10 ), region ).Execute ()
264314 if err != nil {
265315 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
266316 if ok && oapiErr .StatusCode == http .StatusNotFound {
@@ -272,7 +322,7 @@ func (r *scheduleResource) Read(ctx context.Context, req resource.ReadRequest, r
272322 }
273323
274324 // Map response body to schema
275- err = mapFields (scheduleResp , & model )
325+ err = mapFields (scheduleResp , & model , region )
276326 if err != nil {
277327 core .LogAndAddError (ctx , & resp .Diagnostics , "Error reading update schedule" , fmt .Sprintf ("Processing API payload: %v" , err ))
278328 return
@@ -298,8 +348,10 @@ func (r *scheduleResource) Update(ctx context.Context, req resource.UpdateReques
298348 projectId := model .ProjectId .ValueString ()
299349 serverId := model .ServerId .ValueString ()
300350 updateScheduleId := model .UpdateScheduleId .ValueInt64 ()
351+ region := model .Region .ValueString ()
301352 ctx = tflog .SetField (ctx , "project_id" , projectId )
302353 ctx = tflog .SetField (ctx , "server_id" , serverId )
354+ ctx = tflog .SetField (ctx , "region" , region )
303355 ctx = tflog .SetField (ctx , "update_schedule_id" , updateScheduleId )
304356
305357 // Update schedule
@@ -309,14 +361,14 @@ func (r *scheduleResource) Update(ctx context.Context, req resource.UpdateReques
309361 return
310362 }
311363
312- scheduleResp , err := r .client .UpdateUpdateSchedule (ctx , projectId , serverId , strconv .FormatInt (updateScheduleId , 10 )).UpdateUpdateSchedulePayload (* payload ).Execute ()
364+ scheduleResp , err := r .client .UpdateUpdateSchedule (ctx , projectId , serverId , strconv .FormatInt (updateScheduleId , 10 ), region ).UpdateUpdateSchedulePayload (* payload ).Execute ()
313365 if err != nil {
314366 core .LogAndAddError (ctx , & resp .Diagnostics , "Error updating server update schedule" , fmt .Sprintf ("Calling API: %v" , err ))
315367 return
316368 }
317369
318370 // Map response body to schema
319- err = mapFields (scheduleResp , & model )
371+ err = mapFields (scheduleResp , & model , region )
320372 if err != nil {
321373 core .LogAndAddError (ctx , & resp .Diagnostics , "Error updating server update schedule" , fmt .Sprintf ("Processing API payload: %v" , err ))
322374 return
@@ -340,11 +392,13 @@ func (r *scheduleResource) Delete(ctx context.Context, req resource.DeleteReques
340392 projectId := model .ProjectId .ValueString ()
341393 serverId := model .ServerId .ValueString ()
342394 updateScheduleId := model .UpdateScheduleId .ValueInt64 ()
395+ region := model .Region .ValueString ()
343396 ctx = tflog .SetField (ctx , "project_id" , projectId )
344397 ctx = tflog .SetField (ctx , "server_id" , serverId )
398+ ctx = tflog .SetField (ctx , "region" , region )
345399 ctx = tflog .SetField (ctx , "update_schedule_id" , updateScheduleId )
346400
347- err := r .client .DeleteUpdateSchedule (ctx , projectId , serverId , strconv .FormatInt (updateScheduleId , 10 )).Execute ()
401+ err := r .client .DeleteUpdateSchedule (ctx , projectId , serverId , strconv .FormatInt (updateScheduleId , 10 ), region ).Execute ()
348402 if err != nil {
349403 core .LogAndAddError (ctx , & resp .Diagnostics , "Error deleting server update schedule" , fmt .Sprintf ("Calling API: %v" , err ))
350404 return
@@ -356,15 +410,15 @@ func (r *scheduleResource) Delete(ctx context.Context, req resource.DeleteReques
356410// The expected format of the resource import identifier is: // project_id,server_id,schedule_id
357411func (r * scheduleResource ) ImportState (ctx context.Context , req resource.ImportStateRequest , resp * resource.ImportStateResponse ) {
358412 idParts := strings .Split (req .ID , core .Separator )
359- if len (idParts ) != 3 || idParts [0 ] == "" || idParts [1 ] == "" || idParts [2 ] == "" {
413+ if len (idParts ) != 4 || idParts [0 ] == "" || idParts [1 ] == "" || idParts [2 ] == "" || idParts [ 3 ] == "" {
360414 core .LogAndAddError (ctx , & resp .Diagnostics ,
361415 "Error importing server update schedule" ,
362- fmt .Sprintf ("Expected import identifier with format [project_id],[server_id],[update_schedule_id], got %q" , req .ID ),
416+ fmt .Sprintf ("Expected import identifier with format [project_id],[region],[ server_id],[update_schedule_id], got %q" , req .ID ),
363417 )
364418 return
365419 }
366420
367- intId , err := strconv .ParseInt (idParts [2 ], 10 , 64 )
421+ intId , err := strconv .ParseInt (idParts [3 ], 10 , 64 )
368422 if err != nil {
369423 core .LogAndAddError (ctx , & resp .Diagnostics ,
370424 "Error importing server update schedule" ,
@@ -374,12 +428,13 @@ func (r *scheduleResource) ImportState(ctx context.Context, req resource.ImportS
374428 }
375429
376430 resp .Diagnostics .Append (resp .State .SetAttribute (ctx , path .Root ("project_id" ), idParts [0 ])... )
377- resp .Diagnostics .Append (resp .State .SetAttribute (ctx , path .Root ("server_id" ), idParts [1 ])... )
431+ resp .Diagnostics .Append (resp .State .SetAttribute (ctx , path .Root ("region" ), idParts [1 ])... )
432+ resp .Diagnostics .Append (resp .State .SetAttribute (ctx , path .Root ("server_id" ), idParts [2 ])... )
378433 resp .Diagnostics .Append (resp .State .SetAttribute (ctx , path .Root ("update_schedule_id" ), intId )... )
379434 tflog .Info (ctx , "Server update schedule state imported." )
380435}
381436
382- func mapFields (schedule * serverupdate.UpdateSchedule , model * Model ) error {
437+ func mapFields (schedule * serverupdate.UpdateSchedule , model * Model , region string ) error {
383438 if schedule == nil {
384439 return fmt .Errorf ("response input is nil" )
385440 }
@@ -393,6 +448,7 @@ func mapFields(schedule *serverupdate.UpdateSchedule, model *Model) error {
393448 model .UpdateScheduleId = types .Int64PointerValue (schedule .Id )
394449 idParts := []string {
395450 model .ProjectId .ValueString (),
451+ region ,
396452 model .ServerId .ValueString (),
397453 strconv .FormatInt (model .UpdateScheduleId .ValueInt64 (), 10 ),
398454 }
@@ -403,17 +459,18 @@ func mapFields(schedule *serverupdate.UpdateSchedule, model *Model) error {
403459 model .Rrule = types .StringPointerValue (schedule .Rrule )
404460 model .Enabled = types .BoolPointerValue (schedule .Enabled )
405461 model .MaintenanceWindow = types .Int64PointerValue (schedule .MaintenanceWindow )
462+ model .Region = types .StringValue (region )
406463 return nil
407464}
408465
409466// If already enabled, just continues
410- func enableUpdatesService (ctx context.Context , model * Model , client * serverupdate.APIClient ) error {
467+ func enableUpdatesService (ctx context.Context , model * Model , client * serverupdate.APIClient , region string ) error {
411468 projectId := model .ProjectId .ValueString ()
412469 serverId := model .ServerId .ValueString ()
413- enableServicePayload := serverupdate.EnableServicePayload {}
470+ payload := serverupdate.EnableServiceResourcePayload {}
414471
415472 tflog .Debug (ctx , "Enabling server update service" )
416- err := client .EnableService (ctx , projectId , serverId ). EnableServicePayload ( enableServicePayload ).Execute ()
473+ err := client .EnableServiceResource (ctx , projectId , serverId , region ). EnableServiceResourcePayload ( payload ).Execute ()
417474 if err != nil {
418475 if strings .Contains (err .Error (), "Tried to activate already active service" ) {
419476 tflog .Debug (ctx , "Service for server update already enabled" )
0 commit comments