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

Adding support for 'waiting until resource is ready' on create #137

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
2 changes: 2 additions & 0 deletions docs/resources/object.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ description: |-

- **create_method** (String, Optional) Defaults to `create_method` set on the provider. Allows per-resource override of `create_method` (see `create_method` provider config documentation)
- **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`.
- **create_ready_key** (String, Optional) The key to observe during resource creation. As long as its value is not equal to `create_ready_value` the resource is considered as pending. Similar to other configurable keys, the value may be in the format of 'field/field/field' to search for data deeper in the returned object.
- **create_ready_value** (String, Optional) The value at `create_ready_key` indicating that a resource has been successfully created.
- **debug** (Boolean, Optional) Whether to emit verbose debug output while working with the API object on the server.
- **destroy_data** (String, Optional) Valid JSON object to pass during to destroy requests.
- **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)
Expand Down
10 changes: 10 additions & 0 deletions fakeserver/fakeserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"log"
"net/http"
"os"
"strconv"
"strings"
"time"
)
Expand Down Expand Up @@ -174,6 +175,15 @@ func (svr *Fakeserver) handleAPIObject(w http.ResponseWriter, r *http.Request) {
}
return
}

/* Simulating asynchronous endpoint */
if _, ok := obj["count_down"]; ok && r.Method == "GET" {
i, _ := strconv.Atoi(obj["count_down"].(string))
if i > 0 {
obj["count_down"] = strconv.Itoa(i - 1)
}
}

/* if data was sent, parse the data */
if string(b) != "" {
if svr.debug {
Expand Down
112 changes: 60 additions & 52 deletions restapi/api_object.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,43 +12,47 @@ import (
)

type apiObjectOpts struct {
path string
getPath string
postPath string
putPath string
createMethod string
readMethod string
updateMethod string
updateData string
destroyMethod string
destroyData string
deletePath string
searchPath string
queryString string
debug bool
readSearch map[string]string
id string
idAttribute string
data string
path string
getPath string
postPath string
putPath string
createMethod string
readMethod string
updateMethod string
updateData string
destroyMethod string
destroyData string
deletePath string
searchPath string
queryString string
debug bool
createReadyKey string
createReadyValue string
readSearch map[string]string
id string
idAttribute string
data string
}

/*APIObject is the state holding struct for a restapi_object resource*/
type APIObject struct {
apiClient *APIClient
getPath string
postPath string
putPath string
createMethod string
readMethod string
updateMethod string
destroyMethod string
deletePath string
searchPath string
queryString string
debug bool
readSearch map[string]string
id string
idAttribute string
apiClient *APIClient
getPath string
postPath string
putPath string
createMethod string
readMethod string
updateMethod string
destroyMethod string
deletePath string
searchPath string
queryString string
debug bool
createReadyKey string
createReadyValue string
readSearch map[string]string
id string
idAttribute string

/* Set internally */
data map[string]interface{} /* Data as managed by the user */
Expand Down Expand Up @@ -108,25 +112,27 @@ func NewAPIObject(iClient *APIClient, opts *apiObjectOpts) (*APIObject, error) {
}

obj := APIObject{
apiClient: iClient,
getPath: opts.getPath,
postPath: opts.postPath,
putPath: opts.putPath,
createMethod: opts.createMethod,
readMethod: opts.readMethod,
updateMethod: opts.updateMethod,
destroyMethod: opts.destroyMethod,
deletePath: opts.deletePath,
searchPath: opts.searchPath,
queryString: opts.queryString,
debug: opts.debug,
readSearch: opts.readSearch,
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{}),
apiClient: iClient,
getPath: opts.getPath,
postPath: opts.postPath,
putPath: opts.putPath,
createMethod: opts.createMethod,
readMethod: opts.readMethod,
updateMethod: opts.updateMethod,
destroyMethod: opts.destroyMethod,
deletePath: opts.deletePath,
searchPath: opts.searchPath,
queryString: opts.queryString,
debug: opts.debug,
createReadyKey: opts.createReadyKey,
createReadyValue: opts.createReadyValue,
readSearch: opts.readSearch,
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{}),
}

if opts.data != "" {
Expand Down Expand Up @@ -200,6 +206,8 @@ func (obj *APIObject) toString() string {
buffer.WriteString(fmt.Sprintf("update_method: %s\n", obj.updateMethod))
buffer.WriteString(fmt.Sprintf("destroy_method: %s\n", obj.destroyMethod))
buffer.WriteString(fmt.Sprintf("debug: %t\n", obj.debug))
buffer.WriteString(fmt.Sprintf("create_ready_key: %s\n", obj.createReadyKey))
buffer.WriteString(fmt.Sprintf("create_ready_value: %s\n", obj.createReadyValue))
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)))
Expand Down
46 changes: 46 additions & 0 deletions restapi/resource_api_object.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import (
"runtime"
"strconv"
"strings"
"time"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)

Expand Down Expand Up @@ -104,6 +106,16 @@ func resourceRestAPI() *schema.Resource {
Description: "Whether to emit verbose debug output while working with the API object on the server.",
Optional: true,
},
"create_ready_key": {
Type: schema.TypeString,
Description: "The key to observe during resource creation. As long as its value is not equal to `create_ready_value` the resource is considered as pending. Similar to other configurable keys, the value may be in the format of 'field/field/field' to search for data deeper in the returned object.",
Optional: true,
},
"create_ready_value": {
Type: schema.TypeString,
Description: "The value at `create_ready_key` indicating that a resource has been successfully created.",
Optional: true,
},
"read_search": {
Type: schema.TypeMap,
Description: "Custom search for `read_path`. This map will take `search_key`, `search_value`, `results_key` and `query_string` (see datasource config documentation)",
Expand Down Expand Up @@ -178,6 +190,9 @@ func resourceRestAPI() *schema.Resource {
},
}, /* End schema */

Timeouts: &schema.ResourceTimeout{
Create: schema.DefaultTimeout(10 * time.Minute),
},
}
}

Expand Down Expand Up @@ -242,6 +257,34 @@ func resourceRestAPICreate(d *schema.ResourceData, meta interface{}) error {
log.Printf("resource_api_object.go: Create routine called. Object built:\n%s\n", obj.toString())

err = obj.createObject()
if err != nil {
return nil
}

err = resource.Retry(d.Timeout(schema.TimeoutCreate), func() *resource.RetryError {
if obj.createReadyKey == "" || obj.createReadyValue == "" {
return nil
}

err = obj.readObject()
if err != nil {
return resource.NonRetryableError(err)
} else if obj.id == "" {
return resource.NonRetryableError(fmt.Errorf("cannot evaluate readiness unless the ID has been set"))
}

readyValue, err := GetObjectAtKey(obj.apiData, obj.createReadyKey, obj.debug)
if err != nil {
return resource.NonRetryableError(err)
}

if fmt.Sprint(readyValue) == obj.createReadyValue {
/* Resource is ready and we can exit the retry loop */
return nil
} else {
return resource.RetryableError(fmt.Errorf("resource not yet ready - current value: %s", readyValue))
}
})
if err == nil {
/* Setting terraform ID tells terraform the object was created or it exists */
d.SetId(obj.id)
Expand Down Expand Up @@ -415,6 +458,9 @@ func buildAPIObjectOpts(d *schema.ResourceData) (*apiObjectOpts, error) {
opts.queryString = v.(string)
}

opts.createReadyKey = d.Get("create_ready_key").(string)
opts.createReadyValue = d.Get("create_ready_value").(string)

readSearch := expandReadSearch(d.Get("read_search").(map[string]interface{}))
opts.readSearch = readSearch

Expand Down
18 changes: 18 additions & 0 deletions restapi/resource_api_object_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,24 @@ func TestAccRestApiObject_Basic(t *testing.T) {
resource.TestCheckResourceAttrSet("restapi_object.Bar", "api_data.config"),
),
},

{
Config: generateTestResource(
"WaitForReady",
`{ "id": "5678", "count_down": "4" }`,
map[string]interface{}{
"create_ready_key": "count_down",
"create_ready_value": "0",
},
),
Check: resource.ComposeTestCheckFunc(
testAccCheckRestapiObjectExists("restapi_object.WaitForReady", "5678", client),
resource.TestCheckResourceAttr("restapi_object.WaitForReady", "id", "5678"),
resource.TestCheckResourceAttr("restapi_object.WaitForReady", "api_data.count_down", "0"),
resource.TestCheckResourceAttr("restapi_object.WaitForReady", "api_response", "{\"count_down\":\"0\",\"id\":\"5678\"}"),
resource.TestCheckResourceAttr("restapi_object.WaitForReady", "create_response", "{\"count_down\":\"0\",\"id\":\"5678\"}"),
),
},
},
})

Expand Down