Skip to content

Commit

Permalink
restful_resource - Introduce force_new_attrs to support condition…
Browse files Browse the repository at this point in the history
…al 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
  • Loading branch information
magodo authored Dec 28, 2022
1 parent c8e12e6 commit 7aeaecf
Show file tree
Hide file tree
Showing 2 changed files with 73 additions and 1 deletion.
1 change: 1 addition & 0 deletions docs/resources/resource.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
73 changes: 72 additions & 1 deletion internal/provider/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}
Expand Down Expand Up @@ -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.",
Expand Down Expand Up @@ -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 {
Expand Down

0 comments on commit 7aeaecf

Please sign in to comment.