Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for read_data #252

Merged
merged 3 commits into from
Mar 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/resources/object.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
12 changes: 9 additions & 3 deletions restapi/api_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ type apiClientOpt struct {
idAttribute string
createMethod string
readMethod string
readData string
updateMethod string
updateData string
destroyMethod string
Expand Down Expand Up @@ -63,6 +64,7 @@ type APIClient struct {
idAttribute string
createMethod string
readMethod string
readData string
updateMethod string
updateData string
destroyMethod string
Expand All @@ -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")
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
29 changes: 28 additions & 1 deletion restapi/api_object.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ type apiObjectOpts struct {
putPath string
createMethod string
readMethod string
readData string
updateMethod string
updateData string
destroyMethod string
Expand Down Expand Up @@ -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 */
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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{}),
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)))
Expand Down Expand Up @@ -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)
Expand Down
18 changes: 18 additions & 0 deletions restapi/api_object_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
42 changes: 34 additions & 8 deletions restapi/resource_api_object.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
}
Expand Down