55 "fmt"
66 "strings"
77
8+ "github.com/stackitcloud/stackit-sdk-go/services/dns/wait"
9+
810 "github.com/hashicorp/terraform-plugin-framework/path"
911 "github.com/hashicorp/terraform-plugin-framework/resource"
1012 "github.com/hashicorp/terraform-plugin-framework/resource/schema"
@@ -18,7 +20,8 @@ import (
1820 fooUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/foo/utils"
1921 "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils"
2022
21- "github.com/stackitcloud/stackit-sdk-go/services/foo" // Import service "foo" from the STACKIT SDK for Go
23+ "github.com/stackitcloud/stackit-sdk-go/services/foo" // Import service "foo" from the STACKIT SDK for Go
24+ "github.com/stackitcloud/stackit-sdk-go/services/foo/wait" // Import service "foo" waiters from the STACKIT SDK for Go (in case the service API as asynchronous endpoints)
2225 // (...)
2326)
2427
@@ -27,13 +30,15 @@ var (
2730 _ resource.Resource = & barResource {}
2831 _ resource.ResourceWithConfigure = & barResource {}
2932 _ resource.ResourceWithImportState = & barResource {}
33+ _ resource.ResourceWithModifyPlan = & barResource {} // not needed for global APIs
3034)
3135
32- // Provider's internal model
36+ // Model is the internal model of the terraform resource
3337type Model struct {
3438 Id types.String `tfsdk:"id"` // needed by TF
3539 ProjectId types.String `tfsdk:"project_id"`
3640 BarId types.String `tfsdk:"bar_id"`
41+ Region types.String `tfsdk:"region"`
3742 MyRequiredField types.String `tfsdk:"my_required_field"`
3843 MyOptionalField types.String `tfsdk:"my_optional_field"`
3944 MyReadOnlyField types.String `tfsdk:"my_read_only_field"`
@@ -46,14 +51,46 @@ func NewBarResource() resource.Resource {
4651
4752// barResource is the resource implementation.
4853type barResource struct {
49- client * foo.APIClient
54+ client * foo.APIClient
55+ providerData core.ProviderData // not needed for global APIs
5056}
5157
5258// Metadata returns the resource type name.
5359func (r * barResource ) Metadata (_ context.Context , req resource.MetadataRequest , resp * resource.MetadataResponse ) {
5460 resp .TypeName = req .ProviderTypeName + "_foo_bar"
5561}
5662
63+ // ModifyPlan implements resource.ResourceWithModifyPlan.
64+ // Use the modifier to set the effective region in the current plan. - FYI: This isn't needed for global APIs.
65+ func (r * barResource ) ModifyPlan (ctx context.Context , req resource.ModifyPlanRequest , resp * resource.ModifyPlanResponse ) { // nolint:gocritic // function signature required by Terraform
66+ // FYI: the ModifyPlan implementation is not needed for global APIs
67+ var configModel Model
68+ // skip initial empty configuration to avoid follow-up errors
69+ if req .Config .Raw .IsNull () {
70+ return
71+ }
72+ resp .Diagnostics .Append (req .Config .Get (ctx , & configModel )... )
73+ if resp .Diagnostics .HasError () {
74+ return
75+ }
76+
77+ var planModel Model
78+ resp .Diagnostics .Append (req .Plan .Get (ctx , & planModel )... )
79+ if resp .Diagnostics .HasError () {
80+ return
81+ }
82+
83+ utils .AdaptRegion (ctx , configModel .Region , & planModel .Region , r .providerData .GetRegion (), resp )
84+ if resp .Diagnostics .HasError () {
85+ return
86+ }
87+
88+ resp .Diagnostics .Append (resp .Plan .Set (ctx , planModel )... )
89+ if resp .Diagnostics .HasError () {
90+ return
91+ }
92+ }
93+
5794// Configure adds the provider configured client to the resource.
5895func (r * barResource ) Configure (ctx context.Context , req resource.ConfigureRequest , resp * resource.ConfigureResponse ) {
5996 providerData , ok := conversion .ParseProviderData (ctx , req .ProviderData , & resp .Diagnostics )
@@ -76,6 +113,7 @@ func (r *barResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *
76113 "id" : "Terraform's internal resource identifier. It is structured as \" `project_id`,`bar_id`\" ." ,
77114 "project_id" : "STACKIT Project ID to which the bar is associated." ,
78115 "bar_id" : "The bar ID." ,
116+ "region" : "The resource region. If not defined, the provider region is used." ,
79117 "my_required_field" : "My required field description." ,
80118 "my_optional_field" : "My optional field description." ,
81119 "my_read_only_field" : "My read-only field description." ,
@@ -108,6 +146,15 @@ func (r *barResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *
108146 Description : descriptions ["bar_id" ],
109147 Computed : true ,
110148 },
149+ "region" : schema.StringAttribute { // not needed for global APIs
150+ Optional : true ,
151+ // must be computed to allow for storing the override value from the provider
152+ Computed : true ,
153+ Description : descriptions ["region" ],
154+ PlanModifiers : []planmodifier.String {
155+ stringplanmodifier .RequiresReplace (),
156+ },
157+ },
111158 "my_required_field" : schema.StringAttribute {
112159 Description : descriptions ["my_required_field" ],
113160 Required : true ,
@@ -136,36 +183,57 @@ func (r *barResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *
136183// Create creates the resource and sets the initial Terraform state.
137184func (r * barResource ) Create (ctx context.Context , req resource.CreateRequest , resp * resource.CreateResponse ) { // nolint:gocritic // function signature required by Terraform
138185 var model Model
139- diags := req .Plan .Get (ctx , & model )
140- resp .Diagnostics .Append (diags ... )
186+ resp .Diagnostics .Append (req .Plan .Get (ctx , & model )... )
141187 if resp .Diagnostics .HasError () {
142188 return
143189 }
144190 projectId := model .ProjectId .ValueString ()
145- barId := model .BarId .ValueString ()
191+ region := model .Region .ValueString () // not needed for global APIs
146192 ctx = tflog .SetField (ctx , "project_id" , projectId )
193+ ctx = tflog .SetField (ctx , "region" , region )
147194
148- // Create new bar
195+ // prepare the payload struct for the create bar request
149196 payload , err := toCreatePayload (& model )
150197 if err != nil {
151198 core .LogAndAddError (ctx , & resp .Diagnostics , "Error creating credential" , fmt .Sprintf ("Creating API payload: %v" , err ))
152199 return
153200 }
154- resp , err := r .client .CreateBar (ctx , projectId , barId ).CreateBarPayload (* payload ).Execute ()
201+
202+ // Create new bar
203+ barResp , err := r .client .CreateBar (ctx , projectId , region ).CreateBarPayload (* payload ).Execute ()
155204 if err != nil {
156205 core .LogAndAddError (ctx , & resp .Diagnostics , "Error creating bar" , fmt .Sprintf ("Calling API: %v" , err ))
157206 return
158207 }
208+
209+ // only in case the create bar API call is asynchronous (Make sure to include *ALL* fields which are part of the
210+ // internal terraform resource id! And please include the comment below in your code):
211+ // Write id attributes to state before polling via the wait handler - just in case anything goes wrong during the wait handler
212+ utils .SetAndLogStateFields (ctx , & resp .Diagnostics , & resp .State , map [string ]interface {}{
213+ "project_id" : projectId ,
214+ "region" : region ,
215+ "bar_id" : resp .BarId ,
216+ })
217+ if resp .Diagnostics .HasError () {
218+ return
219+ }
220+ // only in case the create bar API request is synchronous: just log the bar id field instead
159221 ctx = tflog .SetField (ctx , "bar_id" , resp .BarId )
160222
161- // Map response body to schema
223+ // only in case the create bar API call is asynchronous: use a wait handler to wait for the create process to complete
224+ barResp , err := wait .CreateBarWaitHandler (ctx , r .client , projectId , region , resp .BarId ).WaitWithContext (ctx )
225+ if err != nil {
226+ core .LogAndAddError (ctx , & resp .Diagnostics , "Error creating bar" , fmt .Sprintf ("Bar creation waiting: %v" , err ))
227+ return
228+ }
229+
230+ // No matter if the API request is synchronous or asynchronous: Map response body to schema
162231 err = mapFields (resp , & model )
163232 if err != nil {
164233 core .LogAndAddError (ctx , & resp .Diagnostics , "Error creating bar" , fmt .Sprintf ("Processing API payload: %v" , err ))
165234 return
166235 }
167- diags = resp .State .Set (ctx , model )
168- resp .Diagnostics .Append (diags ... )
236+ resp .Diagnostics .Append (resp .State .Set (ctx , model )... )
169237 if resp .Diagnostics .HasError () {
170238 return
171239 }
@@ -175,17 +243,18 @@ func (r *barResource) Create(ctx context.Context, req resource.CreateRequest, re
175243// Read refreshes the Terraform state with the latest data.
176244func (r * barResource ) Read (ctx context.Context , req resource.ReadRequest , resp * resource.ReadResponse ) { // nolint:gocritic // function signature required by Terraform
177245 var model Model
178- diags := req .State .Get (ctx , & model )
179- resp .Diagnostics .Append (diags ... )
246+ resp .Diagnostics .Append (req .State .Get (ctx , & model )... )
180247 if resp .Diagnostics .HasError () {
181248 return
182249 }
183250 projectId := model .ProjectId .ValueString ()
251+ region := r .providerData .GetRegionWithOverride (model .Region )
184252 barId := model .BarId .ValueString ()
185253 ctx = tflog .SetField (ctx , "project_id" , projectId )
254+ ctx = tflog .SetField (ctx , "region" , region )
186255 ctx = tflog .SetField (ctx , "bar_id" , barId )
187256
188- barResp , err := r .client .GetBar (ctx , projectId , barId ).Execute ()
257+ barResp , err := r .client .GetBar (ctx , projectId , region , barId ).Execute ()
189258 if err != nil {
190259 core .LogAndAddError (ctx , & resp .Diagnostics , "Error reading bar" , fmt .Sprintf ("Calling API: %v" , err ))
191260 return
@@ -199,8 +268,7 @@ func (r *barResource) Read(ctx context.Context, req resource.ReadRequest, resp *
199268 }
200269
201270 // Set refreshed state
202- diags = resp .State .Set (ctx , model )
203- resp .Diagnostics .Append (diags ... )
271+ resp .Diagnostics .Append (resp .State .Set (ctx , model )... )
204272 if resp .Diagnostics .HasError () {
205273 return
206274 }
@@ -209,7 +277,7 @@ func (r *barResource) Read(ctx context.Context, req resource.ReadRequest, resp *
209277
210278// Update updates the resource and sets the updated Terraform state on success.
211279func (r * barResource ) Update (ctx context.Context , _ resource.UpdateRequest , resp * resource.UpdateResponse ) { // nolint:gocritic // function signature required by Terraform
212- // Similar to Create method, calls r.client.UpdateBar instead
280+ // Similar to Create method, calls r.client.UpdateBar (and wait.UpdateBarWaitHandler if needed) instead
213281}
214282
215283// Delete deletes the resource and removes the Terraform state on success.
@@ -221,33 +289,43 @@ func (r *barResource) Delete(ctx context.Context, req resource.DeleteRequest, re
221289 return
222290 }
223291 projectId := model .ProjectId .ValueString ()
292+ region := model .Region .ValueString ()
224293 barId := model .BarId .ValueString ()
225294 ctx = tflog .SetField (ctx , "project_id" , projectId )
295+ ctx = tflog .SetField (ctx , "region" , region )
226296 ctx = tflog .SetField (ctx , "bar_id" , barId )
227297
228298 // Delete existing bar
229- _ , err := r .client .DeleteBar (ctx , projectId , barId ).Execute ()
299+ _ , err := r .client .DeleteBar (ctx , projectId , region , barId ).Execute ()
230300 if err != nil {
231301 core .LogAndAddError (ctx , & resp .Diagnostics , "Error deleting bar" , fmt .Sprintf ("Calling API: %v" , err ))
232302 }
233303
304+ // only in case the bar delete API endpoint is asynchronous: use a wait handler to wait for the delete operation to complete
305+ _ , err = wait .DeleteBarWaitHandler (ctx , r .client , projectId , region , barId ).WaitWithContext (ctx )
306+ if err != nil {
307+ core .LogAndAddError (ctx , & resp .Diagnostics , "Error deleting bar" , fmt .Sprintf ("Bar deletion waiting: %v" , err ))
308+ return
309+ }
310+
234311 tflog .Info (ctx , "Foo bar deleted" )
235312}
236313
237314// ImportState imports a resource into the Terraform state on success.
238315// The expected format of the bar resource import identifier is: project_id,bar_id
239316func (r * barResource ) ImportState (ctx context.Context , req resource.ImportStateRequest , resp * resource.ImportStateResponse ) {
240317 idParts := strings .Split (req .ID , core .Separator )
241- if len (idParts ) != 2 || idParts [0 ] == "" || idParts [1 ] == "" {
318+ if len (idParts ) != 3 || idParts [0 ] == "" || idParts [1 ] == "" || idParts [ 2 ] == "" {
242319 core .LogAndAddError (ctx , & resp .Diagnostics ,
243320 "Error importing bar" ,
244- fmt .Sprintf ("Expected import identifier with format [project_id],[bar_id], got %q" , req .ID ),
321+ fmt .Sprintf ("Expected import identifier with format [project_id],[region],[ bar_id], got %q" , req .ID ),
245322 )
246323 return
247324 }
248325
249326 resp .Diagnostics .Append (resp .State .SetAttribute (ctx , path .Root ("project_id" ), idParts [0 ])... )
250- resp .Diagnostics .Append (resp .State .SetAttribute (ctx , path .Root ("bar_id" ), idParts [1 ])... )
327+ resp .Diagnostics .Append (resp .State .SetAttribute (ctx , path .Root ("region" ), idParts [1 ])... )
328+ resp .Diagnostics .Append (resp .State .SetAttribute (ctx , path .Root ("bar_id" ), idParts [2 ])... )
251329 tflog .Info (ctx , "Foo bar state imported" )
252330}
253331
@@ -265,7 +343,11 @@ func mapFields(barResp *foo.GetBarResponse, model *Model) error {
265343 bar := barResp .Bar
266344 model .BarId = types .StringPointerValue (bar .BarId )
267345
268- model .Id = utils .BuildInternalTerraformId (model .ProjectId .ValueString (), model .BarId .ValueString ())
346+ model .Id = utils .BuildInternalTerraformId (
347+ model .ProjectId .ValueString (),
348+ model .Region .ValueString (),
349+ model .BarId .ValueString (),
350+ )
269351
270352 model .MyRequiredField = types .StringPointerValue (bar .MyRequiredField )
271353 model .MyOptionalField = types .StringPointerValue (bar .MyOptionalField )
0 commit comments