Skip to content

Commit

Permalink
feat: add skip external request
Browse files Browse the repository at this point in the history
  • Loading branch information
matt-FFFFFF committed Feb 1, 2025
1 parent 6c4f815 commit b11892c
Show file tree
Hide file tree
Showing 7 changed files with 348 additions and 31 deletions.
14 changes: 12 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,13 +163,13 @@ Please ensure that the `MarkdownDescription` field is set in the schema for each
To generate the documentation run either:

```sh
$ make docs
make docs
```

or...

```sh
$ go generate ./...
go generate ./...
```

### Templates
Expand Down Expand Up @@ -210,6 +210,16 @@ provider_installation {
}
```

## Developer: Using the `skip_on` struct field tag

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.

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.

The following operations are supported:

* `update` - Skip the external API call when the operation is an update.

## Credits

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).
28 changes: 19 additions & 9 deletions internal/services/azapi_data_plane_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"github.com/Azure/terraform-provider-azapi/internal/services/myplanmodifier/planmodifierdynamic"
"github.com/Azure/terraform-provider-azapi/internal/services/myvalidator"
"github.com/Azure/terraform-provider-azapi/internal/services/parse"
"github.com/Azure/terraform-provider-azapi/internal/services/skip"
"github.com/Azure/terraform-provider-azapi/internal/tf"
"github.com/Azure/terraform-provider-azapi/utils"
"github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts"
Expand Down Expand Up @@ -48,19 +49,19 @@ type DataPlaneResourceModel struct {
ReplaceTriggersExternalValues types.Dynamic `tfsdk:"replace_triggers_external_values"`
ReplaceTriggersRefs types.List `tfsdk:"replace_triggers_refs"`
ResponseExportValues types.Dynamic `tfsdk:"response_export_values"`
Retry retry.RetryValue `tfsdk:"retry"`
RetryReadAfterCreate retry.RetryValue `tfsdk:"retry_read_after_create"`
Retry retry.RetryValue `tfsdk:"retry" skip_on:"update"`
RetryReadAfterCreate retry.RetryValue `tfsdk:"retry_read_after_create" skip_on:"update"`
Locks types.List `tfsdk:"locks"`
Output types.Dynamic `tfsdk:"output"`
Timeouts timeouts.Value `tfsdk:"timeouts"`
CreateHeaders types.Map `tfsdk:"create_headers"`
CreateQueryParameters types.Map `tfsdk:"create_query_parameters"`
Timeouts timeouts.Value `tfsdk:"timeouts" skip_on:"update"`
CreateHeaders types.Map `tfsdk:"create_headers" skip_on:"update"`
CreateQueryParameters types.Map `tfsdk:"create_query_parameters" skip_on:"update"`
UpdateHeaders types.Map `tfsdk:"update_headers"`
UpdateQueryParameters types.Map `tfsdk:"update_query_parameters"`
DeleteHeaders types.Map `tfsdk:"delete_headers"`
DeleteQueryParameters types.Map `tfsdk:"delete_query_parameters"`
ReadHeaders types.Map `tfsdk:"read_headers"`
ReadQueryParameters types.Map `tfsdk:"read_query_parameters"`
DeleteHeaders types.Map `tfsdk:"delete_headers" skip_on:"update"`
DeleteQueryParameters types.Map `tfsdk:"delete_query_parameters" skip_on:"update"`
ReadHeaders types.Map `tfsdk:"read_headers" skip_on:"update"`
ReadQueryParameters types.Map `tfsdk:"read_query_parameters" skip_on:"update"`
}

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

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

Expand Down
29 changes: 20 additions & 9 deletions internal/services/azapi_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
"github.com/Azure/terraform-provider-azapi/internal/services/myvalidator"
"github.com/Azure/terraform-provider-azapi/internal/services/parse"
"github.com/Azure/terraform-provider-azapi/internal/services/preflight"
"github.com/Azure/terraform-provider-azapi/internal/services/skip"
"github.com/Azure/terraform-provider-azapi/internal/tf"
"github.com/Azure/terraform-provider-azapi/utils"
"github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts"
Expand Down Expand Up @@ -62,20 +63,20 @@ type AzapiResourceModel struct {
ReplaceTriggersExternalValues types.Dynamic `tfsdk:"replace_triggers_external_values"`
ReplaceTriggersRefs types.List `tfsdk:"replace_triggers_refs"`
ResponseExportValues types.Dynamic `tfsdk:"response_export_values"`
Retry retry.RetryValue `tfsdk:"retry"`
RetryReadAfterCreate retry.RetryValue `tfsdk:"retry_read_after_create"`
Retry retry.RetryValue `tfsdk:"retry" skip_on:"update"`
RetryReadAfterCreate retry.RetryValue `tfsdk:"retry_read_after_create" skip_on:"update"`
SchemaValidationEnabled types.Bool `tfsdk:"schema_validation_enabled"`
Tags types.Map `tfsdk:"tags"`
Timeouts timeouts.Value `tfsdk:"timeouts"`
Timeouts timeouts.Value `tfsdk:"timeouts" skip_on:"update"`
Type types.String `tfsdk:"type"`
CreateHeaders types.Map `tfsdk:"create_headers"`
CreateQueryParameters types.Map `tfsdk:"create_query_parameters"`
CreateHeaders types.Map `tfsdk:"create_headers" skip_on:"update"`
CreateQueryParameters types.Map `tfsdk:"create_query_parameters" skip_on:"update"`
UpdateHeaders types.Map `tfsdk:"update_headers"`
UpdateQueryParameters types.Map `tfsdk:"update_query_parameters"`
DeleteHeaders types.Map `tfsdk:"delete_headers"`
DeleteQueryParameters types.Map `tfsdk:"delete_query_parameters"`
ReadHeaders types.Map `tfsdk:"read_headers"`
ReadQueryParameters types.Map `tfsdk:"read_query_parameters"`
DeleteHeaders types.Map `tfsdk:"delete_headers" skip_on:"update"`
DeleteQueryParameters types.Map `tfsdk:"delete_query_parameters" skip_on:"update"`
ReadHeaders types.Map `tfsdk:"read_headers" skip_on:"update"`
ReadQueryParameters types.Map `tfsdk:"read_query_parameters" skip_on:"update"`
}

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

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

Expand Down
26 changes: 19 additions & 7 deletions internal/services/azapi_resource_action_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"github.com/Azure/terraform-provider-azapi/internal/services/myplanmodifier"
"github.com/Azure/terraform-provider-azapi/internal/services/myvalidator"
"github.com/Azure/terraform-provider-azapi/internal/services/parse"
"github.com/Azure/terraform-provider-azapi/internal/services/skip"
"github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts"
"github.com/hashicorp/terraform-plugin-framework-validators/listvalidator"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
Expand Down Expand Up @@ -44,8 +45,8 @@ type ActionResourceModel struct {
SensitiveResponseExportValues types.Dynamic `tfsdk:"sensitive_response_export_values"`
Output types.Dynamic `tfsdk:"output"`
SensitiveOutput types.Dynamic `tfsdk:"sensitive_output"`
Timeouts timeouts.Value `tfsdk:"timeouts"`
Retry retry.RetryValue `tfsdk:"retry"`
Timeouts timeouts.Value `tfsdk:"timeouts" skip_on:"update"`
Retry retry.RetryValue `tfsdk:"retry" skip_on:"update"`
Headers types.Map `tfsdk:"headers"`
QueryParameters types.Map `tfsdk:"query_parameters"`
}
Expand Down Expand Up @@ -279,20 +280,31 @@ func (r *ActionResource) Create(ctx context.Context, request resource.CreateRequ
}

func (r *ActionResource) Update(ctx context.Context, request resource.UpdateRequest, response *resource.UpdateResponse) {
var model ActionResourceModel
if response.Diagnostics.Append(request.Plan.Get(ctx, &model)...); response.Diagnostics.HasError() {
var state, plan ActionResourceModel
if response.Diagnostics.Append(request.Plan.Get(ctx, &plan)...); response.Diagnostics.HasError() {
return
}

timeout, diags := model.Timeouts.Update(ctx, 30*time.Minute)
request.State.Get(ctx, &state)
request.Plan.Get(ctx, &plan)

timeout, diags := plan.Timeouts.Update(ctx, 30*time.Minute)
if response.Diagnostics.Append(diags...); response.Diagnostics.HasError() {
return
}
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()

if model.When.ValueString() == "apply" {
r.Action(ctx, model, &response.State, &response.Diagnostics)
// See if we can skip the external API call (changes are to state only)
if skip.CanSkipExternalRequest(state, plan, "update") {
tflog.Debug(ctx, "azapi_resource.CreateUpdate skipping external request as no unskippable changes were detected")
response.Diagnostics.Append(response.State.Set(ctx, plan)...)
return
}
tflog.Debug(ctx, "azapi_resource.CreateUpdate proceeding with external request as no skippable changes were detected")

if plan.When.ValueString() == "apply" {
r.Action(ctx, plan, &response.State, &response.Diagnostics)
}
}

Expand Down
20 changes: 16 additions & 4 deletions internal/services/azapi_update_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"github.com/Azure/terraform-provider-azapi/internal/services/myplanmodifier"
"github.com/Azure/terraform-provider-azapi/internal/services/myvalidator"
"github.com/Azure/terraform-provider-azapi/internal/services/parse"
"github.com/Azure/terraform-provider-azapi/internal/services/skip"
"github.com/Azure/terraform-provider-azapi/utils"
"github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts"
"github.com/hashicorp/terraform-plugin-framework-validators/listvalidator"
Expand Down Expand Up @@ -44,12 +45,12 @@ type AzapiUpdateResourceModel struct {
ResponseExportValues types.Dynamic `tfsdk:"response_export_values"`
Locks types.List `tfsdk:"locks"`
Output types.Dynamic `tfsdk:"output"`
Timeouts timeouts.Value `tfsdk:"timeouts"`
Retry retry.RetryValue `tfsdk:"retry"`
Timeouts timeouts.Value `tfsdk:"timeouts" skip_on:"update"`
Retry retry.RetryValue `tfsdk:"retry" skip_on:"update"`
UpdateHeaders types.Map `tfsdk:"update_headers"`
UpdateQueryParameters types.Map `tfsdk:"update_query_parameters"`
ReadHeaders types.Map `tfsdk:"read_headers"`
ReadQueryParameters types.Map `tfsdk:"read_query_parameters"`
ReadHeaders types.Map `tfsdk:"read_headers" skip_on:"update"`
ReadQueryParameters types.Map `tfsdk:"read_query_parameters" skip_on:"update"`
}

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

func (r *AzapiUpdateResource) Update(ctx context.Context, request resource.UpdateRequest, response *resource.UpdateResponse) {
// See if we can skip the external API call (changes are to state only)
var plan, state AzapiUpdateResourceModel
request.Plan.Get(ctx, &plan)
request.State.Get(ctx, &state)
if skip.CanSkipExternalRequest(plan, state, "update") {
tflog.Debug(ctx, "azapi_resource.CreateUpdate skipping external request as no unskippable changes were detected")
response.Diagnostics.Append(request.State.Set(ctx, plan)...)
return
}
tflog.Debug(ctx, "azapi_resource.CreateUpdate proceeding with external request as no skippable changes were detected")

r.CreateUpdate(ctx, request.Plan, &response.State, &response.Diagnostics)
}

Expand Down
57 changes: 57 additions & 0 deletions internal/services/skip/skip.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package skip

import (
"reflect"
"slices"
"strings"
)

// CanSkipExternalRequest checks if the external request can be skipped based on the plan and state.
// Two of the same objects are supplied as parameters, together with the operation that is being performed.
// The function uses the `skip_on` struct tag to determine if the field should be skipped.
// 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.
// The function will return true if the external request can be skipped, false otherwise.
func CanSkipExternalRequest[T any](a, b T, operation string) bool {
valA := reflect.ValueOf(a)
valB := reflect.ValueOf(b)

// Since we are using generics, we know that the types of a and b are the same.
// Therefore we can check the type of a to determine if it is a struct.
if valA.Kind() != reflect.Struct {
return false
}

typeOfA := valA.Type()
// iterate over all fields of the struct
for i := 0; i < typeOfA.NumField(); i++ {
field := typeOfA.Field(i)
// Check if the field has the skip_on tag
// If it doesn't we need to compare the valued as we cannot determine if the field should be skipped.
// If the field has the skip_on tag, we can check if the operation is in the list of operations that should be skipped.
tag := field.Tag.Get("skip_on")
if tag != "" {
// Split the tag values by comma and check if the operation is in the list.
// If the operation is in the list, then this field represents a change in state only
// and does not require an external request to be made.
// Therefore we can skip tp the next field.
tagValues := strings.Split(tag, ",")
if slices.Contains(tagValues, operation) {
continue
}
}

// If we get here then we need to compare the field values.
// By now we have determined that the struct fields do not have a valid skip value for this operation.
// Therefore if the field values are not equal, then the external request cannot be skipped.
fieldA := valA.Field(i)
fieldB := valB.Field(i)

if !fieldA.IsValid() || !fieldB.IsValid() {
return false
}
if !reflect.DeepEqual(fieldA.Interface(), fieldB.Interface()) {
return false
}
}
return true
}
Loading

0 comments on commit b11892c

Please sign in to comment.