55 "fmt"
66 "strings"
77
8- "github.com/hashicorp/terraform-plugin-framework/path"
98 "github.com/hashicorp/terraform-plugin-framework/resource"
109 "github.com/hashicorp/terraform-plugin-framework/resource/schema"
1110 "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
@@ -18,7 +17,8 @@ import (
1817 fooUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/foo/utils"
1918 "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils"
2019
21- "github.com/stackitcloud/stackit-sdk-go/services/foo" // Import service "foo" from the STACKIT SDK for Go
20+ "github.com/stackitcloud/stackit-sdk-go/services/foo" // Import service "foo" from the STACKIT SDK for Go
21+ "github.com/stackitcloud/stackit-sdk-go/services/foo/wait" // Import service "foo" waiters from the STACKIT SDK for Go (in case the service API has asynchronous endpoints)
2222 // (...)
2323)
2424
@@ -27,13 +27,15 @@ var (
2727 _ resource.Resource = & barResource {}
2828 _ resource.ResourceWithConfigure = & barResource {}
2929 _ resource.ResourceWithImportState = & barResource {}
30+ _ resource.ResourceWithModifyPlan = & barResource {} // not needed for global APIs
3031)
3132
32- // Provider's internal model
33+ // Model is the internal model of the terraform resource
3334type Model struct {
3435 Id types.String `tfsdk:"id"` // needed by TF
3536 ProjectId types.String `tfsdk:"project_id"`
3637 BarId types.String `tfsdk:"bar_id"`
38+ Region types.String `tfsdk:"region"`
3739 MyRequiredField types.String `tfsdk:"my_required_field"`
3840 MyOptionalField types.String `tfsdk:"my_optional_field"`
3941 MyReadOnlyField types.String `tfsdk:"my_read_only_field"`
@@ -46,14 +48,46 @@ func NewBarResource() resource.Resource {
4648
4749// barResource is the resource implementation.
4850type barResource struct {
49- client * foo.APIClient
51+ client * foo.APIClient
52+ providerData core.ProviderData // not needed for global APIs
5053}
5154
5255// Metadata returns the resource type name.
5356func (r * barResource ) Metadata (_ context.Context , req resource.MetadataRequest , resp * resource.MetadataResponse ) {
5457 resp .TypeName = req .ProviderTypeName + "_foo_bar"
5558}
5659
60+ // ModifyPlan implements resource.ResourceWithModifyPlan.
61+ // Use the modifier to set the effective region in the current plan. - FYI: This isn't needed for global APIs.
62+ func (r * barResource ) ModifyPlan (ctx context.Context , req resource.ModifyPlanRequest , resp * resource.ModifyPlanResponse ) { // nolint:gocritic // function signature required by Terraform
63+ // FYI: the ModifyPlan implementation is not needed for global APIs
64+ var configModel Model
65+ // skip initial empty configuration to avoid follow-up errors
66+ if req .Config .Raw .IsNull () {
67+ return
68+ }
69+ resp .Diagnostics .Append (req .Config .Get (ctx , & configModel )... )
70+ if resp .Diagnostics .HasError () {
71+ return
72+ }
73+
74+ var planModel Model
75+ resp .Diagnostics .Append (req .Plan .Get (ctx , & planModel )... )
76+ if resp .Diagnostics .HasError () {
77+ return
78+ }
79+
80+ utils .AdaptRegion (ctx , configModel .Region , & planModel .Region , r .providerData .GetRegion (), resp )
81+ if resp .Diagnostics .HasError () {
82+ return
83+ }
84+
85+ resp .Diagnostics .Append (resp .Plan .Set (ctx , planModel )... )
86+ if resp .Diagnostics .HasError () {
87+ return
88+ }
89+ }
90+
5791// Configure adds the provider configured client to the resource.
5892func (r * barResource ) Configure (ctx context.Context , req resource.ConfigureRequest , resp * resource.ConfigureResponse ) {
5993 providerData , ok := conversion .ParseProviderData (ctx , req .ProviderData , & resp .Diagnostics )
@@ -76,6 +110,7 @@ func (r *barResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *
76110 "id" : "Terraform's internal resource identifier. It is structured as \" `project_id`,`bar_id`\" ." ,
77111 "project_id" : "STACKIT Project ID to which the bar is associated." ,
78112 "bar_id" : "The bar ID." ,
113+ "region" : "The resource region. If not defined, the provider region is used." ,
79114 "my_required_field" : "My required field description." ,
80115 "my_optional_field" : "My optional field description." ,
81116 "my_read_only_field" : "My read-only field description." ,
@@ -108,6 +143,15 @@ func (r *barResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *
108143 Description : descriptions ["bar_id" ],
109144 Computed : true ,
110145 },
146+ "region" : schema.StringAttribute { // not needed for global APIs
147+ Optional : true ,
148+ // must be computed to allow for storing the override value from the provider
149+ Computed : true ,
150+ Description : descriptions ["region" ],
151+ PlanModifiers : []planmodifier.String {
152+ stringplanmodifier .RequiresReplace (),
153+ },
154+ },
111155 "my_required_field" : schema.StringAttribute {
112156 Description : descriptions ["my_required_field" ],
113157 Required : true ,
@@ -136,36 +180,57 @@ func (r *barResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *
136180// Create creates the resource and sets the initial Terraform state.
137181func (r * barResource ) Create (ctx context.Context , req resource.CreateRequest , resp * resource.CreateResponse ) { // nolint:gocritic // function signature required by Terraform
138182 var model Model
139- diags := req .Plan .Get (ctx , & model )
140- resp .Diagnostics .Append (diags ... )
183+ resp .Diagnostics .Append (req .Plan .Get (ctx , & model )... )
141184 if resp .Diagnostics .HasError () {
142185 return
143186 }
144187 projectId := model .ProjectId .ValueString ()
145- barId := model .BarId .ValueString ()
188+ region := model .Region .ValueString () // not needed for global APIs
146189 ctx = tflog .SetField (ctx , "project_id" , projectId )
190+ ctx = tflog .SetField (ctx , "region" , region )
147191
148- // Create new bar
192+ // prepare the payload struct for the create bar request
149193 payload , err := toCreatePayload (& model )
150194 if err != nil {
151195 core .LogAndAddError (ctx , & resp .Diagnostics , "Error creating credential" , fmt .Sprintf ("Creating API payload: %v" , err ))
152196 return
153197 }
154- resp , err := r .client .CreateBar (ctx , projectId , barId ).CreateBarPayload (* payload ).Execute ()
198+
199+ // Create new bar
200+ barResp , err := r .client .CreateBar (ctx , projectId , region ).CreateBarPayload (* payload ).Execute ()
155201 if err != nil {
156202 core .LogAndAddError (ctx , & resp .Diagnostics , "Error creating bar" , fmt .Sprintf ("Calling API: %v" , err ))
157203 return
158204 }
205+
206+ // only in case the create bar API call is asynchronous (Make sure to include *ALL* fields which are part of the
207+ // internal terraform resource id! And please include the comment below in your code):
208+ // Write id attributes to state before polling via the wait handler - just in case anything goes wrong during the wait handler
209+ utils .SetAndLogStateFields (ctx , & resp .Diagnostics , & resp .State , map [string ]interface {}{
210+ "project_id" : projectId ,
211+ "region" : region ,
212+ "bar_id" : resp .BarId ,
213+ })
214+ if resp .Diagnostics .HasError () {
215+ return
216+ }
217+ // only in case the create bar API request is synchronous: just log the bar id field instead
159218 ctx = tflog .SetField (ctx , "bar_id" , resp .BarId )
160219
161- // Map response body to schema
220+ // only in case the create bar API call is asynchronous: use a wait handler to wait for the create process to complete
221+ barResp , err := wait .CreateBarWaitHandler (ctx , r .client , projectId , region , resp .BarId ).WaitWithContext (ctx )
222+ if err != nil {
223+ core .LogAndAddError (ctx , & resp .Diagnostics , "Error creating bar" , fmt .Sprintf ("Bar creation waiting: %v" , err ))
224+ return
225+ }
226+
227+ // No matter if the API request is synchronous or asynchronous: Map response body to schema
162228 err = mapFields (resp , & model )
163229 if err != nil {
164230 core .LogAndAddError (ctx , & resp .Diagnostics , "Error creating bar" , fmt .Sprintf ("Processing API payload: %v" , err ))
165231 return
166232 }
167- diags = resp .State .Set (ctx , model )
168- resp .Diagnostics .Append (diags ... )
233+ resp .Diagnostics .Append (resp .State .Set (ctx , model )... )
169234 if resp .Diagnostics .HasError () {
170235 return
171236 }
@@ -175,17 +240,18 @@ func (r *barResource) Create(ctx context.Context, req resource.CreateRequest, re
175240// Read refreshes the Terraform state with the latest data.
176241func (r * barResource ) Read (ctx context.Context , req resource.ReadRequest , resp * resource.ReadResponse ) { // nolint:gocritic // function signature required by Terraform
177242 var model Model
178- diags := req .State .Get (ctx , & model )
179- resp .Diagnostics .Append (diags ... )
243+ resp .Diagnostics .Append (req .State .Get (ctx , & model )... )
180244 if resp .Diagnostics .HasError () {
181245 return
182246 }
183247 projectId := model .ProjectId .ValueString ()
248+ region := r .providerData .GetRegionWithOverride (model .Region )
184249 barId := model .BarId .ValueString ()
185250 ctx = tflog .SetField (ctx , "project_id" , projectId )
251+ ctx = tflog .SetField (ctx , "region" , region )
186252 ctx = tflog .SetField (ctx , "bar_id" , barId )
187253
188- barResp , err := r .client .GetBar (ctx , projectId , barId ).Execute ()
254+ barResp , err := r .client .GetBar (ctx , projectId , region , barId ).Execute ()
189255 if err != nil {
190256 core .LogAndAddError (ctx , & resp .Diagnostics , "Error reading bar" , fmt .Sprintf ("Calling API: %v" , err ))
191257 return
@@ -199,8 +265,7 @@ func (r *barResource) Read(ctx context.Context, req resource.ReadRequest, resp *
199265 }
200266
201267 // Set refreshed state
202- diags = resp .State .Set (ctx , model )
203- resp .Diagnostics .Append (diags ... )
268+ resp .Diagnostics .Append (resp .State .Set (ctx , model )... )
204269 if resp .Diagnostics .HasError () {
205270 return
206271 }
@@ -209,7 +274,7 @@ func (r *barResource) Read(ctx context.Context, req resource.ReadRequest, resp *
209274
210275// Update updates the resource and sets the updated Terraform state on success.
211276func (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
277+ // Similar to Create method, calls r.client.UpdateBar (and wait.UpdateBarWaitHandler if needed) instead
213278}
214279
215280// Delete deletes the resource and removes the Terraform state on success.
@@ -221,33 +286,46 @@ func (r *barResource) Delete(ctx context.Context, req resource.DeleteRequest, re
221286 return
222287 }
223288 projectId := model .ProjectId .ValueString ()
289+ region := model .Region .ValueString ()
224290 barId := model .BarId .ValueString ()
225291 ctx = tflog .SetField (ctx , "project_id" , projectId )
292+ ctx = tflog .SetField (ctx , "region" , region )
226293 ctx = tflog .SetField (ctx , "bar_id" , barId )
227294
228295 // Delete existing bar
229- _ , err := r .client .DeleteBar (ctx , projectId , barId ).Execute ()
296+ _ , err := r .client .DeleteBar (ctx , projectId , region , barId ).Execute ()
230297 if err != nil {
231298 core .LogAndAddError (ctx , & resp .Diagnostics , "Error deleting bar" , fmt .Sprintf ("Calling API: %v" , err ))
232299 }
233300
301+ // only in case the bar delete API endpoint is asynchronous: use a wait handler to wait for the delete operation to complete
302+ _ , err = wait .DeleteBarWaitHandler (ctx , r .client , projectId , region , barId ).WaitWithContext (ctx )
303+ if err != nil {
304+ core .LogAndAddError (ctx , & resp .Diagnostics , "Error deleting bar" , fmt .Sprintf ("Bar deletion waiting: %v" , err ))
305+ return
306+ }
307+
234308 tflog .Info (ctx , "Foo bar deleted" )
235309}
236310
237311// ImportState imports a resource into the Terraform state on success.
238312// The expected format of the bar resource import identifier is: project_id,bar_id
239313func (r * barResource ) ImportState (ctx context.Context , req resource.ImportStateRequest , resp * resource.ImportStateResponse ) {
240314 idParts := strings .Split (req .ID , core .Separator )
241- if len (idParts ) != 2 || idParts [0 ] == "" || idParts [1 ] == "" {
315+ if len (idParts ) != 3 || idParts [0 ] == "" || idParts [1 ] == "" || idParts [ 2 ] == "" {
242316 core .LogAndAddError (ctx , & resp .Diagnostics ,
243317 "Error importing bar" ,
244- fmt .Sprintf ("Expected import identifier with format [project_id],[bar_id], got %q" , req .ID ),
318+ fmt .Sprintf ("Expected import identifier with format [project_id],[region],[ bar_id], got %q" , req .ID ),
245319 )
246320 return
247321 }
248322
249- 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 ])... )
323+ utils .SetAndLogStateFields (ctx , & resp .Diagnostics , & resp .State , map [string ]any {
324+ "project_id" : idParts [0 ],
325+ "region" : idParts [1 ],
326+ "bar_id" : idParts [2 ],
327+ })
328+
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