diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..88c31e24 --- /dev/null +++ b/Makefile @@ -0,0 +1,25 @@ +# Change these variables as necessary. +MAIN_PACKAGE_PATH := ./ +BINARY_NAME := terraform-provider-restapi +GO := go +GO_VERSION ?= 1.21 + +# ==================================================================================== # +# DEVELOPMENT +# ==================================================================================== # + +## test: run all tests +.PHONY: test +test: + bash ./scripts/set-local-testing.rc + bash ./scripts/test.sh + +## build: build the application +.PHONY: build +build: + # Include additional build steps compilation here... + $(GO) build -o=${BINARY_NAME}.o ${MAIN_PACKAGE_PATH} + +.PHONY: clean +clean : + rm ${BINARY_NAME}.o diff --git a/docs/index.md b/docs/index.md index 38b18572..6a74c470 100644 --- a/docs/index.md +++ b/docs/index.md @@ -46,7 +46,7 @@ provider "restapi" { - `debug` (Boolean) Enabling this will cause lots of debug information to be printed to STDOUT by the API client. - `destroy_method` (String) Defaults to `DELETE`. The HTTP method used to DELETE objects of this type on the API server. - `headers` (Map of String) 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) 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` (String) 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"}}`. Use `*` if the response from your API is a non JSON object, the response (i.e `1234` or `my-string`) will be used as the obj ID. - `insecure` (Boolean) When using https, this disables TLS verification of the host. - `key_file` (String) 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) 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. diff --git a/fakeserver/fakeserver.go b/fakeserver/fakeserver.go index 8a16a3d5..f22fa366 100644 --- a/fakeserver/fakeserver.go +++ b/fakeserver/fakeserver.go @@ -212,10 +212,20 @@ func (svr *Fakeserver) handleAPIObject(w http.ResponseWriter, r *http.Request) { } svr.objects[id] = obj - /* Coax the data we were sent back to JSON and send it to the user */ - b, _ := json.Marshal(obj) - w.Write(b) - return + /* Edge case to test a response from the server as not a JSON object */ + if val, ok := obj["No_json"]; ok { + if val == true { + log.Printf("fakeserver.go: Returning a non-JSON response\n") + b, _ := json.Marshal(obj["Id"]) + w.Write(b) + return + } + } else { + /* Coax the data we were sent back to JSON and send it to the user */ + b, _ := json.Marshal(obj) + w.Write(b) + return + } } /* No data was sent... must be just a retrieval */ if svr.debug { diff --git a/restapi/api_object.go b/restapi/api_object.go index c0e56ebe..23f668f2 100644 --- a/restapi/api_object.go +++ b/restapi/api_object.go @@ -311,8 +311,37 @@ func (obj *APIObject) createObject() error { obj.apiClient.writeReturnsObject, obj.apiClient.createReturnsObject) } err = obj.updateState(resultString) + + /* Checking if the response is not a normal JSON but probabbly an int or string and the id_attribute is set to '*' + Setting that response value as the ID */ + var result interface{} + err = json.Unmarshal([]byte(resultString), &result) + if err != nil { + return fmt.Errorf("internal validation failed; couldnt unmarshal response from API: %s", err) + } + + if _, ok := result.(map[string]interface{}); !ok { + /*Check for not json responses like plain strings or ints */ + var id string + switch tp := result.(type) { + case string: + id = fmt.Sprintf("%v", result) + case float64: + id = fmt.Sprintf("%.0f", result) + default: + fmt.Printf("api_object.go: Falling default response type is '%T'\n", tp) + } + + if obj.idAttribute == "*" { + if obj.debug { + log.Printf("api_object.go: Getting ID from response as id_attribute is set to '*'. Response is '%v'\n", result) + } + obj.id = id + } + } + /* Yet another failsafe. In case something terrible went wrong internally, - bail out so the user at least knows that the ID did not get set. */ + bail out so the user at least knows that the ID did not get set. */ if obj.id == "" { return fmt.Errorf("internal validation failed; object ID is not set, but *may* have been created; this should never happen") } diff --git a/restapi/api_object_test.go b/restapi/api_object_test.go index 39ed5ef7..4c1ff67e 100644 --- a/restapi/api_object_test.go +++ b/restapi/api_object_test.go @@ -322,6 +322,53 @@ func TestAPIObject(t *testing.T) { } }) + /* Crete an oject with no JSON response, saving int or string as ID */ + t.Run("create_object_no_json_response", func(t *testing.T) { + var err error + if testDebug { + log.Printf("api_object_test.go: Testing create_object() with no JSON response") + } + + objectOpts := &apiObjectOpts{ + path: "/api/objects", + debug: apiObjectDebug, + data: `{ + "Test_case": "no_JSON_response", + "Id": "6", + "No_json": true, + "Name": "cat" + }`, + } + // Important to set the new idAttribute to "*" for non JSON responses + client.idAttribute = "*" + + object, err := NewAPIObject(client, objectOpts) + if err != nil { + t.Fatalf("api_object_test.go: Failed to create new api_object") + } + + err = object.createObject() + if err != nil { + t.Fatalf("api_object_test.go: Failed in create_object() test: %s", err) + } + + if object.id != "6" { + t.Errorf("expected populated object id from creation to be %s but got %s", "6", object.id) + } + + object.data["Name"] = "dog" + object.updateObject() + + if object.data["Name"] != "dog" { + t.Fatalf("api_object_test.go: Failed to update 'Name' field. Expected it to be '%s' but it is '%s'\nFull obj: %+v\n", "dog", object.data["Name"], object) + } + + if testDebug { + log.Printf("api_object_test.go: Object created %v", object) + } + + }) + if testDebug { log.Println("api_object_test.go: Stopping HTTP server") }