From 7aeaecf1a4f7a30588ad65cc8afb8db2c0d1b9f5 Mon Sep 17 00:00:00 2001 From: magodo Date: Wed, 28 Dec 2022 14:14:06 +0800 Subject: [PATCH] `restful_resource` - Introduce `force_new_attrs` to support conditional replace resource (#37) * Surface * `restful_resource` - Introduce `force_new_attrs` to mark resoruce to be replaced once any of these attributes in `body` got changed --- docs/resources/resource.md | 1 + internal/provider/resource.go | 73 ++++++++++++++++++++++++++++++++++- 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/docs/resources/resource.md b/docs/resources/resource.md index 002b9e4..933124b 100644 --- a/docs/resources/resource.md +++ b/docs/resources/resource.md @@ -50,6 +50,7 @@ resource "restful_resource" "rg" { - `create_selector` (String) A selector in [gjson query syntax](https://github.com/tidwall/gjson/blob/master/SYNTAX.md#queries) query syntax, that is used when create returns a collection of resources, to select exactly one member resource of from it. By default, the whole response body is used as the body. - `delete_method` (String) The method used to delete the resource. Possible values are `DELETE` and `POST`. This overrides the `delete_method` set in the provider block (defaults to DELETE). - `delete_path` (String) The API path used to delete the resource. The `id` is used instead if `delete_path` is absent. The path can be string literal, or combined by followings: `$(path)` expanded to `path`, `$(body.x.y.z)` expands to the `x.y.z` property (urlencoded) in API body, `#(body.id)` expands to the `id` property, with `base_url` prefix trimmed. +- `force_new_attrs` (Set of String) A set of `body` attribute paths (in [gjson syntax](https://github.com/tidwall/gjson/blob/master/SYNTAX.md)) whose value once changed, will trigger a replace of this resource. Note this only take effects when the `body` is a unknown before apply. Technically, we do a JSON merge patch and check whether the attribute path appear in the merge patch. - `header` (Map of String) The header parameters that are applied to each request. This overrides the `header` set in the provider block. - `merge_patch_disabled` (Boolean) Whether to use a JSON Merge Patch as the request body in the PATCH update? This is only effective when `update_method` is set to `PATCH`. This overrides the `merge_patch_disabled` set in the provider block (defaults to `false`). - `poll_create` (Attributes) The polling option for the "Create" operation (see [below for nested schema](#nestedatt--poll_create)) diff --git a/internal/provider/resource.go b/internal/provider/resource.go index 6345cfe..945403b 100644 --- a/internal/provider/resource.go +++ b/internal/provider/resource.go @@ -64,7 +64,9 @@ type resourceData struct { MergePatchDisabled types.Bool `tfsdk:"merge_patch_disabled"` Query types.Map `tfsdk:"query"` Header types.Map `tfsdk:"header"` - CheckExistance types.Bool `tfsdk:"check_existance"` + + CheckExistance types.Bool `tfsdk:"check_existance"` + ForceNewAttrs types.Set `tfsdk:"force_new_attrs"` Output types.String `tfsdk:"output"` } @@ -366,6 +368,12 @@ func (r *Resource) Schema(ctx context.Context, req resource.SchemaRequest, resp MarkdownDescription: "Whether to check resource already existed? Defaults to `false`.", Optional: true, }, + "force_new_attrs": schema.SetAttribute{ + Description: "A set of `body` attribute paths (in gjson syntax) whose value once changed, will trigger a replace of this resource. Note this only take effects when the `body` is a unknown before apply. Technically, we do a JSON merge patch and check whether the attribute path appear in the merge patch.", + MarkdownDescription: "A set of `body` attribute paths (in [gjson syntax](https://github.com/tidwall/gjson/blob/master/SYNTAX.md)) whose value once changed, will trigger a replace of this resource. Note this only take effects when the `body` is a unknown before apply. Technically, we do a JSON merge patch and check whether the attribute path appear in the merge patch.", + Optional: true, + ElementType: types.StringType, + }, "output": schema.StringAttribute{ Description: "The response body after reading the resource.", MarkdownDescription: "The response body after reading the resource.", @@ -400,6 +408,69 @@ func (r *Resource) ValidateConfig(ctx context.Context, req resource.ValidateConf } } +func (r *Resource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if req.Plan.Raw.IsNull() { + // // If the entire plan is null, the resource is planned for destruction. + return + } + + if req.State.Raw.IsNull() { + // // If the entire state is null, the resource is planned for creation. + return + } + var plan resourceData + if diags := req.Plan.Get(ctx, &plan); diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + + if !plan.ForceNewAttrs.IsUnknown() && !plan.Body.IsUnknown() { + var forceNewAttrs []types.String + if diags := plan.ForceNewAttrs.ElementsAs(ctx, &forceNewAttrs, false); diags != nil { + resp.Diagnostics.Append(diags...) + return + } + var knownForceNewAttrs []string + for _, attr := range forceNewAttrs { + if attr.IsUnknown() { + continue + } + knownForceNewAttrs = append(knownForceNewAttrs, attr.ValueString()) + } + + if len(knownForceNewAttrs) != 0 { + var state resourceData + if diags := req.State.Get(ctx, &state); diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + + originJson := state.Body.ValueString() + if originJson == "" { + originJson = "{}" + } + + modifiedJson := plan.Body.ValueString() + if modifiedJson == "" { + modifiedJson = "{}" + } + patch, err := jsonpatch.CreateMergePatch([]byte(originJson), []byte(modifiedJson)) + if err != nil { + resp.Diagnostics.AddError("failed to create merge patch", err.Error()) + return + } + + for _, attr := range knownForceNewAttrs { + result := gjson.Get(string(patch), attr) + if result.Exists() { + resp.RequiresReplace = []tfpath.Path{tfpath.Root("body")} + break + } + } + } + } +} + func (r *Resource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { r.p = &Provider{} if req.ProviderData != nil {