Skip to content

Commit 5df62dc

Browse files
committed
Adding support for 'waiting until resource is ready' on create
This adds a feature where the provider waits until a resource is ready. To determine that two new schema parameters were added: * create_ready_key * create_ready_value After creating the resource and if set, the object is repeatedly read and validated using these two parameters. As soon as the value corresponds to the resource value at key the provider will deem the resource ready and continue execution. The default timeout for checking readiness is 10 minutes. Use Terraform's 'timeouts' argument to override. Issue: #33
1 parent b881c1c commit 5df62dc

File tree

5 files changed

+136
-52
lines changed

5 files changed

+136
-52
lines changed

docs/resources/object.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ description: |-
2222

2323
- **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)
2424
- **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`.
25+
- **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.
26+
- **create_ready_value** (String, Optional) The value at `create_ready_key` indicating that a resource has been successfully created.
2527
- **debug** (Boolean, Optional) Whether to emit verbose debug output while working with the API object on the server.
2628
- **destroy_data** (String, Optional) Valid JSON object to pass during to destroy requests.
2729
- **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)

fakeserver/fakeserver.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"log"
88
"net/http"
99
"os"
10+
"strconv"
1011
"strings"
1112
"time"
1213
)
@@ -174,6 +175,15 @@ func (svr *Fakeserver) handleAPIObject(w http.ResponseWriter, r *http.Request) {
174175
}
175176
return
176177
}
178+
179+
/* Simulating asynchronous endpoint */
180+
if _, ok := obj["count_down"]; ok && r.Method == "GET" {
181+
i, _ := strconv.Atoi(obj["count_down"].(string))
182+
if i > 0 {
183+
obj["count_down"] = strconv.Itoa(i - 1)
184+
}
185+
}
186+
177187
/* if data was sent, parse the data */
178188
if string(b) != "" {
179189
if svr.debug {

restapi/api_object.go

Lines changed: 60 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -12,43 +12,47 @@ import (
1212
)
1313

1414
type apiObjectOpts struct {
15-
path string
16-
getPath string
17-
postPath string
18-
putPath string
19-
createMethod string
20-
readMethod string
21-
updateMethod string
22-
updateData string
23-
destroyMethod string
24-
destroyData string
25-
deletePath string
26-
searchPath string
27-
queryString string
28-
debug bool
29-
readSearch map[string]string
30-
id string
31-
idAttribute string
32-
data string
15+
path string
16+
getPath string
17+
postPath string
18+
putPath string
19+
createMethod string
20+
readMethod string
21+
updateMethod string
22+
updateData string
23+
destroyMethod string
24+
destroyData string
25+
deletePath string
26+
searchPath string
27+
queryString string
28+
debug bool
29+
createReadyKey string
30+
createReadyValue string
31+
readSearch map[string]string
32+
id string
33+
idAttribute string
34+
data string
3335
}
3436

3537
/*APIObject is the state holding struct for a restapi_object resource*/
3638
type APIObject struct {
37-
apiClient *APIClient
38-
getPath string
39-
postPath string
40-
putPath string
41-
createMethod string
42-
readMethod string
43-
updateMethod string
44-
destroyMethod string
45-
deletePath string
46-
searchPath string
47-
queryString string
48-
debug bool
49-
readSearch map[string]string
50-
id string
51-
idAttribute string
39+
apiClient *APIClient
40+
getPath string
41+
postPath string
42+
putPath string
43+
createMethod string
44+
readMethod string
45+
updateMethod string
46+
destroyMethod string
47+
deletePath string
48+
searchPath string
49+
queryString string
50+
debug bool
51+
createReadyKey string
52+
createReadyValue string
53+
readSearch map[string]string
54+
id string
55+
idAttribute string
5256

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

110114
obj := APIObject{
111-
apiClient: iClient,
112-
getPath: opts.getPath,
113-
postPath: opts.postPath,
114-
putPath: opts.putPath,
115-
createMethod: opts.createMethod,
116-
readMethod: opts.readMethod,
117-
updateMethod: opts.updateMethod,
118-
destroyMethod: opts.destroyMethod,
119-
deletePath: opts.deletePath,
120-
searchPath: opts.searchPath,
121-
queryString: opts.queryString,
122-
debug: opts.debug,
123-
readSearch: opts.readSearch,
124-
id: opts.id,
125-
idAttribute: opts.idAttribute,
126-
data: make(map[string]interface{}),
127-
updateData: make(map[string]interface{}),
128-
destroyData: make(map[string]interface{}),
129-
apiData: make(map[string]interface{}),
115+
apiClient: iClient,
116+
getPath: opts.getPath,
117+
postPath: opts.postPath,
118+
putPath: opts.putPath,
119+
createMethod: opts.createMethod,
120+
readMethod: opts.readMethod,
121+
updateMethod: opts.updateMethod,
122+
destroyMethod: opts.destroyMethod,
123+
deletePath: opts.deletePath,
124+
searchPath: opts.searchPath,
125+
queryString: opts.queryString,
126+
debug: opts.debug,
127+
createReadyKey: opts.createReadyKey,
128+
createReadyValue: opts.createReadyValue,
129+
readSearch: opts.readSearch,
130+
id: opts.id,
131+
idAttribute: opts.idAttribute,
132+
data: make(map[string]interface{}),
133+
updateData: make(map[string]interface{}),
134+
destroyData: make(map[string]interface{}),
135+
apiData: make(map[string]interface{}),
130136
}
131137

132138
if opts.data != "" {
@@ -200,6 +206,8 @@ func (obj *APIObject) toString() string {
200206
buffer.WriteString(fmt.Sprintf("update_method: %s\n", obj.updateMethod))
201207
buffer.WriteString(fmt.Sprintf("destroy_method: %s\n", obj.destroyMethod))
202208
buffer.WriteString(fmt.Sprintf("debug: %t\n", obj.debug))
209+
buffer.WriteString(fmt.Sprintf("create_ready_key: %s\n", obj.createReadyKey))
210+
buffer.WriteString(fmt.Sprintf("create_ready_value: %s\n", obj.createReadyValue))
203211
buffer.WriteString(fmt.Sprintf("read_search: %s\n", spew.Sdump(obj.readSearch)))
204212
buffer.WriteString(fmt.Sprintf("data: %s\n", spew.Sdump(obj.data)))
205213
buffer.WriteString(fmt.Sprintf("update_data: %s\n", spew.Sdump(obj.updateData)))

restapi/resource_api_object.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ import (
77
"runtime"
88
"strconv"
99
"strings"
10+
"time"
1011

12+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
1113
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
1214
)
1315

@@ -104,6 +106,16 @@ func resourceRestAPI() *schema.Resource {
104106
Description: "Whether to emit verbose debug output while working with the API object on the server.",
105107
Optional: true,
106108
},
109+
"create_ready_key": {
110+
Type: schema.TypeString,
111+
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.",
112+
Optional: true,
113+
},
114+
"create_ready_value": {
115+
Type: schema.TypeString,
116+
Description: "The value at `create_ready_key` indicating that a resource has been successfully created.",
117+
Optional: true,
118+
},
107119
"read_search": {
108120
Type: schema.TypeMap,
109121
Description: "Custom search for `read_path`. This map will take `search_key`, `search_value`, `results_key` and `query_string` (see datasource config documentation)",
@@ -178,6 +190,9 @@ func resourceRestAPI() *schema.Resource {
178190
},
179191
}, /* End schema */
180192

193+
Timeouts: &schema.ResourceTimeout{
194+
Create: schema.DefaultTimeout(10 * time.Minute),
195+
},
181196
}
182197
}
183198

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

244259
err = obj.createObject()
260+
if err != nil {
261+
return nil
262+
}
263+
264+
err = resource.Retry(d.Timeout(schema.TimeoutCreate), func() *resource.RetryError {
265+
if obj.createReadyKey == "" || obj.createReadyValue == "" {
266+
return nil
267+
}
268+
269+
err = obj.readObject()
270+
if err != nil {
271+
return resource.NonRetryableError(err)
272+
} else if obj.id == "" {
273+
return resource.NonRetryableError(fmt.Errorf("cannot evaluate readiness unless the ID has been set"))
274+
}
275+
276+
readyValue, err := GetObjectAtKey(obj.apiData, obj.createReadyKey, obj.debug)
277+
if err != nil {
278+
return resource.NonRetryableError(err)
279+
}
280+
281+
if fmt.Sprint(readyValue) == obj.createReadyValue {
282+
/* Resource is ready and we can exit the retry loop */
283+
return nil
284+
} else {
285+
return resource.RetryableError(fmt.Errorf("resource not yet ready - current value: %s", readyValue))
286+
}
287+
})
245288
if err == nil {
246289
/* Setting terraform ID tells terraform the object was created or it exists */
247290
d.SetId(obj.id)
@@ -415,6 +458,9 @@ func buildAPIObjectOpts(d *schema.ResourceData) (*apiObjectOpts, error) {
415458
opts.queryString = v.(string)
416459
}
417460

461+
opts.createReadyKey = d.Get("create_ready_key").(string)
462+
opts.createReadyValue = d.Get("create_ready_value").(string)
463+
418464
readSearch := expandReadSearch(d.Get("read_search").(map[string]interface{}))
419465
opts.readSearch = readSearch
420466

restapi/resource_api_object_test.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,24 @@ func TestAccRestApiObject_Basic(t *testing.T) {
100100
resource.TestCheckResourceAttrSet("restapi_object.Bar", "api_data.config"),
101101
),
102102
},
103+
104+
{
105+
Config: generateTestResource(
106+
"WaitForReady",
107+
`{ "id": "5678", "count_down": "4" }`,
108+
map[string]interface{}{
109+
"create_ready_key": "count_down",
110+
"create_ready_value": "0",
111+
},
112+
),
113+
Check: resource.ComposeTestCheckFunc(
114+
testAccCheckRestapiObjectExists("restapi_object.WaitForReady", "5678", client),
115+
resource.TestCheckResourceAttr("restapi_object.WaitForReady", "id", "5678"),
116+
resource.TestCheckResourceAttr("restapi_object.WaitForReady", "api_data.count_down", "0"),
117+
resource.TestCheckResourceAttr("restapi_object.WaitForReady", "api_response", "{\"count_down\":\"0\",\"id\":\"5678\"}"),
118+
resource.TestCheckResourceAttr("restapi_object.WaitForReady", "create_response", "{\"count_down\":\"0\",\"id\":\"5678\"}"),
119+
),
120+
},
103121
},
104122
})
105123

0 commit comments

Comments
 (0)