Skip to content

Commit b11892c

Browse files
committed
feat: add skip external request
1 parent 6c4f815 commit b11892c

File tree

7 files changed

+348
-31
lines changed

7 files changed

+348
-31
lines changed

README.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -163,13 +163,13 @@ Please ensure that the `MarkdownDescription` field is set in the schema for each
163163
To generate the documentation run either:
164164

165165
```sh
166-
$ make docs
166+
make docs
167167
```
168168

169169
or...
170170

171171
```sh
172-
$ go generate ./...
172+
go generate ./...
173173
```
174174

175175
### Templates
@@ -210,6 +210,16 @@ provider_installation {
210210
}
211211
```
212212

213+
## Developer: Using the `skip_on` struct field tag
214+
215+
The `skip_on` struct field tag is used to skip the external API call when only attributes that affect the internal state are modified, e.g. retry configuration. The `skip_on` struct field tag is used to skip the external API call when only attributes that affect the internal state are modified, e.g. retry configuration. The `skip_on` struct field tag is a comma-separated list of operations that must be met in order to skip the field.
216+
217+
The provider will compare the state with the plan, and check for changes. If the only fields to me modified are those with the `skip_on` struct field tag set to the supplied operation, e.g. `update`, the provider will skip the external API call.
218+
219+
The following operations are supported:
220+
221+
* `update` - Skip the external API call when the operation is an update.
222+
213223
## Credits
214224

215225
We wish to thank HashiCorp for the use of some MPLv2-licensed code from their open source project [terraform-provider-azurerm](https://github.com/hashicorp/terraform-provider-azurerm).

internal/services/azapi_data_plane_resource.go

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"github.com/Azure/terraform-provider-azapi/internal/services/myplanmodifier/planmodifierdynamic"
2020
"github.com/Azure/terraform-provider-azapi/internal/services/myvalidator"
2121
"github.com/Azure/terraform-provider-azapi/internal/services/parse"
22+
"github.com/Azure/terraform-provider-azapi/internal/services/skip"
2223
"github.com/Azure/terraform-provider-azapi/internal/tf"
2324
"github.com/Azure/terraform-provider-azapi/utils"
2425
"github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts"
@@ -48,19 +49,19 @@ type DataPlaneResourceModel struct {
4849
ReplaceTriggersExternalValues types.Dynamic `tfsdk:"replace_triggers_external_values"`
4950
ReplaceTriggersRefs types.List `tfsdk:"replace_triggers_refs"`
5051
ResponseExportValues types.Dynamic `tfsdk:"response_export_values"`
51-
Retry retry.RetryValue `tfsdk:"retry"`
52-
RetryReadAfterCreate retry.RetryValue `tfsdk:"retry_read_after_create"`
52+
Retry retry.RetryValue `tfsdk:"retry" skip_on:"update"`
53+
RetryReadAfterCreate retry.RetryValue `tfsdk:"retry_read_after_create" skip_on:"update"`
5354
Locks types.List `tfsdk:"locks"`
5455
Output types.Dynamic `tfsdk:"output"`
55-
Timeouts timeouts.Value `tfsdk:"timeouts"`
56-
CreateHeaders types.Map `tfsdk:"create_headers"`
57-
CreateQueryParameters types.Map `tfsdk:"create_query_parameters"`
56+
Timeouts timeouts.Value `tfsdk:"timeouts" skip_on:"update"`
57+
CreateHeaders types.Map `tfsdk:"create_headers" skip_on:"update"`
58+
CreateQueryParameters types.Map `tfsdk:"create_query_parameters" skip_on:"update"`
5859
UpdateHeaders types.Map `tfsdk:"update_headers"`
5960
UpdateQueryParameters types.Map `tfsdk:"update_query_parameters"`
60-
DeleteHeaders types.Map `tfsdk:"delete_headers"`
61-
DeleteQueryParameters types.Map `tfsdk:"delete_query_parameters"`
62-
ReadHeaders types.Map `tfsdk:"read_headers"`
63-
ReadQueryParameters types.Map `tfsdk:"read_query_parameters"`
61+
DeleteHeaders types.Map `tfsdk:"delete_headers" skip_on:"update"`
62+
DeleteQueryParameters types.Map `tfsdk:"delete_query_parameters" skip_on:"update"`
63+
ReadHeaders types.Map `tfsdk:"read_headers" skip_on:"update"`
64+
ReadQueryParameters types.Map `tfsdk:"read_query_parameters" skip_on:"update"`
6465
}
6566

6667
type DataPlaneResource struct {
@@ -359,6 +360,15 @@ func (r *DataPlaneResource) Create(ctx context.Context, request resource.CreateR
359360
}
360361

361362
func (r *DataPlaneResource) Update(ctx context.Context, request resource.UpdateRequest, response *resource.UpdateResponse) {
363+
// See if we can skip the external API call (changes are to state only)
364+
var state, plan DataPlaneResourceModel
365+
request.Plan.Get(ctx, &plan)
366+
request.State.Get(ctx, &state)
367+
if skip.CanSkipExternalRequest(state, plan, "update") {
368+
tflog.Debug(ctx, "azapi_resource.CreateUpdate skipping external request as no unskippable changes were detected")
369+
response.Diagnostics.Append(response.State.Set(ctx, plan)...)
370+
}
371+
tflog.Debug(ctx, "azapi_resource.CreateUpdate proceeding with external request as no skippable changes were detected")
362372
r.CreateUpdate(ctx, request.Plan, &response.State, &response.Diagnostics)
363373
}
364374

internal/services/azapi_resource.go

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
"github.com/Azure/terraform-provider-azapi/internal/services/myvalidator"
2828
"github.com/Azure/terraform-provider-azapi/internal/services/parse"
2929
"github.com/Azure/terraform-provider-azapi/internal/services/preflight"
30+
"github.com/Azure/terraform-provider-azapi/internal/services/skip"
3031
"github.com/Azure/terraform-provider-azapi/internal/tf"
3132
"github.com/Azure/terraform-provider-azapi/utils"
3233
"github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts"
@@ -62,20 +63,20 @@ type AzapiResourceModel struct {
6263
ReplaceTriggersExternalValues types.Dynamic `tfsdk:"replace_triggers_external_values"`
6364
ReplaceTriggersRefs types.List `tfsdk:"replace_triggers_refs"`
6465
ResponseExportValues types.Dynamic `tfsdk:"response_export_values"`
65-
Retry retry.RetryValue `tfsdk:"retry"`
66-
RetryReadAfterCreate retry.RetryValue `tfsdk:"retry_read_after_create"`
66+
Retry retry.RetryValue `tfsdk:"retry" skip_on:"update"`
67+
RetryReadAfterCreate retry.RetryValue `tfsdk:"retry_read_after_create" skip_on:"update"`
6768
SchemaValidationEnabled types.Bool `tfsdk:"schema_validation_enabled"`
6869
Tags types.Map `tfsdk:"tags"`
69-
Timeouts timeouts.Value `tfsdk:"timeouts"`
70+
Timeouts timeouts.Value `tfsdk:"timeouts" skip_on:"update"`
7071
Type types.String `tfsdk:"type"`
71-
CreateHeaders types.Map `tfsdk:"create_headers"`
72-
CreateQueryParameters types.Map `tfsdk:"create_query_parameters"`
72+
CreateHeaders types.Map `tfsdk:"create_headers" skip_on:"update"`
73+
CreateQueryParameters types.Map `tfsdk:"create_query_parameters" skip_on:"update"`
7374
UpdateHeaders types.Map `tfsdk:"update_headers"`
7475
UpdateQueryParameters types.Map `tfsdk:"update_query_parameters"`
75-
DeleteHeaders types.Map `tfsdk:"delete_headers"`
76-
DeleteQueryParameters types.Map `tfsdk:"delete_query_parameters"`
77-
ReadHeaders types.Map `tfsdk:"read_headers"`
78-
ReadQueryParameters types.Map `tfsdk:"read_query_parameters"`
76+
DeleteHeaders types.Map `tfsdk:"delete_headers" skip_on:"update"`
77+
DeleteQueryParameters types.Map `tfsdk:"delete_query_parameters" skip_on:"update"`
78+
ReadHeaders types.Map `tfsdk:"read_headers" skip_on:"update"`
79+
ReadQueryParameters types.Map `tfsdk:"read_query_parameters" skip_on:"update"`
7980
}
8081

8182
var _ resource.Resource = &AzapiResource{}
@@ -590,6 +591,16 @@ func (r *AzapiResource) Create(ctx context.Context, request resource.CreateReque
590591
}
591592

592593
func (r *AzapiResource) Update(ctx context.Context, request resource.UpdateRequest, response *resource.UpdateResponse) {
594+
// See if we can skip the external API call (changes are to state only)
595+
var plan, state AzapiResourceModel
596+
request.State.Get(ctx, &state)
597+
request.Plan.Get(ctx, &plan)
598+
if skip.CanSkipExternalRequest(plan, state, "update") {
599+
response.Diagnostics.Append(response.State.Set(ctx, plan)...)
600+
tflog.Debug(ctx, "azapi_resource.CreateUpdate skipping external request as no unskippable changes were detected")
601+
return
602+
}
603+
tflog.Debug(ctx, "azapi_resource.CreateUpdate proceeding with external request as no skippable changes were detected")
593604
r.CreateUpdate(ctx, request.Plan, &response.State, &response.Diagnostics)
594605
}
595606

internal/services/azapi_resource_action_resource.go

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"github.com/Azure/terraform-provider-azapi/internal/services/myplanmodifier"
1717
"github.com/Azure/terraform-provider-azapi/internal/services/myvalidator"
1818
"github.com/Azure/terraform-provider-azapi/internal/services/parse"
19+
"github.com/Azure/terraform-provider-azapi/internal/services/skip"
1920
"github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts"
2021
"github.com/hashicorp/terraform-plugin-framework-validators/listvalidator"
2122
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
@@ -44,8 +45,8 @@ type ActionResourceModel struct {
4445
SensitiveResponseExportValues types.Dynamic `tfsdk:"sensitive_response_export_values"`
4546
Output types.Dynamic `tfsdk:"output"`
4647
SensitiveOutput types.Dynamic `tfsdk:"sensitive_output"`
47-
Timeouts timeouts.Value `tfsdk:"timeouts"`
48-
Retry retry.RetryValue `tfsdk:"retry"`
48+
Timeouts timeouts.Value `tfsdk:"timeouts" skip_on:"update"`
49+
Retry retry.RetryValue `tfsdk:"retry" skip_on:"update"`
4950
Headers types.Map `tfsdk:"headers"`
5051
QueryParameters types.Map `tfsdk:"query_parameters"`
5152
}
@@ -279,20 +280,31 @@ func (r *ActionResource) Create(ctx context.Context, request resource.CreateRequ
279280
}
280281

281282
func (r *ActionResource) Update(ctx context.Context, request resource.UpdateRequest, response *resource.UpdateResponse) {
282-
var model ActionResourceModel
283-
if response.Diagnostics.Append(request.Plan.Get(ctx, &model)...); response.Diagnostics.HasError() {
283+
var state, plan ActionResourceModel
284+
if response.Diagnostics.Append(request.Plan.Get(ctx, &plan)...); response.Diagnostics.HasError() {
284285
return
285286
}
286287

287-
timeout, diags := model.Timeouts.Update(ctx, 30*time.Minute)
288+
request.State.Get(ctx, &state)
289+
request.Plan.Get(ctx, &plan)
290+
291+
timeout, diags := plan.Timeouts.Update(ctx, 30*time.Minute)
288292
if response.Diagnostics.Append(diags...); response.Diagnostics.HasError() {
289293
return
290294
}
291295
ctx, cancel := context.WithTimeout(ctx, timeout)
292296
defer cancel()
293297

294-
if model.When.ValueString() == "apply" {
295-
r.Action(ctx, model, &response.State, &response.Diagnostics)
298+
// See if we can skip the external API call (changes are to state only)
299+
if skip.CanSkipExternalRequest(state, plan, "update") {
300+
tflog.Debug(ctx, "azapi_resource.CreateUpdate skipping external request as no unskippable changes were detected")
301+
response.Diagnostics.Append(response.State.Set(ctx, plan)...)
302+
return
303+
}
304+
tflog.Debug(ctx, "azapi_resource.CreateUpdate proceeding with external request as no skippable changes were detected")
305+
306+
if plan.When.ValueString() == "apply" {
307+
r.Action(ctx, plan, &response.State, &response.Diagnostics)
296308
}
297309
}
298310

internal/services/azapi_update_resource.go

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"github.com/Azure/terraform-provider-azapi/internal/services/myplanmodifier"
1818
"github.com/Azure/terraform-provider-azapi/internal/services/myvalidator"
1919
"github.com/Azure/terraform-provider-azapi/internal/services/parse"
20+
"github.com/Azure/terraform-provider-azapi/internal/services/skip"
2021
"github.com/Azure/terraform-provider-azapi/utils"
2122
"github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts"
2223
"github.com/hashicorp/terraform-plugin-framework-validators/listvalidator"
@@ -44,12 +45,12 @@ type AzapiUpdateResourceModel struct {
4445
ResponseExportValues types.Dynamic `tfsdk:"response_export_values"`
4546
Locks types.List `tfsdk:"locks"`
4647
Output types.Dynamic `tfsdk:"output"`
47-
Timeouts timeouts.Value `tfsdk:"timeouts"`
48-
Retry retry.RetryValue `tfsdk:"retry"`
48+
Timeouts timeouts.Value `tfsdk:"timeouts" skip_on:"update"`
49+
Retry retry.RetryValue `tfsdk:"retry" skip_on:"update"`
4950
UpdateHeaders types.Map `tfsdk:"update_headers"`
5051
UpdateQueryParameters types.Map `tfsdk:"update_query_parameters"`
51-
ReadHeaders types.Map `tfsdk:"read_headers"`
52-
ReadQueryParameters types.Map `tfsdk:"read_query_parameters"`
52+
ReadHeaders types.Map `tfsdk:"read_headers" skip_on:"update"`
53+
ReadQueryParameters types.Map `tfsdk:"read_query_parameters" skip_on:"update"`
5354
}
5455

5556
type AzapiUpdateResource struct {
@@ -301,6 +302,17 @@ func (r *AzapiUpdateResource) Create(ctx context.Context, request resource.Creat
301302
}
302303

303304
func (r *AzapiUpdateResource) Update(ctx context.Context, request resource.UpdateRequest, response *resource.UpdateResponse) {
305+
// See if we can skip the external API call (changes are to state only)
306+
var plan, state AzapiUpdateResourceModel
307+
request.Plan.Get(ctx, &plan)
308+
request.State.Get(ctx, &state)
309+
if skip.CanSkipExternalRequest(plan, state, "update") {
310+
tflog.Debug(ctx, "azapi_resource.CreateUpdate skipping external request as no unskippable changes were detected")
311+
response.Diagnostics.Append(request.State.Set(ctx, plan)...)
312+
return
313+
}
314+
tflog.Debug(ctx, "azapi_resource.CreateUpdate proceeding with external request as no skippable changes were detected")
315+
304316
r.CreateUpdate(ctx, request.Plan, &response.State, &response.Diagnostics)
305317
}
306318

internal/services/skip/skip.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package skip
2+
3+
import (
4+
"reflect"
5+
"slices"
6+
"strings"
7+
)
8+
9+
// CanSkipExternalRequest checks if the external request can be skipped based on the plan and state.
10+
// Two of the same objects are supplied as parameters, together with the operation that is being performed.
11+
// The function uses the `skip_on` struct tag to determine if the field should be skipped.
12+
// The value of the `skip_on` tag is a comma-separated list of operations that mean that changes to this field value do not require an external request and are in state only.
13+
// The function will return true if the external request can be skipped, false otherwise.
14+
func CanSkipExternalRequest[T any](a, b T, operation string) bool {
15+
valA := reflect.ValueOf(a)
16+
valB := reflect.ValueOf(b)
17+
18+
// Since we are using generics, we know that the types of a and b are the same.
19+
// Therefore we can check the type of a to determine if it is a struct.
20+
if valA.Kind() != reflect.Struct {
21+
return false
22+
}
23+
24+
typeOfA := valA.Type()
25+
// iterate over all fields of the struct
26+
for i := 0; i < typeOfA.NumField(); i++ {
27+
field := typeOfA.Field(i)
28+
// Check if the field has the skip_on tag
29+
// If it doesn't we need to compare the valued as we cannot determine if the field should be skipped.
30+
// If the field has the skip_on tag, we can check if the operation is in the list of operations that should be skipped.
31+
tag := field.Tag.Get("skip_on")
32+
if tag != "" {
33+
// Split the tag values by comma and check if the operation is in the list.
34+
// If the operation is in the list, then this field represents a change in state only
35+
// and does not require an external request to be made.
36+
// Therefore we can skip tp the next field.
37+
tagValues := strings.Split(tag, ",")
38+
if slices.Contains(tagValues, operation) {
39+
continue
40+
}
41+
}
42+
43+
// If we get here then we need to compare the field values.
44+
// By now we have determined that the struct fields do not have a valid skip value for this operation.
45+
// Therefore if the field values are not equal, then the external request cannot be skipped.
46+
fieldA := valA.Field(i)
47+
fieldB := valB.Field(i)
48+
49+
if !fieldA.IsValid() || !fieldB.IsValid() {
50+
return false
51+
}
52+
if !reflect.DeepEqual(fieldA.Interface(), fieldB.Interface()) {
53+
return false
54+
}
55+
}
56+
return true
57+
}

0 commit comments

Comments
 (0)