Skip to content

Commit

Permalink
Merge pull request #104 from magodo/op_support_read_path
Browse files Browse the repository at this point in the history
`restful_operation` - Support `id_builder` for building resource id
  • Loading branch information
magodo authored Jul 19, 2024
2 parents 5298447 + 95442ae commit bd2e266
Show file tree
Hide file tree
Showing 6 changed files with 84 additions and 9 deletions.
3 changes: 2 additions & 1 deletion docs/resources/operation.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ resource "restful_operation" "register_rp" {
- `delete_method` (String) The method for the `Delete` call. Possible values are `POST`, `PUT`, `PATCH` and `DELETE`. If this is not specified, no `Delete` call will occur.
- `delete_path` (String) The path for the `Delete` call, relative to the `base_url` of the provider. The `path` is used instead if `delete_path` is absent.
- `header` (Map of String) The header parameters that are applied to each request. This overrides the `header` set in the provider block.
- `id_builder` (String) The pattern used to build the `id`. The `path` is used as the `id` instead if absent.This can be a 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.
- `output_attrs` (Set of String) A set of `output` attribute paths (in [gjson syntax](https://github.com/tidwall/gjson/blob/master/SYNTAX.md)) that will be exported in the `output`. If this is not specified, all attributes will be exported by `output`.
- `poll` (Attributes) The polling option for the "`Create`/`Update`" operation (see [below for nested schema](#nestedatt--poll))
- `poll_delete` (Attributes) The polling option for the "`Delete`" operation (see [below for nested schema](#nestedatt--poll_delete))
Expand All @@ -56,7 +57,7 @@ resource "restful_operation" "register_rp" {

### Read-Only

- `id` (String) The ID of the operation. Same as the `path`.
- `id` (String) The ID of the operation.
- `output` (Dynamic) The response body.

<a id="nestedatt--poll"></a>
Expand Down
6 changes: 3 additions & 3 deletions docs/resources/resource.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ resource "restful_resource" "rg" {
- `create_method` (String) The method used to create the resource. Possible values are `PUT`, `POST` and `PATCH`. This overrides the `create_method` set in the provider block (defaults to POST).
- `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.
- `delete_path` (String) The API path used to delete the resource. The `id` is used instead if `delete_path` is absent. This can be a 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`).
Expand All @@ -61,14 +61,14 @@ resource "restful_resource" "rg" {
- `precheck_delete` (Attributes List) An array of prechecks that need to pass prior to the "Delete" operation. Exactly one of `mutex` or `api` should be specified. (see [below for nested schema](#nestedatt--precheck_delete))
- `precheck_update` (Attributes List) An array of prechecks that need to pass prior to the "Update" operation. Exactly one of `mutex` or `api` should be specified. (see [below for nested schema](#nestedatt--precheck_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.
- `read_path` (String) The API path used to read the resource, which is used as the `id`. The `path` is used as the `id` instead if `read_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.
- `read_path` (String) The API path used to read the resource, which is used as the `id`. The `path` is used as the `id` instead if `read_path` is absent. This can be a 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.
- `read_selector` (String) A selector in [gjson query syntax](https://github.com/tidwall/gjson/blob/master/SYNTAX.md#queries) query syntax, that is used when read 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.
- `retry_create` (Attributes) The retry option for the "Create (i.e. PUT/POST)" operation (see [below for nested schema](#nestedatt--retry_create))
- `retry_delete` (Attributes) The retry option for the "Delete (i.e. DELETE)" operation (see [below for nested schema](#nestedatt--retry_delete))
- `retry_read` (Attributes) The retry option for the "Read (i.e. GET, but not include the `precheck_xxx`/`poll_xxx`)" operation (see [below for nested schema](#nestedatt--retry_read))
- `retry_update` (Attributes) The retry option for the "Update (i.e. PUT/PATCH/POST)" operation (see [below for nested schema](#nestedatt--retry_update))
- `update_method` (String) The method used to update the resource. Possible values are `PUT`, `POST`, and `PATCH`. This overrides the `update_method` set in the provider block (defaults to PUT).
- `update_path` (String) The API path used to update the resource. The `id` is used instead if `update_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.
- `update_path` (String) The API path used to update the resource. The `id` is used instead if `update_path` is absent. This can be a 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.
- `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
4 changes: 2 additions & 2 deletions internal/buildpath/path.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ import (

var (
// IdPattern matches against a full URL, which is meant to be prefix trimed by the server's base URL.
// This can be "#{body.x.y.z}".
// This can be "#(body.x.y.z)".
IdPattern = regexp.MustCompile(`\#\(([\w.]+)\)`)

// ValuePattern matches against a normal string. This can be either "${path}", or "${body.x.y.z}"
// ValuePattern matches against a normal string. This can be either "$(path)", or "$(body.x.y.z)"
ValuePattern = regexp.MustCompile(`\$(\w*)\(([\w.]+)\)`)
)

Expand Down
50 changes: 50 additions & 0 deletions internal/provider/operation_code_server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
Expand Down Expand Up @@ -38,6 +39,35 @@ func TestOperation_CodeServer_Empty(t *testing.T) {
})
}

func TestOperation_CodeServer_idBuilder(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "POST":
w.Write([]byte(`{"id": 1}`))
return
case "GET":
if !strings.HasSuffix(r.URL.Path, "/1") {
w.WriteHeader(http.StatusNotFound)
return
}
w.Write([]byte(`{"status": "OK"}`))
case "DELETE":
return
}
}))

d := newCodeServerOperation(srv.URL)
resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: acceptance.ProviderFactory(),
Steps: []resource.TestStep{
{
Config: d.idBilder(),
Check: resource.ComposeTestCheckFunc(),
},
},
})
}

func (d codeServerOperation) empty() string {
return fmt.Sprintf(`
provider "restful" {
Expand All @@ -51,3 +81,23 @@ resource "restful_operation" "test" {
}
`, d.url)
}

func (d codeServerOperation) idBilder() string {
return fmt.Sprintf(`
provider "restful" {
base_url = %q
}
resource "restful_operation" "test" {
path = "foo"
id_builder = "bar/$(body.id)"
method = "POST"
poll = {
status_locator = "body.status"
status = {
success = "OK"
}
}
}
`, d.url)
}
27 changes: 25 additions & 2 deletions internal/provider/operation_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ var _ resource.ResourceWithUpgradeState = &OperationResource{}
type operationResourceData struct {
ID types.String `tfsdk:"id"`
Path types.String `tfsdk:"path"`
IdBuilder types.String `tfsdk:"id_builder"`
Method types.String `tfsdk:"method"`
Body types.Dynamic `tfsdk:"body"`
Query types.Map `tfsdk:"query"`
Expand Down Expand Up @@ -68,8 +69,8 @@ func (r *OperationResource) Schema(ctx context.Context, req resource.SchemaReque
Version: 1,
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Description: "The ID of the operation. Same as the `path`.",
MarkdownDescription: "The ID of the operation. Same as the `path`.",
Description: "The ID of the operation.",
MarkdownDescription: "The ID of the operation.",
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
Expand All @@ -83,6 +84,15 @@ func (r *OperationResource) Schema(ctx context.Context, req resource.SchemaReque
stringplanmodifier.RequiresReplace(),
},
},
// This is actually the same as the `read_path` of restful_resource, besides the name
"id_builder": schema.StringAttribute{
Description: "The pattern used to build the `id`. The `path` is used as the `id` instead if absent." + pathDescription,
MarkdownDescription: "The pattern used to build the `id`. The `path` is used as the `id` instead if absent." + pathDescription,
Optional: true,
Validators: []validator.String{
myvalidator.StringIsPathBuilder(),
},
},
"method": schema.StringAttribute{
Description: "The HTTP method for the `Create`/`Update` call. Possible values are `PUT`, `POST`, `PATCH` and `DELETE`.",
MarkdownDescription: "The HTTP method for the `Create`/`Update` call. Possible values are `PUT`, `POST`, `PATCH` and `DELETE`.",
Expand Down Expand Up @@ -227,6 +237,16 @@ func (r *OperationResource) createOrUpdate(ctx context.Context, tfplan tfsdk.Pla
}

resourceId := plan.Path.ValueString()
if !plan.IdBuilder.IsNull() {
resourceId, err = buildpath.BuildPath(plan.IdBuilder.ValueString(), r.p.apiOpt.BaseURL.String(), plan.Path.ValueString(), response.Body())
if err != nil {
diagnostics.AddError(
fmt.Sprintf("Failed to build the id for this resource"),
fmt.Sprintf("Can't build resource id with `id_builder`: %q, `path`: %q, `body`: %q: %v", plan.IdBuilder.ValueString(), plan.Path.ValueString(), string(b), err),
)
return
}
}

// For LRO, wait for completion
if !plan.Poll.IsNull() {
Expand All @@ -240,6 +260,9 @@ func (r *OperationResource) createOrUpdate(ctx context.Context, tfplan tfsdk.Pla
diagnostics.Append(diags...)
return
}
if opt.UrlLocator == nil {
response.Request.URL = resourceId
}
p, err := client.NewPollableForPoll(*response, *opt)
if err != nil {
diagnostics.AddError(
Expand Down
3 changes: 2 additions & 1 deletion internal/provider/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -328,8 +328,9 @@ func retryAttribute(s string) schema.SingleNestedAttribute {
}
}

const pathDescription = "This can be a 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."

func (r *Resource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
const pathDescription = "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."
resp.Schema = schema.Schema{
Description: "`restful_resource` manages a restful resource.",
MarkdownDescription: "`restful_resource` manages a restful resource.",
Expand Down

0 comments on commit bd2e266

Please sign in to comment.