Skip to content
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
1 change: 1 addition & 0 deletions docs/data-sources/object.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ description: |-
- **debug** (Boolean, Optional) Whether to emit verbose debug output while working with the API object on the server.
- **id** (String, Optional) The ID of this resource.
- **id_attribute** (String, Optional) Defaults to `id_attribute` set on the provider. Allows per-resource override of `id_attribute` (see `id_attribute` provider config documentation)
- **id_attribute_url** (Boolean, Optional) Defaults to `id_attribute_url` set on the provider. Allows per-resource override of `id_attribute_url` (see `id_attribute_url` provider config documentation)
- **query_string** (String, Optional) An optional query string to send when performing the search.
- **read_query_string** (String, Optional) Defaults to `query_string` set on data source. This key allows setting a different or empty query string for reading the object.
- **results_key** (String, Optional) When issuing a GET to the path, this JSON key is used to locate the results array. The format is 'field/field/field'. Example: 'results/values'. If omitted, it is assumed the results coming back are already an array and are to be used exactly as-is.
Expand Down
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ description: |-
- **destroy_method** (String, Optional) Defaults to `DELETE`. The HTTP method used to DELETE objects of this type on the API server.
- **headers** (Map of String, Optional) A map of header names and values to set on all outbound requests. This is useful if you want to use a script via the 'external' provider or provide a pre-approved token or change Content-Type from `application/json`. If `username` and `password` are set and Authorization is one of the headers defined here, the BASIC auth credentials take precedence.
- **id_attribute** (String, Optional) When set, this key will be used to operate on REST objects. For example, if the ID is set to 'name', changes to the API object will be to http://foo.com/bar/VALUE_OF_NAME. This value may also be a '/'-delimited path to the id attribute if it is multiple levels deep in the data (such as `attributes/id` in the case of an object `{ "attributes": { "id": 1234 }, "config": { "name": "foo", "something": "bar"}}`
- **id_attribute_url** (Boolean, Optional) When set, the key specified in `id_attribute` will be parsed as a URL to extract the ID.
- **insecure** (Boolean, Optional) When using https, this disables TLS verification of the host.
- **key_file** (String, Optional) When set with the cert_file parameter, the provider will load a client certificate as a file for mTLS authentication. Note that this mechanism simply delegates to golang's tls.LoadX509KeyPair which does not support passphrase protected private keys. The most robust security protections available to the key_file are simple file system permissions.
- **key_string** (String, Optional) When set with the cert_string parameter, the provider will load a client certificate as a string for mTLS authentication. Note that this mechanism simply delegates to golang's tls.LoadX509KeyPair which does not support passphrase protected private keys. The most robust security protections available to the key_file are simple file system permissions.
Expand Down
39 changes: 39 additions & 0 deletions restapi/api_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ type apiClientOpt struct {
headers map[string]string
timeout int
idAttribute string
idAttributeUrl bool
createMethod string
readMethod string
updateMethod string
Expand Down Expand Up @@ -60,6 +61,7 @@ type APIClient struct {
password string
headers map[string]string
idAttribute string
idAttributeUrl bool
createMethod string
readMethod string
updateMethod string
Expand Down Expand Up @@ -159,6 +161,7 @@ func NewAPIClient(opt *apiClientOpt) (*APIClient, error) {
password: opt.password,
headers: opt.headers,
idAttribute: opt.idAttribute,
idAttributeUrl: opt.idAttributeUrl,
createMethod: opt.createMethod,
readMethod: opt.readMethod,
updateMethod: opt.updateMethod,
Expand Down Expand Up @@ -197,6 +200,7 @@ func (client *APIClient) toString() string {
buffer.WriteString(fmt.Sprintf("username: %s\n", client.username))
buffer.WriteString(fmt.Sprintf("password: %s\n", client.password))
buffer.WriteString(fmt.Sprintf("id_attribute: %s\n", client.idAttribute))
buffer.WriteString(fmt.Sprintf("id_attribute_url: %t\n", client.idAttributeUrl))
buffer.WriteString(fmt.Sprintf("write_returns_object: %t\n", client.writeReturnsObject))
buffer.WriteString(fmt.Sprintf("create_returns_object: %t\n", client.createReturnsObject))
buffer.WriteString("headers:\n")
Expand Down Expand Up @@ -319,6 +323,41 @@ func (client *APIClient) sendRequest(method string, path string, data string) (s
return body, fmt.Errorf("unexpected response code '%d': %s", resp.StatusCode, body)
}

location := resp.Header.Get("Location")
if client.debug {
log.Printf("api_client.go: Found Location: '%s'", location)
}

if resp.StatusCode == 201 && location != "" {
locationURL, err := url.Parse(location)
if err != nil {
return "", fmt.Errorf("could not parse location header data: '%s'", location)
}

req.URL = locationURL
req.Method = "GET"

resp, err := client.httpClient.Do(req)
if err != nil {
return "", err
}

bodyBytes, err := ioutil.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
return "", err
}

body = strings.TrimPrefix(string(bodyBytes), client.xssiPrefix)
if client.debug {
log.Printf("api_client.go: BODY:\n%s\n", body)
}

if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return body, fmt.Errorf("unexpected response code '%d': %s", resp.StatusCode, body)
}
}

return body, nil

}
137 changes: 79 additions & 58 deletions restapi/api_object.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,43 +12,45 @@ 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
readSearch map[string]string
id string
idAttribute string
idAttributeUrl bool
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
readSearch map[string]string
id string
idAttribute string
idAttributeUrl bool

/* Set internally */
data map[string]interface{} /* Data as managed by the user */
Expand All @@ -65,14 +67,18 @@ func NewAPIObject(iClient *APIClient, opts *apiObjectOpts) (*APIObject, error) {
log.Printf(" id: %s\n", opts.id)
}

/* id_attribute can be set either on the client (to apply for all calls with the server)
or on a per object basis (for only calls to this kind of object).
Permit overridding from the API client here by using the client-wide value only
if a per-object value is not set */
/* id_attribute and id_attribute_url can be set either on the client (to apply
for all calls with the server) or on a per object basis (for only calls to
this kind of object). Permit overridding from the API client here by using
the client-wide value only if a per-object value is not set */
if opts.idAttribute == "" {
opts.idAttribute = iClient.idAttribute
}

if opts.idAttributeUrl {
opts.idAttributeUrl = iClient.idAttributeUrl
}

if opts.createMethod == "" {
opts.createMethod = iClient.createMethod
}
Expand Down Expand Up @@ -108,25 +114,26 @@ 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,
readSearch: opts.readSearch,
id: opts.id,
idAttribute: opts.idAttribute,
idAttributeUrl: opts.idAttributeUrl,
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 @@ -239,6 +246,12 @@ func (obj *APIObject) updateState(state string) error {
if err != nil {
return fmt.Errorf("api_object.go: Error extracting ID from data element: %s", err)
}
if obj.idAttributeUrl {
val, err = parseIdAsURL(val)
if err != nil {
return fmt.Errorf("api_object.go: Error extracting ID as URL: %s", err)
}
}
obj.id = val
} else if obj.debug {
log.Printf("api_object.go: Not updating id. It is already set to '%s'\n", obj.id)
Expand Down Expand Up @@ -268,7 +281,7 @@ func (obj *APIObject) createObject() error {
with the id of whatever gets created, we have no way to know what
the object's id will be. Abandon this attempt */
if obj.id == "" && !obj.apiClient.writeReturnsObject && !obj.apiClient.createReturnsObject {
return fmt.Errorf("provided object does not have an id set and the client is not configured to read the object from a POST or PUT response; please set write_returns_object to true, or include an id in the object's data")
return fmt.Errorf("provided object does not have an id set and the client is not configured to read the object from a POST or PUT response; please set write_returns_object or create_returns_object to true, or include an id in the object's data")
}

b, _ := json.Marshal(obj.data)
Expand Down Expand Up @@ -516,11 +529,19 @@ func (obj *APIObject) findObject(queryString string, searchKey string, searchVal
/* We found our record */
if tmp == searchValue {
objFound = hash
obj.id, err = GetStringAtKey(hash, obj.idAttribute, obj.debug)
val, err := GetStringAtKey(hash, obj.idAttribute, obj.debug)
if err != nil {
return objFound, (fmt.Errorf("failed to find id_attribute '%s' in the record: %s", obj.idAttribute, err))
}

if obj.idAttributeUrl {
val, err = parseIdAsURL(val)
if err != nil {
return objFound, fmt.Errorf("api_object.go: Error extracting ID as URL: %s", err)
}
}
obj.id = val

if obj.debug {
log.Printf("api_object.go: Found ID '%s'", obj.id)
}
Expand Down
21 changes: 21 additions & 0 deletions restapi/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"os"
"strconv"
"strings"
"net/url"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)
Expand All @@ -22,6 +23,25 @@ func setResourceState(obj *APIObject, d *schema.ResourceData) {
d.Set("api_response", obj.apiResponse)
}


func parseIdAsURL(object_url string) (string, error) {
parsedUrl, err := url.Parse(object_url)
if err != nil {
return "", fmt.Errorf("could not parse url: %v", err)
}

segments := strings.Split(strings.TrimRight(parsedUrl.Path, "/"), "/")

object_id := segments[len(segments)-1]

if object_id == "" {
return "", fmt.Errorf("could not extract id from %s", object_url)
}

return object_id, nil
}


/*GetStringAtKey uses GetObjectAtKey to verify the resulting
object is either a JSON string or Number and returns it as a string */
func GetStringAtKey(data map[string]interface{}, path string, debug bool) (string, error) {
Expand All @@ -41,6 +61,7 @@ func GetStringAtKey(data map[string]interface{}, path string, debug bool) (strin
}
}


/*GetObjectAtKey is a handy helper that will dig through a map and find something
at the defined key. The returned data is not type checked
Example:
Expand Down
31 changes: 31 additions & 0 deletions restapi/common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,3 +163,34 @@ func TestGetListStringAtKey(t *testing.T) {
t.Fatalf("Error: Expected '2', but got %s", res)
}
}

func TestParseIdAsURL(t *testing.T) {
testCases := []struct {
url string
expectedID string
shouldError bool
expectedErr string
}{
{"https://example.com/path/to/89d364bc-d738-4594-86ad-c12f4c437500", "89d364bc-d738-4594-86ad-c12f4c437500", false, ""},
{"https://example.com/path/to/1234/", "1234", false, ""},
{"https://example.com", "", true, "could not extract id from https://example.com"},
}

for _, tc := range testCases {
actualID, err := parseIdAsURL(tc.url)

if tc.shouldError {
if err == nil || err.Error() != tc.expectedErr {
t.Errorf("Expected error '%s' but got '%v'", tc.expectedErr, err)
}
} else {
if err != nil {
t.Errorf("Expected no error but got '%v'", err)
}

if actualID != tc.expectedID {
t.Errorf("Expected ID '%s' but got '%s'", tc.expectedID, actualID)
}
}
}
}
7 changes: 7 additions & 0 deletions restapi/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,12 @@ func Provider() *schema.Provider {
DefaultFunc: schema.EnvDefaultFunc("REST_API_ID_ATTRIBUTE", nil),
Description: "When set, this key will be used to operate on REST objects. For example, if the ID is set to 'name', changes to the API object will be to http://foo.com/bar/VALUE_OF_NAME. This value may also be a '/'-delimeted path to the id attribute if it is multple levels deep in the data (such as `attributes/id` in the case of an object `{ \"attributes\": { \"id\": 1234 }, \"config\": { \"name\": \"foo\", \"something\": \"bar\"}}`",
},
"id_attribute_url": {
Type: schema.TypeBool,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("REST_API_ID_ATTRIBUTE_URL", nil),
Description: "When set, the key specified in `id_attribute` will be parsed as a URL to extract the ID.",
},
"create_method": {
Type: schema.TypeString,
DefaultFunc: schema.EnvDefaultFunc("REST_API_CREATE_METHOD", nil),
Expand Down Expand Up @@ -233,6 +239,7 @@ func configureProvider(d *schema.ResourceData) (interface{}, error) {
useCookies: d.Get("use_cookies").(bool),
timeout: d.Get("timeout").(int),
idAttribute: d.Get("id_attribute").(string),
idAttributeUrl: d.Get("id_attribute_url").(bool),
copyKeys: copyKeys,
writeReturnsObject: d.Get("write_returns_object").(bool),
createReturnsObject: d.Get("create_returns_object").(bool),
Expand Down
5 changes: 5 additions & 0 deletions restapi/resource_api_object.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@ func resourceRestAPI() *schema.Resource {
Description: "Defaults to `id_attribute` set on the provider. Allows per-resource override of `id_attribute` (see `id_attribute` provider config documentation)",
Optional: true,
},
"id_attribute_url": {
Type: schema.TypeString,
Description: "Defaults to `id_attribute_url` set on the provider. Allows per-resource override of `id_attribute_url` (see `id_attribute_url` provider config documentation)",
Optional: true,
},
"object_id": {
Type: schema.TypeString,
Description: "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.",
Expand Down