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

Add support for destroy_data and update_data to be sent #182

Merged
merged 6 commits into from
Nov 2, 2022
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
20 changes: 11 additions & 9 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,32 +14,34 @@ on:
push:
tags:
- 'v*'
permissions:
contents: write
jobs:
goreleaser:
runs-on: ubuntu-latest
steps:
-
name: Checkout
uses: actions/checkout@v2.4.0
uses: actions/checkout@v3
-
name: Unshallow
run: git fetch --prune --unshallow
-
name: Set up Go
uses: actions/setup-go@v2
uses: actions/setup-go@v3
with:
go-version: 1.17
go-version-file: 'go.mod'
cache: true
-
name: Import GPG key
uses: crazy-max/ghaction-import-gpg@v5
id: import_gpg
uses: hashicorp/ghaction-import-gpg@v2.1.0
env:
# These secrets will need to be configured for the repository:
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
PASSPHRASE: ${{ secrets.PASSPHRASE }}
with:
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
passphrase: ${{ secrets.PASSPHRASE }}
-
name: Run GoReleaser
uses: goreleaser/goreleaser-action@v2.8.1
uses: goreleaser/goreleaser-action@v3.0.0
with:
version: latest
args: release --parallelism 2 --rm-dist --timeout 1h --release-notes .release_info.md
Expand Down
2 changes: 2 additions & 0 deletions docs/resources/object.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ description: |-
- **create_path** (String, Optional) Defaults to `path`. The API path that represents where to CREATE (POST) objects of this type on the API server. The string `{id}` will be replaced with the terraform ID of the object if the data contains the `id_attribute`.
- **debug** (Boolean, Optional) Whether to emit verbose debug output while working with the API object on the server.
- **destroy_method** (String, Optional) Defaults to `destroy_method` set on the provider. Allows per-resource override of `destroy_method` (see `destroy_method` provider config documentation)
- **destroy_data** (String, Optional) Valid JSON data that this provider will send to the API server on DESTROY (DELETE) operations. If not set, defaults to the value of `data`.
- **destroy_path** (String, Optional) Defaults to `path/{id}`. The API path that represents where to DESTROY (DELETE) objects of this type on the API server. The string `{id}` will be replaced with the terraform ID of the object.
- **force_new** (List of String, Optional) Any changes to these values will result in recreating the resource instead of updating.
- **id** (String, Optional) The ID of this resource.
Expand All @@ -34,6 +35,7 @@ description: |-
- **read_path** (String, Optional) 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, Optional) Custom search for `read_path`. This map will take `search_key`, `search_value`, `results_key` and `query_string` (see datasource config documentation)
- **update_method** (String, Optional) Defaults to `update_method` set on the provider. Allows per-resource override of `update_method` (see `update_method` provider config documentation)
- **update_data** (String, Optional) Valid JSON data that this provider will send to the API server on UPDATE (PUT) operations. If not set, defaults to the value of `data`.
- **update_path** (String, Optional) Defaults to `path/{id}`. The API path that represents where to UPDATE (PUT) objects of this type on the API server. The string `{id}` will be replaced with the terraform ID of the object.

### Read-only
Expand Down
6 changes: 6 additions & 0 deletions restapi/api_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ type apiClientOpt struct {
createMethod string
readMethod string
updateMethod string
updateData string
destroyMethod string
destroyData string
copyKeys []string
writeReturnsObject bool
createReturnsObject bool
Expand Down Expand Up @@ -61,7 +63,9 @@ type APIClient struct {
createMethod string
readMethod string
updateMethod string
updateData string
destroyMethod string
destroyData string
copyKeys []string
writeReturnsObject bool
createReturnsObject bool
Expand Down Expand Up @@ -158,7 +162,9 @@ func NewAPIClient(opt *apiClientOpt) (*APIClient, error) {
createMethod: opt.createMethod,
readMethod: opt.readMethod,
updateMethod: opt.updateMethod,
updateData: opt.updateData,
destroyMethod: opt.destroyMethod,
destroyData: opt.destroyData,
copyKeys: opt.copyKeys,
writeReturnsObject: opt.writeReturnsObject,
createReturnsObject: opt.createReturnsObject,
Expand Down
67 changes: 61 additions & 6 deletions restapi/api_object.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ type apiObjectOpts struct {
createMethod string
readMethod string
updateMethod string
updateData string
destroyMethod string
destroyData string
deletePath string
searchPath string
queryString string
Expand Down Expand Up @@ -50,11 +52,13 @@ type APIObject struct {

/* Set internally */
data map[string]interface{} /* 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 */
apiResponse string
}

//NewAPIObject makes an APIobject to manage a RESTful object in an API
// NewAPIObject makes an APIobject to manage a RESTful object in an API
func NewAPIObject(iClient *APIClient, opts *apiObjectOpts) (*APIObject, error) {
if opts.debug {
log.Printf("api_object.go: Constructing debug api_object\n")
Expand All @@ -78,10 +82,15 @@ func NewAPIObject(iClient *APIClient, opts *apiObjectOpts) (*APIObject, error) {
if opts.updateMethod == "" {
opts.updateMethod = iClient.updateMethod
}
if opts.updateData == "" {
opts.updateData = iClient.updateData
}
if opts.destroyMethod == "" {
opts.destroyMethod = iClient.destroyMethod
}

if opts.destroyData == "" {
opts.destroyData = iClient.destroyData
}
if opts.postPath == "" {
opts.postPath = opts.path
}
Expand Down Expand Up @@ -115,6 +124,8 @@ func NewAPIObject(iClient *APIClient, opts *apiObjectOpts) (*APIObject, error) {
id: opts.id,
idAttribute: opts.idAttribute,
data: make(map[string]interface{}),
updateData: make(map[string]interface{}),
destroyData: make(map[string]interface{}),
apiData: make(map[string]interface{}),
}

Expand Down Expand Up @@ -146,6 +157,28 @@ func NewAPIObject(iClient *APIClient, opts *apiObjectOpts) (*APIObject, error) {
}
}

if opts.updateData != "" {
if opts.debug {
log.Printf("api_object.go: Parsing update data: '%s'", opts.updateData)
}

err := json.Unmarshal([]byte(opts.updateData), &obj.updateData)
if err != nil {
return &obj, fmt.Errorf("api_object.go: error parsing update data provided: %v", err.Error())
}
}

if opts.destroyData != "" {
if opts.debug {
log.Printf("api_object.go: Parsing destroy data: '%s'", opts.destroyData)
}

err := json.Unmarshal([]byte(opts.destroyData), &obj.destroyData)
if err != nil {
return &obj, fmt.Errorf("api_object.go: error parsing destroy data provided: %v", err.Error())
}
}

if opts.debug {
log.Printf("api_object.go: Constructed object: %s", obj.toString())
}
Expand All @@ -169,13 +202,18 @@ 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("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)))
return buffer.String()
}

/* Centralized function to ensure that our data as managed by
the api_object is updated with data that has come back from
the API */
/*
Centralized function to ensure that our data as managed by

the api_object is updated with data that has come back from
the API
*/
func (obj *APIObject) updateState(state string) error {
if obj.debug {
log.Printf("api_object.go: Updating API object state to '%s'\n", state)
Expand Down Expand Up @@ -327,6 +365,14 @@ func (obj *APIObject) updateObject() error {

b, _ := json.Marshal(obj.data)

updateData, _ := json.Marshal(obj.updateData)
if string(updateData) != "{}" {
if obj.debug {
log.Printf("api_object.go: Using update data '%s'", string(updateData))
}
b = updateData
}

putPath := obj.putPath
if obj.queryString != "" {
if obj.debug {
Expand Down Expand Up @@ -368,7 +414,16 @@ func (obj *APIObject) deleteObject() error {
deletePath = fmt.Sprintf("%s?%s", obj.deletePath, obj.queryString)
}

_, err := obj.apiClient.sendRequest(obj.destroyMethod, strings.Replace(deletePath, "{id}", obj.id, -1), "")
b := []byte{}
destroyData, _ := json.Marshal(obj.destroyData)
if string(destroyData) != "{}" {
if obj.debug {
log.Printf("api_object.go: Using destroy data '%s'", string(destroyData))
}
b = destroyData
}

_, err := obj.apiClient.sendRequest(obj.destroyMethod, strings.Replace(deletePath, "{id}", obj.id, -1), string(b))
if err != nil {
return err
}
Expand Down
32 changes: 30 additions & 2 deletions restapi/api_object_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,21 @@ func TestAPIObject(t *testing.T) {
}
})

/* Update once more with update_data */
t.Run("update_object_with_update_data", func(t *testing.T) {
if testDebug {
log.Printf("api_object_test.go: Testing update_object() with update_data")
}
testingObjects["minimal"].updateData["Thing"] = "knife"
testingObjects["minimal"].updateObject()
if err != nil {
t.Fatalf("api_object_test.go: Failed in update_object() test: %s", err)
} else if testingObjects["minimal"].apiData["Thing"] != "knife" {
t.Fatalf("api_object_test.go: Failed to update 'Thing' field of 'minimal' object. Expected it to be '%s' but it is '%s'\nFull obj: %+v\n",
"knife", testingObjects["minimal"].apiData["Thing"], testingObjects["minimal"])
}
})

/* Delete one and make sure a 404 follows */
t.Run("delete_object", func(t *testing.T) {
if testDebug {
Expand All @@ -233,9 +248,9 @@ func TestAPIObject(t *testing.T) {
err = testingObjects["pet"].createObject()
if err != nil {
t.Fatalf("api_object_test.go: Failed in create_object() test: %s", err)
} else if testingObjects["minimal"].apiData["Thing"] != "spoon" {
} else if testingObjects["minimal"].apiData["Thing"] != "knife" {
t.Fatalf("api_object_test.go: Failed to update 'Thing' field of 'minimal' object. Expected it to be '%s' but it is '%s'\nFull obj: %+v\n",
"spoon", testingObjects["minimal"].apiData["Thing"], testingObjects["minimal"])
"knife", testingObjects["minimal"].apiData["Thing"], testingObjects["minimal"])
}

/* verify it's there */
Expand Down Expand Up @@ -276,6 +291,19 @@ func TestAPIObject(t *testing.T) {
}
})

/* Delete it again with destroy_data and make sure a 404 follows */
t.Run("delete_object_with_destroy_data", func(t *testing.T) {
if testDebug {
log.Printf("api_object_test.go: Testing delete_object() with destroy_data")
}
testingObjects["pet"].destroyData["destroy"] = "true"
testingObjects["pet"].deleteObject()
err = testingObjects["pet"].readObject()
if err != nil {
t.Fatalf("api_object_test.go: 'pet' object deleted, but an error was returned when reading the object (expected the provider to cope with this!\n")
}
})

if testDebug {
log.Println("api_object_test.go: Stopping HTTP server")
}
Expand Down
40 changes: 40 additions & 0 deletions restapi/resource_api_object.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,40 @@ func resourceRestAPI() *schema.Resource {
ForceNew: true,
Description: "Any changes to these values will result in recreating the resource instead of updating.",
},
"update_data": {
Type: schema.TypeString,
Optional: true,
Description: "Valid JSON data to pass during to update 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("update_data attribute is invalid JSON: %v", err))
}
}
return warns, errs
},
},
"destroy_data": {
Type: schema.TypeString,
Optional: true,
Description: "Valid JSON data to pass during to destroy 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("destroy_data attribute is invalid JSON: %v", err))
}
}
return warns, errs
},
},
}, /* End schema */

}
Expand Down Expand Up @@ -365,9 +399,15 @@ func buildAPIObjectOpts(d *schema.ResourceData) (*apiObjectOpts, error) {
if v, ok := d.GetOk("update_method"); ok {
opts.updateMethod = v.(string)
}
if v, ok := d.GetOk("update_data"); ok {
opts.updateData = v.(string)
}
if v, ok := d.GetOk("destroy_method"); ok {
opts.destroyMethod = v.(string)
}
if v, ok := d.GetOk("destroy_data"); ok {
opts.destroyData = v.(string)
}
if v, ok := d.GetOk("destroy_path"); ok {
opts.deletePath = v.(string)
}
Expand Down