Skip to content

Commit

Permalink
Rename ignore_changes to write_only_attrs to clarify its usage
Browse files Browse the repository at this point in the history
  • Loading branch information
magodo committed Jun 22, 2022
1 parent 0672617 commit c7bb768
Show file tree
Hide file tree
Showing 5 changed files with 295 additions and 35 deletions.
2 changes: 1 addition & 1 deletion docs/resources/restful_resource.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,13 @@ resource "restful_resource" "rg" {

- `create_method` (String) The method used to create the resource. Possible values are `PUT` and `POST`. This overrides the `create_method` set in the provider block (defaults to POST).
- `header` (Map of String) The header parameters that are applied to each request. This overrides the `header` set in the provider block.
- `ignore_changes` (List of String) A list of paths (in [gjson syntax](https://github.com/tidwall/gjson/blob/master/SYNTAX.md)) to the attributes that should not affect the resource after its creation.
- `name_path` (String) The path (in [gjson syntax](https://github.com/tidwall/gjson/blob/master/SYNTAX.md)) to the name attribute in the response, which is only used during creation of the resource to construct the resource identifier. This is ignored when `create_method` is `PUT`. Either `name_path` or `url_path` needs to set when `create_method` is `POST`.
- `poll_create` (Attributes) The polling option for the "Create" operation (see [below for nested schema](#nestedatt--poll_create))
- `poll_delete` (Attributes) The polling option for the "Delete" operation (see [below for nested schema](#nestedatt--poll_delete))
- `poll_update` (Attributes) The polling option for the "Update" operation (see [below for nested schema](#nestedatt--poll_update))
- `query` (Map of List of String) The query parameters that are applied to each request. This overrides the `query` set in the provider block.
- `url_path` (String) The path (in [gjson syntax](https://github.com/tidwall/gjson/blob/master/SYNTAX.md)) to the id attribute in the response, which is only used during creation of the resource to be as the resource identifier. This is ignored when `create_method` is `PUT`. Either `name_path` or `url_path` needs to set when `create_method` is `POST`.
- `write_only_attrs` (List of String) A list of paths (in [gjson syntax](https://github.com/tidwall/gjson/blob/master/SYNTAX.md)) to the attributes that are only settable, but won't be read in GET response.

### Read-Only

Expand Down
65 changes: 65 additions & 0 deletions examples/usecases/msgraph/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
terraform {
required_providers {
restful = {
source = "magodo/restful"
}
}
}

variable "client_id" {
type = string
}

variable "client_secret" {
type = string
}

variable "tenant_id" {
type = string
}

provider "restful" {
base_url = "https://graph.microsoft.com/v1.0"
security = {
oauth2 = {
client_id = var.client_id
client_secret = var.client_secret
token_url = format("https://login.microsoftonline.com/%s/oauth2/v2.0/token", var.tenant_id)
scopes = ["https://graph.microsoft.com/.default"]
}
}
}

resource "restful_resource" "group" {
path = "/groups"
name_path = "id"
body = jsonencode({
description = "Self help community for library"
displayName = "Library Assist"
groupTypes = [
"Unified"
]
mailEnabled = true
mailNickname = "library"
securityEnabled = false
})
}

resource "restful_resource" "user" {
path = "/users"
name_path = "id"
body = jsonencode({
accountEnabled = true
mailNickname = "AdeleV"
displayName = "J.Doe"
userPrincipalName = "[email protected]"
passwordProfile = {
password = "SecretP@sswd99!"
}
})
write_only_attrs = [
"mailNickname",
"accountEnabled",
"passwordProfile",
]
}
38 changes: 28 additions & 10 deletions internal/provider/body.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,46 @@ import (
"encoding/json"
"fmt"

"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
)

// ModifyBody modifies the body based on the base body, removing any attribute
// attribute that only exists in the body, or is specified to be ignored.
func ModifyBody(base, body string, ignoreChanges []string) (string, error) {
// ModifyBody modifies the body based on the base body, only keeps attributes that exist on both sides.
// If compensateBaseAttrs is set, then any attribute path element only found in the base body will
// be added up to the result body.
func ModifyBody(base, body string, compensateBaseAttrs []string) (string, error) {
var baseJSON map[string]interface{}
if err := json.Unmarshal([]byte(base), &baseJSON); err != nil {
return "", fmt.Errorf("unmarshal the base %q: %v", base, err)
}
for _, path := range ignoreChanges {
var err error
body, err = sjson.Delete(body, path)
if err != nil {
return "", fmt.Errorf("deleting attribute in path %q: %v", path, err)
}
}

var bodyJSON map[string]interface{}
if err := json.Unmarshal([]byte(body), &bodyJSON); err != nil {
return "", fmt.Errorf("unmarshal the body %q: %v", body, err)
}

b, err := json.Marshal(getUpdatedJSON(baseJSON, bodyJSON))
if err != nil {
return "", err
}
result := string(b)

for _, path := range compensateBaseAttrs {
if gjson.Get(base, path).Exists() && !gjson.Get(body, path).Exists() {
var err error
result, err = sjson.Set(result, path, gjson.Get(base, path).Value())
if err != nil {
return "", err
}
}
}

// Remarshal to keep order.
var m interface{}
if err := json.Unmarshal([]byte(result), &m); err != nil {
return "", err
}
b, err = json.Marshal(m)
return string(b), err
}

Expand Down
68 changes: 44 additions & 24 deletions internal/provider/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,9 +118,9 @@ func (r resourceType) GetSchema(_ context.Context) (tfsdk.Schema, diag.Diagnosti
Optional: true,
Type: types.StringType,
},
"ignore_changes": {
Description: "A list of paths (in gjson syntax) to the attributes that should not affect the resource after its creation.",
MarkdownDescription: "A list of paths (in [gjson syntax](https://github.com/tidwall/gjson/blob/master/SYNTAX.md)) to the attributes that should not affect the resource after its creation.",
"write_only_attrs": {
Description: "A list of paths (in gjson syntax) to the attributes that are only settable, but won't be read in GET response.",
MarkdownDescription: "A list of paths (in [gjson syntax](https://github.com/tidwall/gjson/blob/master/SYNTAX.md)) to the attributes that are only settable, but won't be read in GET response.",
Optional: true,
Computed: true,
Type: types.ListType{ElemType: types.StringType},
Expand Down Expand Up @@ -235,8 +235,28 @@ func (r resource) ValidateConfig(ctx context.Context, req tfsdk.ValidateResource
validatePoll(config.PollUpdate, "poll_update")
validatePoll(config.PollDelete, "poll_delete")

if resp.Diagnostics.HasError() {
return
if !config.Body.IsUnknown() {
var body map[string]interface{}
if err := json.Unmarshal([]byte(config.Body.Value), &body); err != nil {
resp.Diagnostics.AddError(
"Invalid configuration",
fmt.Sprintf(`Failed to unmarshal "body": %s: %s`, err.Error(), config.Body.String()),
)
}

if !config.WriteOnlyAttributes.IsUnknown() && !config.WriteOnlyAttributes.IsNull() {
for _, ie := range config.WriteOnlyAttributes.Elems {
ie := ie.(types.String)
if !ie.IsUnknown() && !ie.IsNull() {
if !gjson.Get(config.Body.Value, ie.Value).Exists() {
resp.Diagnostics.AddError(
"Invalid configuration",
fmt.Sprintf(`Invalid path in "write_only_attrs": %s`, ie.String()),
)
}
}
}
}
}
}

Expand All @@ -251,19 +271,19 @@ type resource struct {
var _ tfsdk.Resource = resource{}

type resourceData struct {
ID types.String `tfsdk:"id"`
Path types.String `tfsdk:"path"`
Body types.String `tfsdk:"body"`
NamePath types.String `tfsdk:"name_path"`
UrlPath types.String `tfsdk:"url_path"`
IgnoreChanges types.List `tfsdk:"ignore_changes"`
PollCreate types.Object `tfsdk:"poll_create"`
PollUpdate types.Object `tfsdk:"poll_update"`
PollDelete types.Object `tfsdk:"poll_delete"`
CreateMethod types.String `tfsdk:"create_method"`
Query types.Map `tfsdk:"query"`
Header types.Map `tfsdk:"header"`
Output types.String `tfsdk:"output"`
ID types.String `tfsdk:"id"`
Path types.String `tfsdk:"path"`
Body types.String `tfsdk:"body"`
NamePath types.String `tfsdk:"name_path"`
UrlPath types.String `tfsdk:"url_path"`
WriteOnlyAttributes types.List `tfsdk:"write_only_attrs"`
PollCreate types.Object `tfsdk:"poll_create"`
PollUpdate types.Object `tfsdk:"poll_update"`
PollDelete types.Object `tfsdk:"poll_delete"`
CreateMethod types.String `tfsdk:"create_method"`
Query types.Map `tfsdk:"query"`
Header types.Map `tfsdk:"header"`
Output types.String `tfsdk:"output"`
}

type pollDataGo struct {
Expand Down Expand Up @@ -457,15 +477,15 @@ func (r resource) Read(ctx context.Context, req tfsdk.ReadResourceRequest, resp

b := response.Body()

var ignoreChanges []string
// In case ignore_changes (O+C) is not set, set its default value as is defined in schema. This can avoid unnecessary plan diff after import.
if state.IgnoreChanges.Null {
state.IgnoreChanges = types.List{
var writeOnlyAttributes []string
// In case write_only_attrs (O+C) is not set, set its default value as is defined in schema. This can avoid unnecessary plan diff after import.
if state.WriteOnlyAttributes.Null {
state.WriteOnlyAttributes = types.List{
ElemType: types.StringType,
Elems: []attr.Value{},
}
}
diags = state.IgnoreChanges.ElementsAs(ctx, &ignoreChanges, false)
diags = state.WriteOnlyAttributes.ElementsAs(ctx, &writeOnlyAttributes, false)
resp.Diagnostics.Append(diags...)
if diags.HasError() {
return
Expand All @@ -476,7 +496,7 @@ func (r resource) Read(ctx context.Context, req tfsdk.ReadResourceRequest, resp
// This branch is only invoked during `terraform import`.
body, err = ModifyBodyForImport(strings.TrimPrefix(state.Body.Value, __IMPORT_HEADER__), string(b))
} else {
body, err = ModifyBody(state.Body.Value, string(b), ignoreChanges)
body, err = ModifyBody(state.Body.Value, string(b), writeOnlyAttributes)
}
if err != nil {
resp.Diagnostics.AddError(
Expand Down
Loading

0 comments on commit c7bb768

Please sign in to comment.