From 5742e34fd2f6f5a8255ea38aeb8203cd5dac404a Mon Sep 17 00:00:00 2001 From: detvdl Date: Tue, 12 Dec 2023 11:42:19 +0100 Subject: [PATCH 1/3] feat: support read_data for POST-based APIs --- restapi/api_client.go | 12 +++++++--- restapi/api_object.go | 29 ++++++++++++++++++++++- restapi/resource_api_object.go | 42 +++++++++++++++++++++++++++------- 3 files changed, 71 insertions(+), 12 deletions(-) diff --git a/restapi/api_client.go b/restapi/api_client.go index be90cf82..7ad665db 100644 --- a/restapi/api_client.go +++ b/restapi/api_client.go @@ -30,6 +30,7 @@ type apiClientOpt struct { idAttribute string createMethod string readMethod string + readData string updateMethod string updateData string destroyMethod string @@ -63,6 +64,7 @@ type APIClient struct { idAttribute string createMethod string readMethod string + readData string updateMethod string updateData string destroyMethod string @@ -76,7 +78,7 @@ type APIClient struct { oauthConfig *clientcredentials.Config } -//NewAPIClient makes a new api client for RESTful calls +// NewAPIClient makes a new api client for RESTful calls func NewAPIClient(opt *apiClientOpt) (*APIClient, error) { if opt.debug { log.Printf("api_client.go: Constructing debug api_client\n") @@ -162,6 +164,7 @@ func NewAPIClient(opt *apiClientOpt) (*APIClient, error) { idAttribute: opt.idAttribute, createMethod: opt.createMethod, readMethod: opt.readMethod, + readData: opt.readData, updateMethod: opt.updateMethod, updateData: opt.updateData, destroyMethod: opt.destroyMethod, @@ -210,8 +213,11 @@ func (client *APIClient) toString() string { return buffer.String() } -/* Helper function that handles sending/receiving and handling - of HTTP data in and out. */ +/* +Helper function that handles sending/receiving and handling + + of HTTP data in and out. +*/ func (client *APIClient) sendRequest(method string, path string, data string) (string, error) { fullURI := client.uri + path var req *http.Request diff --git a/restapi/api_object.go b/restapi/api_object.go index 771e8215..1f893f04 100644 --- a/restapi/api_object.go +++ b/restapi/api_object.go @@ -18,6 +18,7 @@ type apiObjectOpts struct { putPath string createMethod string readMethod string + readData string updateMethod string updateData string destroyMethod string @@ -52,6 +53,7 @@ type APIObject struct { /* Set internally */ data map[string]interface{} /* Data as managed by the user */ + readData map[string]interface{} /* Read data as managed by the user */ updateData map[string]interface{} /* Update data as managed by the user */ destroyData map[string]interface{} /* Destroy data as managed by the user */ apiData map[string]interface{} /* Data as available from the API */ @@ -79,6 +81,9 @@ func NewAPIObject(iClient *APIClient, opts *apiObjectOpts) (*APIObject, error) { if opts.readMethod == "" { opts.readMethod = iClient.readMethod } + if opts.readData == "" { + opts.readData = iClient.readData + } if opts.updateMethod == "" { opts.updateMethod = iClient.updateMethod } @@ -124,6 +129,7 @@ func NewAPIObject(iClient *APIClient, opts *apiObjectOpts) (*APIObject, error) { id: opts.id, idAttribute: opts.idAttribute, data: make(map[string]interface{}), + readData: make(map[string]interface{}), updateData: make(map[string]interface{}), destroyData: make(map[string]interface{}), apiData: make(map[string]interface{}), @@ -157,6 +163,17 @@ func NewAPIObject(iClient *APIClient, opts *apiObjectOpts) (*APIObject, error) { } } + if opts.readData != "" { + if opts.debug { + log.Printf("api_object.go: Parsing read data: '%s'", opts.readData) + } + + err := json.Unmarshal([]byte(opts.readData), &obj.readData) + if err != nil { + return &obj, fmt.Errorf("api_object.go: error parsing read data provided: %v", err.Error()) + } + } + if opts.updateData != "" { if opts.debug { log.Printf("api_object.go: Parsing update data: '%s'", opts.updateData) @@ -202,6 +219,7 @@ func (obj *APIObject) toString() string { buffer.WriteString(fmt.Sprintf("debug: %t\n", obj.debug)) buffer.WriteString(fmt.Sprintf("read_search: %s\n", spew.Sdump(obj.readSearch))) buffer.WriteString(fmt.Sprintf("data: %s\n", spew.Sdump(obj.data))) + buffer.WriteString(fmt.Sprintf("read_data: %s\n", spew.Sdump(obj.readData))) buffer.WriteString(fmt.Sprintf("update_data: %s\n", spew.Sdump(obj.updateData))) buffer.WriteString(fmt.Sprintf("destroy_data: %s\n", spew.Sdump(obj.destroyData))) buffer.WriteString(fmt.Sprintf("api_data: %s\n", spew.Sdump(obj.apiData))) @@ -321,7 +339,16 @@ func (obj *APIObject) readObject() error { getPath = fmt.Sprintf("%s?%s", obj.getPath, obj.queryString) } - resultString, err := obj.apiClient.sendRequest(obj.readMethod, strings.Replace(getPath, "{id}", obj.id, -1), "") + b := []byte{} + readData, _ := json.Marshal(obj.readData) + if string(readData) != "" { + if obj.debug { + log.Printf("api_object.go: Using read data '%s'", string(readData)) + } + b = readData + } + + resultString, err := obj.apiClient.sendRequest(obj.readMethod, strings.Replace(getPath, "{id}", obj.id, -1), string(b)) if err != nil { if strings.Contains(err.Error(), "unexpected response code '404'") { log.Printf("api_object.go: 404 error while refreshing state for '%s' at path '%s'. Removing from state.", obj.id, obj.getPath) diff --git a/restapi/resource_api_object.go b/restapi/resource_api_object.go index 4289fc87..c797d71f 100644 --- a/restapi/resource_api_object.go +++ b/restapi/resource_api_object.go @@ -144,6 +144,23 @@ func resourceRestAPI() *schema.Resource { ForceNew: true, Description: "Any changes to these values will result in recreating the resource instead of updating.", }, + "read_data": { + Type: schema.TypeString, + Optional: true, + Description: "Valid JSON object to pass during read requests.", + Sensitive: isDataSensitive, + ValidateFunc: func(val interface{}, key string) (warns []string, errs []error) { + v := val.(string) + if v != "" { + data := make(map[string]interface{}) + err := json.Unmarshal([]byte(v), &data) + if err != nil { + errs = append(errs, fmt.Errorf("read_data attribute is invalid JSON: %v", err)) + } + } + return warns, errs + }, + }, "update_data": { Type: schema.TypeString, Optional: true, @@ -183,10 +200,13 @@ func resourceRestAPI() *schema.Resource { } } -/* Since there is nothing in the ResourceData structure other - than the "id" passed on the command line, we have to use an opinionated - view of the API paths to figure out how to read that object - from the API */ +/* +Since there is nothing in the ResourceData structure other + + than the "id" passed on the command line, we have to use an opinionated + view of the API paths to figure out how to read that object + from the API +*/ func resourceRestAPIImport(d *schema.ResourceData, meta interface{}) (imported []*schema.ResourceData, err error) { input := d.Id() @@ -339,10 +359,13 @@ func resourceRestAPIExists(d *schema.ResourceData, meta interface{}) (exists boo return exists, err } -/* Simple helper routine to build an api_object struct - for the various calls terraform will use. Unfortunately, - terraform cannot just reuse objects, so each CRUD operation - results in a new object created */ +/* +Simple helper routine to build an api_object struct + + for the various calls terraform will use. Unfortunately, + terraform cannot just reuse objects, so each CRUD operation + results in a new object created +*/ func makeAPIObject(d *schema.ResourceData, meta interface{}) (*APIObject, error) { opts, err := buildAPIObjectOpts(d) if err != nil { @@ -398,6 +421,9 @@ func buildAPIObjectOpts(d *schema.ResourceData) (*apiObjectOpts, error) { if v, ok := d.GetOk("read_method"); ok { opts.readMethod = v.(string) } + if v, ok := d.GetOk("read_data"); ok { + opts.readData = v.(string) + } if v, ok := d.GetOk("update_method"); ok { opts.updateMethod = v.(string) } From 1807ca375cdcdc3ce7cf1584195280c7445e4dcf Mon Sep 17 00:00:00 2001 From: detvdl Date: Tue, 12 Dec 2023 13:38:07 +0100 Subject: [PATCH 2/3] test(api_object_test): add arbitrary test case --- restapi/api_object_test.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/restapi/api_object_test.go b/restapi/api_object_test.go index ede3b0b2..39ed5ef7 100644 --- a/restapi/api_object_test.go +++ b/restapi/api_object_test.go @@ -187,6 +187,24 @@ func TestAPIObject(t *testing.T) { } }) + t.Run("read_object_with_read_data", func(t *testing.T) { + if testDebug { + log.Printf("api_object_test.go: Testing read_object() with read_data") + } + for testCase := range testingObjects { + t.Run(testCase, func(t *testing.T) { + if testDebug { + log.Printf("api_object_test.go: Getting data for '%s' test case from server\n", testCase) + } + testingObjects[testCase].readData["path"] = "/" + testCase + err := testingObjects[testCase].readObject() + if err != nil { + t.Fatalf("api_object_test.go: Failed to read data for test case '%s': %s", testCase, err) + } + }) + } + }) + /* Verify our copy_keys is happy by seeing if Thing made it into the data hash */ t.Run("copy_keys", func(t *testing.T) { if testDebug { From 802e293f7ba0f48d19f4a2155bbb9748bc148cb3 Mon Sep 17 00:00:00 2001 From: detvdl Date: Tue, 12 Dec 2023 13:56:45 +0100 Subject: [PATCH 3/3] docs: add read_data info to docs --- docs/resources/object.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/resources/object.md b/docs/resources/object.md index 2344756d..ddadcad6 100644 --- a/docs/resources/object.md +++ b/docs/resources/object.md @@ -40,6 +40,7 @@ resource "restapi_object" "Foo2" { - `id_attribute` (String) Defaults to `id_attribute` set on the provider. Allows per-resource override of `id_attribute` (see `id_attribute` provider config documentation) - `object_id` (String) Defaults to the id learned by the provider during normal operations and `id_attribute`. Allows you to set the id manually. This is used in conjunction with the `*_path` attributes. - `query_string` (String) Query string to be included in the path +- `read_data` (String) Valid JSON object to pass during read requests. - `read_method` (String) Defaults to `read_method` set on the provider. Allows per-resource override of `read_method` (see `read_method` provider config documentation) - `read_path` (String) Defaults to `path/{id}`. The API path that represents where to READ (GET) objects of this type on the API server. The string `{id}` will be replaced with the terraform ID of the object. - `read_search` (Map of String) Custom search for `read_path`. This map will take `search_key`, `search_value`, `results_key` and `query_string` (see datasource config documentation)