From af048bc08ec5a51182371d970a17439fa994c2a3 Mon Sep 17 00:00:00 2001 From: dikhan Date: Fri, 21 Sep 2018 16:07:34 -0700 Subject: [PATCH 1/7] update example to include object type support --- .../swaggercodegen/api/api/content_delivery_network.go | 2 ++ examples/swaggercodegen/api/resources/swagger.yaml | 7 +++++++ examples/swaggercodegen/main.tf | 5 +++++ 3 files changed, 14 insertions(+) diff --git a/examples/swaggercodegen/api/api/content_delivery_network.go b/examples/swaggercodegen/api/api/content_delivery_network.go index 2cbd3b95a..aa7d95bb9 100644 --- a/examples/swaggercodegen/api/api/content_delivery_network.go +++ b/examples/swaggercodegen/api/api/content_delivery_network.go @@ -25,4 +25,6 @@ type ContentDeliveryNetwork struct { ExampleNumber float32 `json:"exampleNumber,omitempty"` ExampleBoolean bool `json:"example_boolean,omitempty"` + + ObjectProperty *ObjectProperty `json:"object_property,omitempty"` } diff --git a/examples/swaggercodegen/api/resources/swagger.yaml b/examples/swaggercodegen/api/resources/swagger.yaml index 2b40d1415..4c23862f5 100644 --- a/examples/swaggercodegen/api/resources/swagger.yaml +++ b/examples/swaggercodegen/api/resources/swagger.yaml @@ -291,6 +291,13 @@ definitions: x-terraform-field-name: betterExampleNumberFieldName # overriding exampleNumber with a different name 'betterExampleNumberFieldName'; the preferred name is not terraform compliant either so the provider will perform the name conversion automatically when translating the name into the provider resource configuration and when saving the field into the state file example_boolean: type: boolean + object_property: + $ref: "#/definitions/ObjectProperty" + ObjectProperty: + type: object + properties: + message: + type: string LBV1: type: "object" required: diff --git a/examples/swaggercodegen/main.tf b/examples/swaggercodegen/main.tf index addfd7b93..40b71a4ac 100644 --- a/examples/swaggercodegen/main.tf +++ b/examples/swaggercodegen/main.tf @@ -13,6 +13,11 @@ resource "swaggercodegen_cdn_v1" "my_cdn" { example_int = 12 better_example_number_field_name = 1.12 example_boolean = true + + object_property = { + message = "" + } + } resource "swaggercodegen_lbs_v1" "my_lb" { From 8b7c079d4e8f02033fcd4eae1a6ec6e4a5c151cb Mon Sep 17 00:00:00 2001 From: dikhan Date: Fri, 21 Sep 2018 16:08:31 -0700 Subject: [PATCH 2/7] add object_property model --- .../swaggercodegen/api/api/object_property.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 examples/swaggercodegen/api/api/object_property.go diff --git a/examples/swaggercodegen/api/api/object_property.go b/examples/swaggercodegen/api/api/object_property.go new file mode 100644 index 000000000..007246143 --- /dev/null +++ b/examples/swaggercodegen/api/api/object_property.go @@ -0,0 +1,16 @@ +/* + * Dummy Service Provider generated using 'swaggercodegen' that has two resources 'cdns' and 'lbs' which are terraform compliant + * + * This service provider allows the creation of fake 'cdns' and 'lbs' resources + * + * API version: 1.0.0 + * Contact: apiteam@serviceprovider.io + * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git) + */ + +package api + +type ObjectProperty struct { + + Message string `json:"message,omitempty"` +} From 44ec8951686e532b3dbef7e78b868334494a9522 Mon Sep 17 00:00:00 2001 From: dikhan Date: Tue, 25 Sep 2018 15:51:00 -0700 Subject: [PATCH 3/7] update examples with various forms of object properties - readoonly object property with nested schema - required object property with ref to a schema - object support for statues --- examples/swaggercodegen/api/api/cdn.go | 24 ++++++++++++------ .../api/api/content_delivery_network.go | 8 +++--- ...etwork_v1_object_nested_scheme_property.go | 16 ++++++++++++ examples/swaggercodegen/api/api/lb_api.go | 10 +++++--- examples/swaggercodegen/api/api/lbv1.go | 2 ++ examples/swaggercodegen/api/api/status.go | 18 +++++++++++++ .../swaggercodegen/api/resources/swagger.yaml | 25 ++++++++++++++++--- examples/swaggercodegen/main.tf | 2 +- 8 files changed, 87 insertions(+), 18 deletions(-) create mode 100644 examples/swaggercodegen/api/api/content_delivery_network_v1_object_nested_scheme_property.go create mode 100644 examples/swaggercodegen/api/api/status.go diff --git a/examples/swaggercodegen/api/api/cdn.go b/examples/swaggercodegen/api/api/cdn.go index e2beafad1..a4cd5c69b 100644 --- a/examples/swaggercodegen/api/api/cdn.go +++ b/examples/swaggercodegen/api/api/cdn.go @@ -9,7 +9,7 @@ import ( "log" ) -var db = map[string]*ContentDeliveryNetwork{} +var db = map[string]*ContentDeliveryNetworkV1{} func ContentDeliveryNetworkCreateV1(w http.ResponseWriter, r *http.Request) { if AuthenticateRequest(r, w) != nil { @@ -17,13 +17,16 @@ func ContentDeliveryNetworkCreateV1(w http.ResponseWriter, r *http.Request) { } xRequestID := r.Header.Get("X-Request-ID") log.Printf("Header [X-Request-ID]: %s", xRequestID) - cdn := &ContentDeliveryNetwork{} + cdn := &ContentDeliveryNetworkV1{} err := readRequest(r, cdn) if err != nil { sendErrorResponse(http.StatusBadRequest, err.Error(), w) return } cdn.Id = uuid.New() + cdn.ObjectNestedSchemeProperty = &ContentDeliveryNetworkV1ObjectNestedSchemeProperty{ + Name: "autogenerated name", + } db[cdn.Id] = cdn sendResponse(http.StatusCreated, w, cdn) } @@ -49,15 +52,22 @@ func ContentDeliveryNetworkUpdateV1(w http.ResponseWriter, r *http.Request) { sendErrorResponse(http.StatusNotFound, err.Error(), w) return } - newCDN := &ContentDeliveryNetwork{} + newCDN := &ContentDeliveryNetworkV1{} err = readRequest(r, newCDN) if err != nil { sendErrorResponse(http.StatusBadRequest, err.Error(), w) return } - newCDN.Id = cdn.Id - db[cdn.Id] = newCDN - sendResponse(http.StatusOK, w, newCDN) + + cdn.Ips = newCDN.Ips + cdn.Hostnames = newCDN.Hostnames + cdn.ExampleInt = newCDN.ExampleInt + cdn.ExampleNumber = newCDN.ExampleNumber + cdn.ExampleBoolean = newCDN.ExampleBoolean + cdn.ObjectProperty = newCDN.ObjectProperty + + db[cdn.Id] = cdn + sendResponse(http.StatusOK, w, cdn) } func ContentDeliveryNetworkDeleteV1(w http.ResponseWriter, r *http.Request) { @@ -73,7 +83,7 @@ func ContentDeliveryNetworkDeleteV1(w http.ResponseWriter, r *http.Request) { sendResponse(http.StatusNoContent, w, nil) } -func retrieveCdn(r *http.Request) (*ContentDeliveryNetwork, error) { +func retrieveCdn(r *http.Request) (*ContentDeliveryNetworkV1, error) { id := strings.TrimPrefix(r.URL.Path, "/v1/cdns/") if id == "" { return nil, fmt.Errorf("cdn id path param not provided") diff --git a/examples/swaggercodegen/api/api/content_delivery_network.go b/examples/swaggercodegen/api/api/content_delivery_network.go index aa7d95bb9..1df1c7d62 100644 --- a/examples/swaggercodegen/api/api/content_delivery_network.go +++ b/examples/swaggercodegen/api/api/content_delivery_network.go @@ -10,7 +10,7 @@ package api -type ContentDeliveryNetwork struct { +type ContentDeliveryNetworkV1 struct { Id string `json:"id,omitempty"` @@ -26,5 +26,7 @@ type ContentDeliveryNetwork struct { ExampleBoolean bool `json:"example_boolean,omitempty"` - ObjectProperty *ObjectProperty `json:"object_property,omitempty"` -} + ObjectProperty *ObjectProperty `json:"object_property"` + + ObjectNestedSchemeProperty *ContentDeliveryNetworkV1ObjectNestedSchemeProperty `json:"object_nested_scheme_property,omitempty"` +} \ No newline at end of file diff --git a/examples/swaggercodegen/api/api/content_delivery_network_v1_object_nested_scheme_property.go b/examples/swaggercodegen/api/api/content_delivery_network_v1_object_nested_scheme_property.go new file mode 100644 index 000000000..a64dbb29d --- /dev/null +++ b/examples/swaggercodegen/api/api/content_delivery_network_v1_object_nested_scheme_property.go @@ -0,0 +1,16 @@ +/* + * Dummy Service Provider generated using 'swaggercodegen' that has two resources 'cdns' and 'lbs' which are terraform compliant + * + * This service provider allows the creation of fake 'cdns' and 'lbs' resources + * + * API version: 1.0.0 + * Contact: apiteam@serviceprovider.io + * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git) + */ + +package api + +type ContentDeliveryNetworkV1ObjectNestedSchemeProperty struct { + + Name string `json:"name"` +} diff --git a/examples/swaggercodegen/api/api/lb_api.go b/examples/swaggercodegen/api/api/lb_api.go index 732ad835b..a58e5c4a8 100644 --- a/examples/swaggercodegen/api/api/lb_api.go +++ b/examples/swaggercodegen/api/api/lb_api.go @@ -47,7 +47,9 @@ func LBGetV1(w http.ResponseWriter, r *http.Request) { } func LBCreateV1(w http.ResponseWriter, r *http.Request) { - lb := &Lbv1{} + lb := &Lbv1{ + NewStatus: &Status{}, + } err := readRequest(r, lb) if err != nil { sendErrorResponse(http.StatusBadRequest, err.Error(), w) @@ -146,9 +148,9 @@ func sleepAndDestroyLB(lb *Lbv1, waitTime int32) { } func updateLBStatus(lb *Lbv1, newStatus status) { - oldStatus := lb.Status - lb.Status = string(newStatus) - log.Printf("LB [%s] status updated '%s' => '%s'", lb.Id, oldStatus, newStatus) + oldStatus := lb.NewStatus.Status + lb.NewStatus.Status = string(newStatus) + log.Printf("LB [%s] status updated '%s' => '%s'", lb.Id, oldStatus, lb.NewStatus.Status) } func retrieveLB(r *http.Request) (*Lbv1, error) { diff --git a/examples/swaggercodegen/api/api/lbv1.go b/examples/swaggercodegen/api/api/lbv1.go index 3f3c0747c..3b35a9209 100644 --- a/examples/swaggercodegen/api/api/lbv1.go +++ b/examples/swaggercodegen/api/api/lbv1.go @@ -24,4 +24,6 @@ type Lbv1 struct { // lb resource status Status string `json:"status,omitempty"` + + NewStatus *Status `json:"newStatus,omitempty"` } diff --git a/examples/swaggercodegen/api/api/status.go b/examples/swaggercodegen/api/api/status.go new file mode 100644 index 000000000..1599d039a --- /dev/null +++ b/examples/swaggercodegen/api/api/status.go @@ -0,0 +1,18 @@ +/* + * Dummy Service Provider generated using 'swaggercodegen' that has two resources 'cdns' and 'lbs' which are terraform compliant + * + * This service provider allows the creation of fake 'cdns' and 'lbs' resources + * + * API version: 1.0.0 + * Contact: apiteam@serviceprovider.io + * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git) + */ + +package api + +type Status struct { + + Message string `json:"message,omitempty"` + + Status string `json:"status,omitempty"` +} diff --git a/examples/swaggercodegen/api/resources/swagger.yaml b/examples/swaggercodegen/api/resources/swagger.yaml index 4c23862f5..1b94c4a40 100644 --- a/examples/swaggercodegen/api/resources/swagger.yaml +++ b/examples/swaggercodegen/api/resources/swagger.yaml @@ -267,6 +267,7 @@ definitions: - label - ips - hostnames + - object_property properties: id: type: "string" @@ -292,9 +293,18 @@ definitions: example_boolean: type: boolean object_property: + #type: object - type is optional for properties of object type that use $ref $ref: "#/definitions/ObjectProperty" + object_nested_scheme_property: + type: object # nested properties required type equal object to be considered as object + readOnly: true + properties: + name: + type: string ObjectProperty: type: object + required: + - message properties: message: type: string @@ -314,8 +324,7 @@ definitions: type: "array" items: type: "string" - status: - x-terraform-field-status: true # identifies the field that should be used as status for async operations. This is handy when the field name is not status but some other name the service provider might have chosen and enables the provider to identify the field as the status field that will be used to track progress for the async operations + status: # no longer used, using the object status instead for the sake of testing description: lb resource status type: string readOnly: true @@ -328,12 +337,22 @@ definitions: - delete_in_progress - delete_failed - deleted + newStatus: + $ref: "#/definitions/Status" + x-terraform-field-status: true # identifies the field that should be used as status for async operations. This is handy when the field name is not status but some other name the service provider might have chosen and enables the provider to identify the field as the status field that will be used to track progress for the async operations + readOnly: true timeToProcess: # time that the resource will take to be processed in seconds type: integer default: 60 # it will take two minute to process the resource operation (POST/PUT/READ/DELETE) simulate_failure: # allows user to set it to true and force an error on the API when the given operation (POST/PUT/READ/DELETE) is being performed type: boolean - + Status: + type: object + properties: + message: + type: string + status: + type: string # Schema for error response body Error: type: object diff --git a/examples/swaggercodegen/main.tf b/examples/swaggercodegen/main.tf index 40b71a4ac..3b01410ac 100644 --- a/examples/swaggercodegen/main.tf +++ b/examples/swaggercodegen/main.tf @@ -15,7 +15,7 @@ resource "swaggercodegen_cdn_v1" "my_cdn" { example_boolean = true object_property = { - message = "" + message = "some message news" } } From e194ed128fc75810638a54816ee1c141130b954e Mon Sep 17 00:00:00 2001 From: dikhan Date: Tue, 25 Sep 2018 15:52:53 -0700 Subject: [PATCH 4/7] add helper method GetSchemaDefinition to assist with ref look ups --- openapi/openapiutils/openapi_utils.go | 29 +++++ openapi/openapiutils/openapi_utils_test.go | 123 +++++++++++++++++++++ openapi/terraformutils/terraform_utils.go | 1 - 3 files changed, 152 insertions(+), 1 deletion(-) diff --git a/openapi/openapiutils/openapi_utils.go b/openapi/openapiutils/openapi_utils.go index c48c4fd62..a15871770 100644 --- a/openapi/openapiutils/openapi_utils.go +++ b/openapi/openapiutils/openapi_utils.go @@ -1,12 +1,14 @@ package openapiutils import ( + "fmt" "github.com/dikhan/terraform-provider-openapi/openapi/terraformutils" "github.com/go-openapi/spec" "regexp" "strings" ) +const swaggerResourcePayloadDefinitionRegex = "(\\w+)[^//]*$" const fqdnInURLRegex = `\b(?:(?:[^.-/]{0,1})[\w-]{1,63}[-]{0,1}[.]{1})+(?:[a-zA-Z]{2,63})?|localhost(?:[:]\d+)?\b` const extTfHeader = "x-terraform-header" @@ -113,3 +115,30 @@ func StringExtensionExists(extensions spec.Extensions, key string) (string, bool } return value, exists } + +// getPayloadDefName only supports references to the same document. External references like URLs is not supported at the moment +func getPayloadDefName(ref string) (string, error) { + reg, err := regexp.Compile(swaggerResourcePayloadDefinitionRegex) + if err != nil { + return "", fmt.Errorf("an error occurred while compiling the swaggerResourcePayloadDefinitionRegex regex '%s': %s", swaggerResourcePayloadDefinitionRegex, err) + } + payloadDefName := reg.FindStringSubmatch(ref)[0] + if payloadDefName == "" { + return "", fmt.Errorf("could not find a valid definition name for '%s'", ref) + } + return payloadDefName, nil +} + +// GetSchemaDefinition queries the definitions and tries to find the schema definition for the given ref. If the schema +// definition the ref value is pointing at does not exist and error is returned. Otherwise, the corresponding schema definition is returned. +func GetSchemaDefinition(definitions map[string]spec.Schema, ref string) (*spec.Schema, error) { + payloadDefName, err := getPayloadDefName(ref) + if err != nil { + return nil, err + } + payloadDefinition, exists := definitions[payloadDefName] + if !exists { + return nil, fmt.Errorf("missing schema definition in the swagger file with the supplied ref '%s'", ref) + } + return &payloadDefinition, nil +} diff --git a/openapi/openapiutils/openapi_utils_test.go b/openapi/openapiutils/openapi_utils_test.go index be5c594dc..09e51dc91 100644 --- a/openapi/openapiutils/openapi_utils_test.go +++ b/openapi/openapiutils/openapi_utils_test.go @@ -1,7 +1,9 @@ package openapiutils import ( + "encoding/json" "fmt" + "github.com/go-openapi/loads" "github.com/go-openapi/spec" . "github.com/smartystreets/goconvey/convey" "testing" @@ -380,3 +382,124 @@ func TestStringExtensionExists(t *testing.T) { }) }) } + +func TestGetPayloadDefName(t *testing.T) { + Convey("Given a valid internal definition path", t, func() { + ref := "#/definitions/ContentDeliveryNetworkV1" + // Local Reference use cases + Convey("When getPayloadDefName method is called with a valid internal definition path", func() { + defName, err := getPayloadDefName(ref) + Convey("Then the error returned should be nil", func() { + So(err, ShouldBeNil) + }) + Convey("And the value returned should be true", func() { + So(defName, ShouldEqual, "ContentDeliveryNetworkV1") + }) + }) + }) + + Convey("Given a ref URL (not supported)", t, func() { + ref := "http://path/to/your/resource.json#myElement" + Convey("When getPayloadDefName method is called ", func() { + _, err := getPayloadDefName(ref) + Convey("Then the error returned should not be nil", func() { + So(err, ShouldBeNil) + }) + }) + }) + + Convey("Given an element of the document located on the same server (not supported)", t, func() { + ref := "document.json#/myElement" + // Remote Reference use cases + Convey("When getPayloadDefName method is called with ", func() { + _, err := getPayloadDefName(ref) + Convey("Then the error returned should not be nil", func() { + So(err, ShouldBeNil) + }) + }) + }) + + Convey("Given an element of the document located in the parent folder (not supported)", t, func() { + ref := "../document.json#/myElement" + Convey("When getPayloadDefName method is called with ", func() { + _, err := getPayloadDefName(ref) + Convey("Then the error returned should not be nil", func() { + So(err, ShouldBeNil) + }) + }) + }) + + Convey("Given an specific element of the document stored on the different server (not supported)", t, func() { + ref := "http://path/to/your/resource.json#myElement" + Convey("When getPayloadDefName method is called with ", func() { + _, err := getPayloadDefName(ref) + Convey("Then the error returned should not be nil", func() { + So(err, ShouldBeNil) + }) + }) + }) + + Convey("Given an element of the document located in another folder (not supported)", t, func() { + ref := "../another-folder/document.json#/myElement" + // URL Reference use case + Convey("When getPayloadDefName method is called with ", func() { + _, err := getPayloadDefName(ref) + Convey("Then the error returned should not be nil", func() { + So(err, ShouldBeNil) + }) + }) + }) + + Convey("Given a document on the different server, which uses the same protocol (not supported)", t, func() { + ref := "//anotherserver.com/files/example.json" + Convey("When getPayloadDefName method is called with ", func() { + _, err := getPayloadDefName(ref) + Convey("Then the error returned should not be nil", func() { + So(err, ShouldBeNil) + }) + }) + }) +} + +func TestGetResourcePayloadSchemaDef(t *testing.T) { + Convey("Given a swagger doc", t, func() { + swaggerContent := `swagger: "2.0" +definitions: + Users: + type: "object" + required: + - name + properties: + id: + type: "string" + readOnly: true` + spec := initSwagger(swaggerContent) + Convey("When getResourcePayloadSchemaDef method is called with an operation containing a valid ref: '#/definitions/Users'", func() { + ref := "#/definitions/Users" + resourcePayloadSchemaDef, err := GetSchemaDefinition(spec.Definitions, ref) + Convey("Then the error returned should be nil", func() { + So(err, ShouldBeNil) + }) + Convey("Then the value returned should be a valid schema def", func() { + So(len(resourcePayloadSchemaDef.Type), ShouldEqual, 1) + So(resourcePayloadSchemaDef.Type, ShouldContain, "object") + }) + }) + Convey("When getResourcePayloadSchemaDef method is called with schema that is missing the definition the ref is pointing at", func() { + ref := "#/definitions/NonExistingDef" + _, err := GetSchemaDefinition(spec.Definitions, ref) + Convey("Then the error returned should NOT be nil", func() { + So(err, ShouldNotBeNil) + }) + Convey("And the error message should be", func() { + So(err.Error(), ShouldContainSubstring, "missing schema definition in the swagger file with the supplied ref '#/definitions/NonExistingDef'") + }) + }) + }) +} + +func initSwagger(swaggerContent string) *spec.Swagger { + swagger := json.RawMessage([]byte(swaggerContent)) + d, _ := loads.Analyzed(swagger, "2.0") + return d.Spec() +} diff --git a/openapi/terraformutils/terraform_utils.go b/openapi/terraformutils/terraform_utils.go index df9ee28da..239ec2f3f 100644 --- a/openapi/terraformutils/terraform_utils.go +++ b/openapi/terraformutils/terraform_utils.go @@ -37,6 +37,5 @@ func (t *TerraformUtils) GetTerraformPluginsVendorDir() (string, error) { // Terraform's snake case field name convention (lower case and snake case). func ConvertToTerraformCompliantName(name string) string { compliantName := strcase.ToSnake(name) - log.Printf("[DEBUG] ConvertToTerraformCompliantName - originalName = %s; compliantName = %s)", name, compliantName) return compliantName } From 82551e2c4a001dc29cc3d83101494078d29e541c Mon Sep 17 00:00:00 2001 From: dikhan Date: Tue, 25 Sep 2018 15:54:35 -0700 Subject: [PATCH 5/7] pass asa.d.Spec().Definitions to resourceInfo. This is neeeded so resource info is able to look up ref by object type properties inside the resource schemaDefinition - move getPayloadDefName to utils --- openapi/api_spec_analyser.go | 40 +++++--------------- openapi/api_spec_analyser_test.go | 61 ------------------------------- 2 files changed, 10 insertions(+), 91 deletions(-) diff --git a/openapi/api_spec_analyser.go b/openapi/api_spec_analyser.go index 59941fb67..7b5607355 100644 --- a/openapi/api_spec_analyser.go +++ b/openapi/api_spec_analyser.go @@ -2,6 +2,7 @@ package openapi import ( "fmt" + "github.com/dikhan/terraform-provider-openapi/openapi/openapiutils" "log" "regexp" "strings" @@ -11,7 +12,6 @@ import ( ) const resourceInstanceRegex = "((?:.*)){.*}" -const swaggerResourcePayloadDefinitionRegex = "(\\w+)[^//]*$" // apiSpecAnalyser analyses the swagger doc and provides helper methods to retrieve all the end points that can // be used as terraform resources. These endpoints have to meet certain criteria to be considered eligible resources @@ -36,13 +36,14 @@ func (asa apiSpecAnalyser) getResourcesInfo() (resourcesInfo, error) { } r := resourceInfo{ - basePath: asa.d.BasePath(), - path: resourceRootPath, - host: asa.d.Spec().Host, - httpSchemes: asa.d.Spec().Schemes, - schemaDefinition: *resourcePayloadSchemaDef, - createPathInfo: *resourceRoot, - pathInfo: pathItem, + basePath: asa.d.BasePath(), + path: resourceRootPath, + host: asa.d.Spec().Host, + httpSchemes: asa.d.Spec().Schemes, + schemaDefinition: resourcePayloadSchemaDef, + createPathInfo: *resourceRoot, + pathInfo: pathItem, + schemaDefinitions: asa.d.Spec().Definitions, } if r.shouldIgnoreResource() { @@ -220,15 +221,7 @@ func (asa apiSpecAnalyser) getResourcePayloadSchemaDef(resourceRootPostOperation if err != nil { return nil, err } - payloadDefName, err := asa.getPayloadDefName(ref) - if err != nil { - return nil, err - } - payloadDefinition, exists := asa.d.Spec().Definitions[payloadDefName] - if !exists { - return nil, fmt.Errorf("missing schema definition in the swagger file with the supplied ref '%s'", ref) - } - return &payloadDefinition, nil + return openapiutils.GetSchemaDefinition(asa.d.Spec().Definitions, ref) } func (asa apiSpecAnalyser) getResourcePayloadSchemaRef(resourceRootPostOperation *spec.Operation) (string, error) { @@ -261,19 +254,6 @@ func (asa apiSpecAnalyser) getResourcePayloadSchemaRef(resourceRootPostOperation return payloadDefinitionSchemaRef.Ref.String(), nil } -// getPayloadDefName only supports references to the same document. External references like URLs is not supported at the moment -func (asa apiSpecAnalyser) getPayloadDefName(ref string) (string, error) { - reg, err := regexp.Compile(swaggerResourcePayloadDefinitionRegex) - if err != nil { - return "", fmt.Errorf("an error occurred while compiling the swaggerResourcePayloadDefinitionRegex regex '%s': %s", swaggerResourcePayloadDefinitionRegex, err) - } - payloadDefName := reg.FindStringSubmatch(ref)[0] - if payloadDefName == "" { - return "", fmt.Errorf("could not find a valid definition name for '%s'", ref) - } - return payloadDefName, nil -} - // resourceInstanceRegex loads up the regex specified in const resourceInstanceRegex // If the regex is not able to compile the regular expression the function exists calling os.Exit(1) as // there is the regex is completely busted diff --git a/openapi/api_spec_analyser_test.go b/openapi/api_spec_analyser_test.go index c80df3d52..066007625 100644 --- a/openapi/api_spec_analyser_test.go +++ b/openapi/api_spec_analyser_test.go @@ -66,67 +66,6 @@ func TestResourceInstanceEndPoint(t *testing.T) { }) } -func TestGetPayloadDefName(t *testing.T) { - Convey("Given an apiSpecAnalyser", t, func() { - a := apiSpecAnalyser{} - - // Local Reference use cases - Convey("When getPayloadDefName method is called with a valid internal definition path", func() { - defName, err := a.getPayloadDefName("#/definitions/ContentDeliveryNetworkV1") - Convey("Then the error returned should be nil", func() { - So(err, ShouldBeNil) - }) - Convey("And the value returned should be true", func() { - So(defName, ShouldEqual, "ContentDeliveryNetworkV1") - }) - }) - - Convey("When getPayloadDefName method is called with a URL (not supported)", func() { - _, err := a.getPayloadDefName("http://path/to/your/resource.json#myElement") - Convey("Then the error returned should not be nil", func() { - So(err, ShouldBeNil) - }) - }) - - // Remote Reference use cases - Convey("When getPayloadDefName method is called with an element of the document located on the same server (not supported)", func() { - _, err := a.getPayloadDefName("document.json#/myElement") - Convey("Then the error returned should not be nil", func() { - So(err, ShouldBeNil) - }) - }) - - Convey("When getPayloadDefName method is called with an element of the document located in the parent folder (not supported)", func() { - _, err := a.getPayloadDefName("../document.json#/myElement") - Convey("Then the error returned should not be nil", func() { - So(err, ShouldBeNil) - }) - }) - - Convey("When getPayloadDefName method is called with an specific element of the document stored on the different server (not supported)", func() { - _, err := a.getPayloadDefName("http://path/to/your/resource.json#myElement") - Convey("Then the error returned should not be nil", func() { - So(err, ShouldBeNil) - }) - }) - - // URL Reference use case - Convey("When getPayloadDefName method is called with an element of the document located in another folder (not supported)", func() { - _, err := a.getPayloadDefName("../another-folder/document.json#/myElement") - Convey("Then the error returned should not be nil", func() { - So(err, ShouldBeNil) - }) - }) - - Convey("When getPayloadDefName method is called with document on the different server, which uses the same protocol (not supported)", func() { - _, err := a.getPayloadDefName("//anotherserver.com/files/example.json") - Convey("Then the error returned should not be nil", func() { - So(err, ShouldBeNil) - }) - }) - }) -} - func TestGetResourcePayloadSchemaRef(t *testing.T) { Convey("Given an apiSpecAnalyser", t, func() { a := apiSpecAnalyser{} From ac22a09a6bb2154986e0c0b285b5cea90377aacf Mon Sep 17 00:00:00 2001 From: dikhan Date: Tue, 25 Sep 2018 15:58:25 -0700 Subject: [PATCH 6/7] add support for object types - support for objects when building terraform resource schema - support for objects when building payload data from resource Data - support for objects in status fields - add units tests --- openapi/resource_factory.go | 14 +- openapi/resource_info.go | 153 ++++++-- openapi/resource_info_test.go | 679 +++++++++++++++++++++++++++------- 3 files changed, 672 insertions(+), 174 deletions(-) diff --git a/openapi/resource_factory.go b/openapi/resource_factory.go index da8b00167..d3f9b4772 100644 --- a/openapi/resource_factory.go +++ b/openapi/resource_factory.go @@ -290,7 +290,6 @@ func (r resourceFactory) handlePollingIfConfigured(responsePayload *map[string]i func (r resourceFactory) resourceStateRefreshFunc(resourceLocalData *schema.ResourceData, providerConfig providerConfig) resource.StateRefreshFunc { return func() (interface{}, string, error) { remoteData, err := r.readRemote(resourceLocalData.Id(), providerConfig) - if err != nil { if openapiErr, ok := err.(openapierr.Error); ok { if openapierr.NotFound == openapiErr.Code() { @@ -299,17 +298,10 @@ func (r resourceFactory) resourceStateRefreshFunc(resourceLocalData *schema.Reso } return nil, "", fmt.Errorf("error on retrieving resource '%s' (%s) when waiting: %s", r.resourceInfo.path, resourceLocalData.Id(), err) } - - statusIdentifier, err := r.resourceInfo.getStatusIdentifier() + newStatus, err := r.resourceInfo.getStatusValueFromPayload(remoteData) if err != nil { - return nil, "", fmt.Errorf("error occurred while retrieving status identifier for resource '%s' (%s): %s", r.resourceInfo.path, resourceLocalData.Id(), err) - } - - value, statusIdentifierPresentInResponse := remoteData[statusIdentifier] - if !statusIdentifierPresentInResponse { - return nil, "", fmt.Errorf("response payload received from GET /%s/%s missing the status identifier field", r.resourceInfo.path, resourceLocalData.Id()) + return nil, "", fmt.Errorf("failed to get the status value after receiving response from GET /%s/%s: %s", r.resourceInfo.path, resourceLocalData.Id(), err) } - newStatus := value.(string) log.Printf("[DEBUG] resource '%s' status (%s): %s", r.resourceInfo.path, resourceLocalData.Id(), newStatus) return remoteData, newStatus, nil } @@ -428,6 +420,8 @@ func (r resourceFactory) createPayloadFromLocalStateData(resourceLocalData *sche } if dataValue, ok := r.getResourceDataOKExists(propertyName, resourceLocalData); ok { switch reflect.TypeOf(dataValue).Kind() { + case reflect.Map: + input[propertyName] = dataValue.(map[string]interface{}) case reflect.Slice: input[propertyName] = dataValue.([]interface{}) case reflect.String: diff --git a/openapi/resource_info.go b/openapi/resource_info.go index f13ab0e3e..ec51a305e 100644 --- a/openapi/resource_info.go +++ b/openapi/resource_info.go @@ -2,6 +2,7 @@ package openapi import ( "fmt" + "reflect" "log" "strings" @@ -47,20 +48,33 @@ type resourceInfo struct { path string host string httpSchemes []string - schemaDefinition spec.Schema + schemaDefinition *spec.Schema // createPathInfo contains info about /resource, including the POST operation createPathInfo spec.PathItem // pathInfo contains info about /resource/{id}, including GET, PUT and REMOVE operations if applicable pathInfo spec.PathItem + + // schemaDefinitions contains all the definitions which might be needed in case the resource schema contains properties + // of type object which in turn refer to other definitions + schemaDefinitions map[string]spec.Schema } func (r resourceInfo) createTerraformResourceSchema() (map[string]*schema.Schema, error) { + return r.terraformSchema(r.schemaDefinition, true) +} + +// terraformSchema returns the terraform schema for the given schema definition. if ignoreID is true then properties named +// id will be ignored, this is because terraform already has an ID field reserved that identifies uniquely the resource and +// root level schema can not contain a property named ID. For other levels, in case there are properties of type object +// id named properties is allowed as there won't be a conflict with terraform in that case. +func (r resourceInfo) terraformSchema(schemaDefinition *spec.Schema, ignoreID bool) (map[string]*schema.Schema, error) { s := map[string]*schema.Schema{} - for propertyName, property := range r.schemaDefinition.Properties { - if r.isIDProperty(propertyName) { + for propertyName, property := range schemaDefinition.Properties { + // ID should only be ignored when looping through the root level properties of the schema definition + if r.isIDProperty(propertyName) && ignoreID { continue } - tfSchema, err := r.createTerraformPropertySchema(propertyName, property) + tfSchema, err := r.createTerraformPropertyBasicSchema(propertyName, property, schemaDefinition.Required) if err != nil { return nil, err } @@ -76,18 +90,6 @@ func (r resourceInfo) convertToTerraformCompliantFieldName(propertyName string, return terraformutils.ConvertToTerraformCompliantName(propertyName) } -func (r resourceInfo) createTerraformPropertySchema(propertyName string, property spec.Schema) (*schema.Schema, error) { - propertySchema, err := r.createTerraformPropertyBasicSchema(propertyName, property) - if err != nil { - return nil, err - } - // ValidateFunc is not yet supported on lists or sets - if !r.isArrayProperty(property) { - propertySchema.ValidateFunc = r.validateFunc(propertyName, property) - } - return propertySchema, nil -} - func (r resourceInfo) validateFunc(propertyName string, property spec.Schema) schema.SchemaValidateFunc { return func(v interface{}, k string) (ws []string, errors []error) { if property.Default != nil { @@ -114,10 +116,23 @@ func (r resourceInfo) isRequired(propertyName string, requiredProps []string) bo return required } -func (r resourceInfo) createTerraformPropertyBasicSchema(propertyName string, property spec.Schema) (*schema.Schema, error) { +func (r resourceInfo) createTerraformPropertyBasicSchema(propertyName string, property spec.Schema, requiredProperties []string) (*schema.Schema, error) { var propertySchema *schema.Schema - // Arrays only support 'string' items at the moment - if r.isArrayProperty(property) { + if isObject, schemaDefinition, err := r.isObjectProperty(property); isObject { + if err != nil { + return nil, err + } + s, err := r.terraformSchema(schemaDefinition, false) + if err != nil { + return nil, err + } + propertySchema = &schema.Schema{ + Type: schema.TypeMap, + Elem: &schema.Resource{ + Schema: s, + }, + } + } else if r.isArrayProperty(property) { // Arrays only support 'string' items at the moment propertySchema = &schema.Schema{ Type: schema.TypeList, Elem: &schema.Schema{Type: schema.TypeString}, @@ -141,7 +156,7 @@ func (r resourceInfo) createTerraformPropertyBasicSchema(propertyName string, pr } // Set the property as required or optional - required := r.isRequired(propertyName, r.schemaDefinition.Required) + required := r.isRequired(propertyName, requiredProperties) if required { propertySchema.Required = true } else { @@ -175,11 +190,41 @@ func (r resourceInfo) createTerraformPropertyBasicSchema(propertyName string, pr propertySchema.Default = property.Default } } + + // ValidateFunc is not yet supported on lists or sets + if !r.isArrayProperty(property) { + propertySchema.ValidateFunc = r.validateFunc(propertyName, property) + } + return propertySchema, nil } +func (r resourceInfo) isObjectProperty(property spec.Schema) (bool, *spec.Schema, error) { + + if r.isObjectTypeProperty(property) && len(property.Properties) != 0 { + return true, &property, nil + } + + if property.Ref.Ref.GetURL() != nil { + schema, err := openapiutils.GetSchemaDefinition(r.schemaDefinitions, property.Ref.String()) + if err != nil { + return true, nil, err + } + return true, schema, nil + } + return false, nil, nil +} + func (r resourceInfo) isArrayProperty(property spec.Schema) bool { - return property.Type.Contains("array") + return r.isOfType(property, "array") +} + +func (r resourceInfo) isObjectTypeProperty(property spec.Schema) bool { + return r.isOfType(property, "object") +} + +func (r resourceInfo) isOfType(property spec.Schema, propertyType string) bool { + return property.Type.Contains(propertyType) } func (r resourceInfo) getImmutableProperties() []string { @@ -256,17 +301,25 @@ func (r resourceInfo) getResourceIdentifier() (string, error) { return identifierProperty, nil } -// getStatusIdentifier returns the property name that is supposed to be used as the status field. The status field -// is selected as follows: +// getStatusIdentifier loops through the schema definition given and tries to find the status id. This method supports both simple structures +// where the status field is at the schema definition root level or complex structures where status field is meant to be +// a sub-property of an object type property // 1.If the given schema definition contains a property configured with metadata 'x-terraform-field-status' set to true, that property // will be used to check the different statues for the asynchronous pooling mechanism. // 2. If none of the properties of the given schema definition contain such metadata, it is expected that the payload // will have a property named 'status' -// 3. If none of the above requirements is met, an error will be returned -func (r resourceInfo) getStatusIdentifier() (string, error) { - statusProperty := "" - for propertyName, property := range r.schemaDefinition.Properties { - if r.isIDProperty(propertyName) { +// 3. If the status field is NOT an object, then the array returned will contain one element with the property name that identifies +// the status field. +// 3. If the schema definition contains a deemed status field (as described above) and the property is of object type, the same logic +// as above will be applied to identify the status field to be used within the object property. In this case the result will +// be an array containing the property hierarchy, starting from the root and ending with the actual status field. This is needed +// so the correct status field can be extracted from payloads. +// 4. If none of the above requirements is met, an error will be returned +func (r resourceInfo) getStatusIdentifier(schemaDefinition *spec.Schema, shouldIgnoreID, shouldEnforceReadOnly bool) ([]string, error) { + var statusProperty string + var statusHierarchy []string + for propertyName, property := range schemaDefinition.Properties { + if r.isIDProperty(propertyName) && shouldIgnoreID { continue } if r.isStatusProperty(propertyName) { @@ -283,12 +336,48 @@ func (r resourceInfo) getStatusIdentifier() (string, error) { // if the id field is missing and there isn't any properties set with extTfFieldStatus, there is not way for the resource // to be identified and therefore an error is returned if statusProperty == "" { - return "", fmt.Errorf("could not find any status property in the resource swagger definition. Please make sure the resource definition has either one property named 'status' or one property that contains %s metadata", extTfFieldStatus) + return nil, fmt.Errorf("could not find any status property in the resource swagger definition. Please make sure the resource definition has either one property named 'status' or one property that contains %s metadata", extTfFieldStatus) + } + if !schemaDefinition.Properties[statusProperty].ReadOnly && shouldEnforceReadOnly { + return nil, fmt.Errorf("schema definition status property '%s' must be readOnly", statusProperty) + } + + statusHierarchy = append(statusHierarchy, statusProperty) + if isObject, propertySchemaDefinition, err := r.isObjectProperty(schemaDefinition.Properties[statusProperty]); isObject { + if err != nil { + return nil, err + } + statusIdentifier, err := r.getStatusIdentifier(propertySchemaDefinition, false, false) + if err != nil { + return nil, err + } + statusHierarchy = append(statusHierarchy, statusIdentifier...) } - if !r.schemaDefinition.Properties[statusProperty].ReadOnly { - return "", fmt.Errorf("schema definition status property '%s' must be readOnly", statusProperty) + + return statusHierarchy, nil +} + +func (r resourceInfo) getStatusValueFromPayload(payload map[string]interface{}) (string, error) { + statuses, err := r.getStatusIdentifier(r.schemaDefinition, true, true) + if err != nil { + return "", err + } + var property = payload + for _, statusField := range statuses { + propertyValue, statusExistsInPayload := property[statusField] + if !statusExistsInPayload { + return "", fmt.Errorf("payload does not match resouce schema, could not find the status field: %s", statuses) + } + switch reflect.TypeOf(propertyValue).Kind() { + case reflect.Map: + property = propertyValue.(map[string]interface{}) + case reflect.String: + return propertyValue.(string), nil + default: + return "", fmt.Errorf("status property value '%s' does not have a supported type [string/map]", statuses) + } } - return statusProperty, nil + return "", fmt.Errorf("could not find status value [%s] in the payload provided", statuses) } // shouldIgnoreResource checks whether the POST operation for a given resource as the 'x-terraform-exclude-resource' extension diff --git a/openapi/resource_info_test.go b/openapi/resource_info_test.go index 3fec45a97..254216204 100644 --- a/openapi/resource_info_test.go +++ b/openapi/resource_info_test.go @@ -288,7 +288,7 @@ func TestGetImmutableProperties(t *testing.T) { extensions := spec.Extensions{} extensions.Add("x-terraform-immutable", true) r := resourceInfo{ - schemaDefinition: spec.Schema{ + schemaDefinition: &spec.Schema{ SchemaProps: spec.SchemaProps{ Properties: map[string]spec.Schema{ "id": { @@ -314,7 +314,7 @@ func TestGetImmutableProperties(t *testing.T) { Convey("Given resource info is configured with schemaDefinition that DOES NOT contain immutable properties", t, func() { r := resourceInfo{ - schemaDefinition: spec.Schema{ + schemaDefinition: &spec.Schema{ SchemaProps: spec.SchemaProps{ Properties: map[string]spec.Schema{ "id": { @@ -346,16 +346,17 @@ func TestCreateTerraformPropertyBasicSchema(t *testing.T) { }, } r := resourceInfo{ - schemaDefinition: spec.Schema{ + schemaDefinition: &spec.Schema{ SchemaProps: spec.SchemaProps{ Properties: map[string]spec.Schema{ "string_prop": propSchema, }, + Required: []string{}, }, }, } Convey("When createTerraformPropertyBasicSchema method is called", func() { - tfPropSchema, err := r.createTerraformPropertyBasicSchema("string_prop", propSchema) + tfPropSchema, err := r.createTerraformPropertyBasicSchema("string_prop", propSchema, r.schemaDefinition.Required) Convey("Then the resulted terraform property schema should be of type string too", func() { So(err, ShouldBeNil) So(tfPropSchema.Type, ShouldEqual, schema.TypeString) @@ -371,16 +372,17 @@ func TestCreateTerraformPropertyBasicSchema(t *testing.T) { }, } r := resourceInfo{ - schemaDefinition: spec.Schema{ + schemaDefinition: &spec.Schema{ SchemaProps: spec.SchemaProps{ Properties: map[string]spec.Schema{ "int_prop": propSchema, }, + Required: []string{}, }, }, } Convey("When createTerraformPropertyBasicSchema method is called", func() { - tfPropSchema, err := r.createTerraformPropertyBasicSchema("int_prop", propSchema) + tfPropSchema, err := r.createTerraformPropertyBasicSchema("int_prop", propSchema, r.schemaDefinition.Required) Convey("Then the resulted terraform property schema should be of type int too", func() { So(err, ShouldBeNil) So(tfPropSchema.Type, ShouldEqual, schema.TypeInt) @@ -396,16 +398,17 @@ func TestCreateTerraformPropertyBasicSchema(t *testing.T) { }, } r := resourceInfo{ - schemaDefinition: spec.Schema{ + schemaDefinition: &spec.Schema{ SchemaProps: spec.SchemaProps{ Properties: map[string]spec.Schema{ "number_prop": propSchema, }, + Required: []string{}, }, }, } Convey("When createTerraformPropertyBasicSchema method is called", func() { - tfPropSchema, err := r.createTerraformPropertyBasicSchema("number_prop", propSchema) + tfPropSchema, err := r.createTerraformPropertyBasicSchema("number_prop", propSchema, r.schemaDefinition.Required) Convey("Then the resulted terraform property schema should be of type float too", func() { So(err, ShouldBeNil) So(tfPropSchema.Type, ShouldEqual, schema.TypeFloat) @@ -415,7 +418,7 @@ func TestCreateTerraformPropertyBasicSchema(t *testing.T) { Convey("Given a swagger schema definition that has a property of type 'boolean'", t, func() { r := resourceInfo{ - schemaDefinition: spec.Schema{ + schemaDefinition: &spec.Schema{ SchemaProps: spec.SchemaProps{ Properties: map[string]spec.Schema{ "boolean_prop": { @@ -425,11 +428,12 @@ func TestCreateTerraformPropertyBasicSchema(t *testing.T) { }, }, }, + Required: []string{}, }, }, } Convey("When createTerraformPropertyBasicSchema method is called", func() { - tfPropSchema, err := r.createTerraformPropertyBasicSchema("boolean_prop", r.schemaDefinition.Properties["boolean_prop"]) + tfPropSchema, err := r.createTerraformPropertyBasicSchema("boolean_prop", r.schemaDefinition.Properties["boolean_prop"], r.schemaDefinition.Required) Convey("Then the resulted terraform property schema should be of type int too", func() { So(err, ShouldBeNil) So(tfPropSchema.Type, ShouldEqual, schema.TypeBool) @@ -439,7 +443,7 @@ func TestCreateTerraformPropertyBasicSchema(t *testing.T) { Convey("Given a swagger schema definition that has a property of type 'array'", t, func() { r := resourceInfo{ - schemaDefinition: spec.Schema{ + schemaDefinition: &spec.Schema{ SchemaProps: spec.SchemaProps{ Properties: map[string]spec.Schema{ "array_prop": { @@ -449,11 +453,12 @@ func TestCreateTerraformPropertyBasicSchema(t *testing.T) { }, }, }, + Required: []string{}, }, }, } Convey("When createTerraformPropertyBasicSchema method is called", func() { - tfPropSchema, err := r.createTerraformPropertyBasicSchema("array_prop", r.schemaDefinition.Properties["array_prop"]) + tfPropSchema, err := r.createTerraformPropertyBasicSchema("array_prop", r.schemaDefinition.Properties["array_prop"], r.schemaDefinition.Required) Convey("Then the resulted terraform property schema should be of type array too", func() { So(err, ShouldBeNil) So(tfPropSchema.Type, ShouldEqual, schema.TypeList) @@ -465,9 +470,140 @@ func TestCreateTerraformPropertyBasicSchema(t *testing.T) { }) }) + Convey("Given a swagger schema definition that has a property of type object and a ref pointing to the schema", t, func() { + expectedRef := "#/definitions/ObjectProperty" + ref := spec.MustCreateRef(expectedRef) + r := resourceInfo{ + schemaDefinition: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Properties: map[string]spec.Schema{ + "object_prop": { + VendorExtensible: spec.VendorExtensible{}, + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Ref: ref, + }, + }, + }, + }, + }, + schemaDefinitions: map[string]spec.Schema{ + "ObjectProperty": { + SchemaProps: spec.SchemaProps{ + Properties: map[string]spec.Schema{ + "message": { + VendorExtensible: spec.VendorExtensible{}, + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + }, + }, + }, + }, + }, + }, + } + Convey("When createTerraformResourceSchema method is called", func() { + tfPropSchema, err := r.createTerraformPropertyBasicSchema("object_prop", r.schemaDefinition.Properties["object_prop"], r.schemaDefinition.Required) + Convey("Then the error returned should be nil", func() { + So(err, ShouldBeNil) + }) + Convey("And the tf resource schema returned should not be nil", func() { + So(tfPropSchema, ShouldNotBeNil) + }) + Convey("And the tf resource schema returned should contained the sub property - 'message'", func() { + So(tfPropSchema.Elem.(*schema.Resource).Schema, ShouldContainKey, "message") + }) + }) + }) + + Convey("Given a swagger schema definition that has a property of type object that has nested schema", t, func() { + r := resourceInfo{ + schemaDefinition: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Properties: map[string]spec.Schema{ + "object_prop": { + VendorExtensible: spec.VendorExtensible{}, + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "message": { + VendorExtensible: spec.VendorExtensible{}, + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + }, + }, + }, + }, + }, + }, + }, + }, + schemaDefinitions: map[string]spec.Schema{}, + } + Convey("When createTerraformResourceSchema method is called", func() { + tfPropSchema, err := r.createTerraformPropertyBasicSchema("object_prop", r.schemaDefinition.Properties["object_prop"], r.schemaDefinition.Required) + Convey("Then the error returned should be nil", func() { + So(err, ShouldBeNil) + }) + Convey("And the tf resource schema returned should not be nil", func() { + So(tfPropSchema, ShouldNotBeNil) + }) + Convey("And the tf resource schema returned should contained the sub property - 'message'", func() { + So(tfPropSchema.Elem.(*schema.Resource).Schema, ShouldContainKey, "message") + }) + }) + }) + + Convey("Given a swagger schema definition that has a property of type object that has nested schema and property named id", t, func() { + r := resourceInfo{ + schemaDefinition: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Properties: map[string]spec.Schema{ + "object_prop": { + VendorExtensible: spec.VendorExtensible{}, + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "id": { + VendorExtensible: spec.VendorExtensible{}, + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + }, + }, + "message": { + VendorExtensible: spec.VendorExtensible{}, + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + }, + }, + }, + }, + }, + }, + }, + }, + schemaDefinitions: map[string]spec.Schema{}, + } + Convey("When createTerraformResourceSchema method is called", func() { + tfPropSchema, err := r.createTerraformPropertyBasicSchema("object_prop", r.schemaDefinition.Properties["object_prop"], r.schemaDefinition.Required) + Convey("Then the error returned should be nil", func() { + So(err, ShouldBeNil) + }) + Convey("And the tf resource schema returned should not be nil", func() { + So(tfPropSchema, ShouldNotBeNil) + }) + Convey("And the tf resource schema returned should contain the sub property - 'message'", func() { + So(tfPropSchema.Elem.(*schema.Resource).Schema, ShouldContainKey, "message") + }) + Convey("And the tf resource schema returned should contain the sub property - 'id' and should not be ignored", func() { + So(tfPropSchema.Elem.(*schema.Resource).Schema, ShouldContainKey, "id") + }) + }) + }) + Convey("Given a swagger schema definition that has a property 'string_prop' which is required", t, func() { r := resourceInfo{ - schemaDefinition: spec.Schema{ + schemaDefinition: &spec.Schema{ SchemaProps: spec.SchemaProps{ Properties: map[string]spec.Schema{ "string_prop": { @@ -482,7 +618,7 @@ func TestCreateTerraformPropertyBasicSchema(t *testing.T) { }, } Convey("When createTerraformPropertyBasicSchema method is called", func() { - tfPropSchema, err := r.createTerraformPropertyBasicSchema("string_prop", r.schemaDefinition.Properties["string_prop"]) + tfPropSchema, err := r.createTerraformPropertyBasicSchema("string_prop", r.schemaDefinition.Properties["string_prop"], r.schemaDefinition.Required) Convey("Then the returned value should be true", func() { So(err, ShouldBeNil) So(tfPropSchema.Required, ShouldBeTrue) @@ -500,16 +636,17 @@ func TestCreateTerraformPropertyBasicSchema(t *testing.T) { }, } r := resourceInfo{ - schemaDefinition: spec.Schema{ + schemaDefinition: &spec.Schema{ SchemaProps: spec.SchemaProps{ Properties: map[string]spec.Schema{ "boolean_prop": propSchema, }, + Required: []string{}, }, }, } Convey("When createTerraformPropertyBasicSchema method is called", func() { - tfPropSchema, err := r.createTerraformPropertyBasicSchema("boolean_prop", propSchema) + tfPropSchema, err := r.createTerraformPropertyBasicSchema("boolean_prop", propSchema, r.schemaDefinition.Required) Convey("Then the resulted terraform property schema should be of type int too", func() { So(err, ShouldBeNil) So(tfPropSchema.ForceNew, ShouldBeTrue) @@ -526,16 +663,17 @@ func TestCreateTerraformPropertyBasicSchema(t *testing.T) { SwaggerSchemaProps: spec.SwaggerSchemaProps{ReadOnly: true}, } r := resourceInfo{ - schemaDefinition: spec.Schema{ + schemaDefinition: &spec.Schema{ SchemaProps: spec.SchemaProps{ Properties: map[string]spec.Schema{ "boolean_prop": propSchema, }, + Required: []string{}, }, }, } Convey("When createTerraformPropertyBasicSchema method is called", func() { - tfPropSchema, err := r.createTerraformPropertyBasicSchema("boolean_prop", propSchema) + tfPropSchema, err := r.createTerraformPropertyBasicSchema("boolean_prop", propSchema, r.schemaDefinition.Required) Convey("Then the resulted terraform property schema should be configured as computed", func() { So(err, ShouldBeNil) So(tfPropSchema.Computed, ShouldBeTrue) @@ -554,16 +692,17 @@ func TestCreateTerraformPropertyBasicSchema(t *testing.T) { }, } r := resourceInfo{ - schemaDefinition: spec.Schema{ + schemaDefinition: &spec.Schema{ SchemaProps: spec.SchemaProps{ Properties: map[string]spec.Schema{ "boolean_prop": propSchema, }, + Required: []string{}, }, }, } Convey("When createTerraformPropertyBasicSchema method is called", func() { - tfPropSchema, err := r.createTerraformPropertyBasicSchema("boolean_prop", propSchema) + tfPropSchema, err := r.createTerraformPropertyBasicSchema("boolean_prop", propSchema, r.schemaDefinition.Required) Convey("Then the resulted terraform property schema should be configured as forceNew and sensitive", func() { So(err, ShouldBeNil) So(tfPropSchema.ForceNew, ShouldBeTrue) @@ -582,22 +721,93 @@ func TestCreateTerraformPropertyBasicSchema(t *testing.T) { }, } r := resourceInfo{ - schemaDefinition: spec.Schema{ + schemaDefinition: &spec.Schema{ SchemaProps: spec.SchemaProps{ Properties: map[string]spec.Schema{ "boolean_prop": propSchema, }, + Required: []string{}, }, }, } Convey("When createTerraformPropertyBasicSchema method is called", func() { - tfPropSchema, err := r.createTerraformPropertyBasicSchema("boolean_prop", propSchema) + tfPropSchema, err := r.createTerraformPropertyBasicSchema("boolean_prop", propSchema, r.schemaDefinition.Required) Convey("Then the resulted terraform property schema should be configured with the according default value, ", func() { So(err, ShouldBeNil) So(tfPropSchema.Default, ShouldEqual, expectedDefaultValue) }) }) }) + + Convey("Given a swagger schema definition that has a property 'string_prop' of type string, required, sensitive and has a default value 'defaultValue'", t, func() { + expectedDefaultValue := "defaultValue" + extensions := spec.Extensions{} + extensions.Add("x-terraform-sensitive", true) + r := resourceInfo{ + schemaDefinition: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Properties: map[string]spec.Schema{ + "string_prop": { + VendorExtensible: spec.VendorExtensible{Extensions: extensions}, + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Default: expectedDefaultValue, + }, + }, + }, + Required: []string{"string_prop"}, // This array contains the list of properties that are required + }, + }, + } + Convey("When createTerraformPropertyBasicSchema method is called", func() { + tfPropSchema, err := r.createTerraformPropertyBasicSchema("string_prop", r.schemaDefinition.Properties["string_prop"], r.schemaDefinition.Required) + Convey("Then the returned tf tfPropSchema should be of type string", func() { + So(err, ShouldBeNil) + So(tfPropSchema.Type, ShouldEqual, schema.TypeString) + }) + Convey("And a validateFunc should be configured", func() { + So(tfPropSchema.ValidateFunc, ShouldNotBeNil) + }) + Convey("And be configured as required", func() { + So(tfPropSchema.Required, ShouldBeTrue) + }) + Convey("And be configured as sensitive", func() { + So(tfPropSchema.Sensitive, ShouldBeTrue) + }) + Convey("And the default value should match 'defaultValue'", func() { + So(tfPropSchema.Default, ShouldEqual, expectedDefaultValue) + }) + }) + }) + + Convey("Given a swagger schema definition that has a property 'array_prop' of type array", t, func() { + r := resourceInfo{ + schemaDefinition: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Properties: map[string]spec.Schema{ + "array_prop": { + VendorExtensible: spec.VendorExtensible{}, + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + }, + }, + }, + Required: []string{"array_prop"}, // This array contains the list of properties that are required + }, + }, + } + Convey("When createTerraformPropertyBasicSchema method is called", func() { + tfPropSchema, err := r.createTerraformPropertyBasicSchema("array_prop", r.schemaDefinition.Properties["array_prop"], r.schemaDefinition.Required) + Convey("Then the returned tf tfPropSchema should be of type array", func() { + So(err, ShouldBeNil) + So(tfPropSchema.Type, ShouldEqual, schema.TypeList) + }) + Convey("And there should not be any validation function attached to it", func() { + So(tfPropSchema.ValidateFunc, ShouldBeNil) + }) + }) + }) + } func TestIsArrayProperty(t *testing.T) { @@ -609,7 +819,7 @@ func TestIsArrayProperty(t *testing.T) { }, } r := resourceInfo{ - schemaDefinition: spec.Schema{ + schemaDefinition: &spec.Schema{ SchemaProps: spec.SchemaProps{ Properties: map[string]spec.Schema{ "array_prop": propSchema, @@ -633,7 +843,7 @@ func TestIsArrayProperty(t *testing.T) { }, } r := resourceInfo{ - schemaDefinition: spec.Schema{ + schemaDefinition: &spec.Schema{ SchemaProps: spec.SchemaProps{ Properties: map[string]spec.Schema{ "string_prop": propSchema, @@ -659,7 +869,7 @@ func TestIsRequired(t *testing.T) { }, } r := resourceInfo{ - schemaDefinition: spec.Schema{ + schemaDefinition: &spec.Schema{ SchemaProps: spec.SchemaProps{ Properties: map[string]spec.Schema{ "string_prop": propSchema, @@ -684,7 +894,7 @@ func TestIsRequired(t *testing.T) { }, } r := resourceInfo{ - schemaDefinition: spec.Schema{ + schemaDefinition: &spec.Schema{ SchemaProps: spec.SchemaProps{ Properties: map[string]spec.Schema{ "string_prop": propSchema, @@ -701,81 +911,10 @@ func TestIsRequired(t *testing.T) { }) } -func TestCreateTerraformPropertySchema(t *testing.T) { - Convey("Given a swagger schema definition that has a property 'string_prop' of type string, required, sensitive and has a default value 'defaultValue'", t, func() { - expectedDefaultValue := "defaultValue" - extensions := spec.Extensions{} - extensions.Add("x-terraform-sensitive", true) - r := resourceInfo{ - schemaDefinition: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Properties: map[string]spec.Schema{ - "string_prop": { - VendorExtensible: spec.VendorExtensible{Extensions: extensions}, - SchemaProps: spec.SchemaProps{ - Type: []string{"string"}, - Default: expectedDefaultValue, - }, - }, - }, - Required: []string{"string_prop"}, // This array contains the list of properties that are required - }, - }, - } - Convey("When createTerraformPropertyBasicSchema method is called", func() { - tfPropSchema, err := r.createTerraformPropertySchema("string_prop", r.schemaDefinition.Properties["string_prop"]) - Convey("Then the returned tf tfPropSchema should be of type string", func() { - So(err, ShouldBeNil) - So(tfPropSchema.Type, ShouldEqual, schema.TypeString) - }) - Convey("And a validateFunc should be configured", func() { - So(tfPropSchema.ValidateFunc, ShouldNotBeNil) - }) - Convey("And be configured as required, sensitive and the default value should match 'defaultValue'", func() { - So(tfPropSchema.Required, ShouldBeTrue) - }) - Convey("And be configured as sensitive", func() { - So(tfPropSchema.Sensitive, ShouldBeTrue) - }) - Convey("And the default value should match 'defaultValue'", func() { - So(tfPropSchema.Default, ShouldEqual, expectedDefaultValue) - }) - }) - }) - - Convey("Given a swagger schema definition that has a property 'array_prop' of type array", t, func() { - r := resourceInfo{ - schemaDefinition: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Properties: map[string]spec.Schema{ - "array_prop": { - VendorExtensible: spec.VendorExtensible{}, - SchemaProps: spec.SchemaProps{ - Type: []string{"array"}, - }, - }, - }, - Required: []string{"array_prop"}, // This array contains the list of properties that are required - }, - }, - } - Convey("When createTerraformPropertyBasicSchema method is called", func() { - tfPropSchema, err := r.createTerraformPropertySchema("array_prop", r.schemaDefinition.Properties["array_prop"]) - Convey("Then the returned tf tfPropSchema should be of type array", func() { - So(err, ShouldBeNil) - So(tfPropSchema.Type, ShouldEqual, schema.TypeList) - }) - Convey("And there should not be any validation function attached to it", func() { - So(tfPropSchema.ValidateFunc, ShouldBeNil) - }) - }) - }) -} - func TestValidateFunc(t *testing.T) { Convey("Given a swagger schema definition that has one property", t, func() { r := resourceInfo{ - schemaDefinition: spec.Schema{ + schemaDefinition: &spec.Schema{ SchemaProps: spec.SchemaProps{ Properties: map[string]spec.Schema{ "array_prop": { @@ -798,7 +937,7 @@ func TestValidateFunc(t *testing.T) { Convey("Given a swagger schema definition that has a property which is supposed to be computed but has a default value set", t, func() { r := resourceInfo{ - schemaDefinition: spec.Schema{ + schemaDefinition: &spec.Schema{ SchemaProps: spec.SchemaProps{ Properties: map[string]spec.Schema{ "array_prop": { @@ -829,7 +968,7 @@ func TestValidateFunc(t *testing.T) { func TestCreateTerraformResourceSchema(t *testing.T) { Convey("Given a swagger schema definition that has multiple properties - 'string_prop', 'int_prop', 'number_prop', 'bool_prop' and 'array_prop'", t, func() { r := resourceInfo{ - schemaDefinition: spec.Schema{ + schemaDefinition: &spec.Schema{ SchemaProps: spec.SchemaProps{ Properties: map[string]spec.Schema{ "string_prop": { @@ -885,13 +1024,79 @@ func TestCreateTerraformResourceSchema(t *testing.T) { }) }) }) + + Convey("Given a swagger schema definition that has object properties and a list of schema definitions containing the definition the object refs to", t, func() { + expectedRef := "#/definitions/ObjectProperty" + ref := spec.MustCreateRef(expectedRef) + r := resourceInfo{ + schemaDefinition: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Properties: map[string]spec.Schema{ + "string_prop": { + VendorExtensible: spec.VendorExtensible{}, + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + }, + }, + "object_prop": { // This prop does not have a terraform compliant name; however an automatic translation is performed behind the scenes to make it compliant + VendorExtensible: spec.VendorExtensible{}, + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Ref: ref, + }, + }, + }, + Required: []string{"string_prop", "object_prop"}, + }, + }, + schemaDefinitions: map[string]spec.Schema{ + "ObjectProperty": { + SchemaProps: spec.SchemaProps{ + Properties: map[string]spec.Schema{ + "message": { + VendorExtensible: spec.VendorExtensible{}, + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + }, + }, + }, + Required: []string{"message"}, + }, + }, + }, + } + Convey("When createTerraformResourceSchema method is called", func() { + resourceSchema, err := r.createTerraformResourceSchema() + Convey("Then the error returned should be nil", func() { + So(err, ShouldBeNil) + }) + Convey("And the tf resource schema returned should not be nil", func() { + So(resourceSchema, ShouldNotBeNil) + }) + Convey("And the tf resource schema returned should match the swagger props - 'string_prop' and 'object_prop'", func() { + So(resourceSchema, ShouldNotBeNil) + So(resourceSchema, ShouldContainKey, "string_prop") + So(resourceSchema, ShouldContainKey, "object_prop") + }) + Convey("And the properties 'string_prop' and 'object_prop' should be required", func() { + So(resourceSchema["string_prop"].Required, ShouldBeTrue) + So(resourceSchema["object_prop"].Required, ShouldBeTrue) + }) + Convey("And the tf resource schema ", func() { + So(resourceSchema["object_prop"].Elem.(*schema.Resource).Schema, ShouldContainKey, "message") + }) + Convey("And the properties 'message' should be required", func() { + So(resourceSchema["object_prop"].Elem.(*schema.Resource).Schema["message"].Required, ShouldBeTrue) + }) + }) + }) } func TestConvertToTerraformCompliantFieldName(t *testing.T) { Convey("Given a property with a name that is terraform field name compliant", t, func() { propertyName := "some_prop_name_that_is_terraform_field_name_compliant" r := resourceInfo{ - schemaDefinition: spec.Schema{ + schemaDefinition: &spec.Schema{ SchemaProps: spec.SchemaProps{ Properties: map[string]spec.Schema{ propertyName: { @@ -915,7 +1120,7 @@ func TestConvertToTerraformCompliantFieldName(t *testing.T) { Convey("Given a property with a name that is NOT terraform field name compliant", t, func() { propertyName := "thisPropIsNotTerraformField_Compliant" r := resourceInfo{ - schemaDefinition: spec.Schema{ + schemaDefinition: &spec.Schema{ SchemaProps: spec.SchemaProps{ Properties: map[string]spec.Schema{ propertyName: { @@ -940,7 +1145,7 @@ func TestConvertToTerraformCompliantFieldName(t *testing.T) { propertyName := "thisPropIsNotTerraformField_Compliant" expectedPropertyName := "this_property_is_now_terraform_field_compliant" r := resourceInfo{ - schemaDefinition: spec.Schema{ + schemaDefinition: &spec.Schema{ SchemaProps: spec.SchemaProps{ Properties: map[string]spec.Schema{ propertyName: { @@ -968,7 +1173,7 @@ func TestConvertToTerraformCompliantFieldName(t *testing.T) { Convey("Given a property with a name that is NOT terraform field name compliant but has an extension that overrides it which in turn is also not terraform name compliant", t, func() { propertyName := "thisPropIsNotTerraformField_Compliant" r := resourceInfo{ - schemaDefinition: spec.Schema{ + schemaDefinition: &spec.Schema{ SchemaProps: spec.SchemaProps{ Properties: map[string]spec.Schema{ propertyName: { @@ -997,7 +1202,7 @@ func TestConvertToTerraformCompliantFieldName(t *testing.T) { func TestGetResourceIdentifier(t *testing.T) { Convey("Given a swagger schema definition that has an id property", t, func() { r := resourceInfo{ - schemaDefinition: spec.Schema{ + schemaDefinition: &spec.Schema{ SchemaProps: spec.SchemaProps{ Properties: map[string]spec.Schema{ idDefaultPropertyName: { @@ -1025,7 +1230,7 @@ func TestGetResourceIdentifier(t *testing.T) { extensions := spec.Extensions{} extensions.Add(extTfID, true) r := resourceInfo{ - schemaDefinition: spec.Schema{ + schemaDefinition: &spec.Schema{ SchemaProps: spec.SchemaProps{ Properties: map[string]spec.Schema{ "some-other-id": { @@ -1053,7 +1258,7 @@ func TestGetResourceIdentifier(t *testing.T) { extensions := spec.Extensions{} extensions.Add(extTfID, true) r := resourceInfo{ - schemaDefinition: spec.Schema{ + schemaDefinition: &spec.Schema{ SchemaProps: spec.SchemaProps{ Properties: map[string]spec.Schema{ "id": { @@ -1087,7 +1292,7 @@ func TestGetResourceIdentifier(t *testing.T) { extensions := spec.Extensions{} extensions.Add(extTfID, false) r := resourceInfo{ - schemaDefinition: spec.Schema{ + schemaDefinition: &spec.Schema{ SchemaProps: spec.SchemaProps{ Properties: map[string]spec.Schema{ "some-other-id": { @@ -1110,7 +1315,7 @@ func TestGetResourceIdentifier(t *testing.T) { Convey("Given a swagger schema definition that NEITHER HAS an 'id' property NOR a property configured with x-terraform-id set to true", t, func() { r := resourceInfo{ - schemaDefinition: spec.Schema{ + schemaDefinition: &spec.Schema{ SchemaProps: spec.SchemaProps{ Properties: map[string]spec.Schema{ "prop-that-is-not-id": { @@ -1138,10 +1343,10 @@ func TestGetResourceIdentifier(t *testing.T) { }) } -func TestGetStatusIdentifier(t *testing.T) { - Convey("Given a swagger schema definition that has an status property", t, func() { +func TestGetStatusId(t *testing.T) { + Convey("Given a swagger schema definition that has an status property that is not an object", t, func() { r := resourceInfo{ - schemaDefinition: spec.Schema{ + schemaDefinition: &spec.Schema{ SchemaProps: spec.SchemaProps{ Properties: map[string]spec.Schema{ statusDefaultPropertyName: { @@ -1158,12 +1363,15 @@ func TestGetStatusIdentifier(t *testing.T) { }, } Convey("When getStatusIdentifier method is called", func() { - status, err := r.getStatusIdentifier() + status, err := r.getStatusIdentifier(r.schemaDefinition, true, true) Convey("Then the error returned should be nil", func() { So(err, ShouldBeNil) }) - Convey("Then the value returned should be 'status'", func() { - So(status, ShouldEqual, statusDefaultPropertyName) + Convey("And the status returned should not be empty'", func() { + So(status, ShouldNotBeEmpty) + }) + Convey("Then the value returned should contain the name of the property 'status'", func() { + So(status[0], ShouldEqual, statusDefaultPropertyName) }) }) }) @@ -1173,7 +1381,7 @@ func TestGetStatusIdentifier(t *testing.T) { extensions.Add(extTfFieldStatus, true) expectedStatusProperty := "some-other-property-holding-status" r := resourceInfo{ - schemaDefinition: spec.Schema{ + schemaDefinition: &spec.Schema{ SchemaProps: spec.SchemaProps{ Properties: map[string]spec.Schema{ expectedStatusProperty: { @@ -1190,12 +1398,15 @@ func TestGetStatusIdentifier(t *testing.T) { }, } Convey("When getStatusIdentifier method is called", func() { - id, err := r.getStatusIdentifier() + status, err := r.getStatusIdentifier(r.schemaDefinition, true, true) Convey("Then the error returned should be nil", func() { So(err, ShouldBeNil) }) - Convey("Then the value returned should be 'some-other-property-holding-status'", func() { - So(id, ShouldEqual, expectedStatusProperty) + Convey("And the status returned should not be empty'", func() { + So(status, ShouldNotBeEmpty) + }) + Convey("Then the value returned should contain the name of the property 'some-other-property-holding-status'", func() { + So(status[0], ShouldEqual, expectedStatusProperty) }) }) }) @@ -1205,7 +1416,7 @@ func TestGetStatusIdentifier(t *testing.T) { extensions.Add(extTfFieldStatus, true) expectedStatusProperty := "some-other-property-holding-status" r := resourceInfo{ - schemaDefinition: spec.Schema{ + schemaDefinition: &spec.Schema{ SchemaProps: spec.SchemaProps{ Properties: map[string]spec.Schema{ "status": { @@ -1213,6 +1424,9 @@ func TestGetStatusIdentifier(t *testing.T) { SchemaProps: spec.SchemaProps{ Type: []string{"string"}, }, + SwaggerSchemaProps: spec.SwaggerSchemaProps{ + ReadOnly: true, + }, }, expectedStatusProperty: { VendorExtensible: spec.VendorExtensible{Extensions: extensions}, @@ -1228,12 +1442,72 @@ func TestGetStatusIdentifier(t *testing.T) { }, } Convey("When getStatusIdentifier method is called", func() { - id, err := r.getStatusIdentifier() + status, err := r.getStatusIdentifier(r.schemaDefinition, true, true) Convey("Then the error returned should be nil", func() { So(err, ShouldBeNil) }) + Convey("And the status returned should not be empty'", func() { + So(status, ShouldNotBeEmpty) + }) Convey("Then the value returned should be 'some-other-property-holding-status' as it takes preference over the default 'status' property", func() { - So(id, ShouldEqual, expectedStatusProperty) + So(status[0], ShouldEqual, expectedStatusProperty) + }) + }) + }) + + Convey("Given a swagger schema definition that HAS an status field which is an object containing a status field", t, func() { + extensions := spec.Extensions{} + extensions.Add(extTfFieldStatus, true) + expectedStatusProperty := "actualStatus" + r := resourceInfo{ + schemaDefinition: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "id": { + VendorExtensible: spec.VendorExtensible{}, + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + }, + SwaggerSchemaProps: spec.SwaggerSchemaProps{ + ReadOnly: true, + }, + }, + "status": { + VendorExtensible: spec.VendorExtensible{}, + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + expectedStatusProperty: { + VendorExtensible: spec.VendorExtensible{Extensions: extensions}, + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + }, + SwaggerSchemaProps: spec.SwaggerSchemaProps{ + ReadOnly: true, + }, + }, + }, + }, + SwaggerSchemaProps: spec.SwaggerSchemaProps{ + ReadOnly: true, + }, + }, + }, + }, + }, + } + Convey("When getStatusIdentifier method is called", func() { + status, err := r.getStatusIdentifier(r.schemaDefinition, true, true) + Convey("Then the error returned should be nil", func() { + So(err, ShouldBeNil) + }) + Convey("And the status returned should not be empty'", func() { + So(status, ShouldNotBeEmpty) + }) + Convey("Then the status array returned should contain the different the trace of property names (hierarchy) until the last one which is the one used as status, in this case 'actualStatus' on the last index", func() { + So(status[0], ShouldEqual, "status") + So(status[1], ShouldEqual, expectedStatusProperty) }) }) }) @@ -1243,7 +1517,7 @@ func TestGetStatusIdentifier(t *testing.T) { extensions.Add(extTfFieldStatus, false) expectedStatusProperty := "some-other-property-holding-status" r := resourceInfo{ - schemaDefinition: spec.Schema{ + schemaDefinition: &spec.Schema{ SchemaProps: spec.SchemaProps{ Properties: map[string]spec.Schema{ expectedStatusProperty: { @@ -1260,7 +1534,7 @@ func TestGetStatusIdentifier(t *testing.T) { }, } Convey("When getStatusIdentifier method is called", func() { - _, err := r.getStatusIdentifier() + _, err := r.getStatusIdentifier(r.schemaDefinition, true, true) Convey("Then the error returned should not be nil", func() { So(err, ShouldNotBeNil) }) @@ -1269,7 +1543,7 @@ func TestGetStatusIdentifier(t *testing.T) { Convey("Given a swagger schema definition that NEITHER HAS an 'status' property NOR a property configured with 'x-terraform-field-status' set to true", t, func() { r := resourceInfo{ - schemaDefinition: spec.Schema{ + schemaDefinition: &spec.Schema{ SchemaProps: spec.SchemaProps{ Properties: map[string]spec.Schema{ "prop-that-is-not-status": { @@ -1292,19 +1566,19 @@ func TestGetStatusIdentifier(t *testing.T) { }, } Convey("When getStatusIdentifier method is called", func() { - _, err := r.getStatusIdentifier() + _, err := r.getStatusIdentifier(r.schemaDefinition, true, true) Convey("Then the error returned should NOT be nil", func() { So(err, ShouldNotBeNil) }) }) }) - Convey("Given a swagger schema definition with a property configured with 'x-terraform-field-status' set to true but is not readonly", t, func() { + Convey("Given a swagger schema definition with a status property that is not readonly", t, func() { r := resourceInfo{ - schemaDefinition: spec.Schema{ + schemaDefinition: &spec.Schema{ SchemaProps: spec.SchemaProps{ Properties: map[string]spec.Schema{ - "prop-that-is-not-status": { + "status": { VendorExtensible: spec.VendorExtensible{}, SchemaProps: spec.SchemaProps{ Type: []string{"string"}, @@ -1313,7 +1587,24 @@ func TestGetStatusIdentifier(t *testing.T) { ReadOnly: false, }, }, - "prop-that-is-not-status-and-does-not-have-status-metadata-either": { + }, + }, + }, + } + Convey("When getStatusIdentifier method is called", func() { + _, err := r.getStatusIdentifier(r.schemaDefinition, true, true) + Convey("Then the error returned should NOT be nil", func() { + So(err, ShouldNotBeNil) + }) + }) + }) + + Convey("Given a swagger schema definition with a property configured with 'x-terraform-field-status' set to true and it is not readonly", t, func() { + r := resourceInfo{ + schemaDefinition: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Properties: map[string]spec.Schema{ + "status": { VendorExtensible: spec.VendorExtensible{}, SchemaProps: spec.SchemaProps{ Type: []string{"string"}, @@ -1323,13 +1614,137 @@ func TestGetStatusIdentifier(t *testing.T) { }, }, } - Convey("When getStatusIdentifier method is called", func() { - _, err := r.getStatusIdentifier() + Convey("When getStatusIdentifier method is called with a schema definition and forceReadOnly check is disabled (this happens when the method is called recursively)", func() { + status, err := r.getStatusIdentifier(r.schemaDefinition, false, false) + Convey("Then the error returned should be nil", func() { + So(err, ShouldBeNil) + }) + Convey("Then the status array returned should contain the status property even though it's not read only...readonly checks are only perform on root level properties", func() { + So(status[0], ShouldEqual, "status") + }) + }) + }) +} + +func TestGetStatusValueFromPayload(t *testing.T) { + Convey("Given a swagger schema definition that has an status property that is not an object", t, func() { + r := resourceInfo{ + schemaDefinition: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Properties: map[string]spec.Schema{ + statusDefaultPropertyName: { + VendorExtensible: spec.VendorExtensible{}, + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + }, + SwaggerSchemaProps: spec.SwaggerSchemaProps{ + ReadOnly: true, + }, + }, + }, + }, + }, + } + Convey("When getStatusValueFromPayload method is called with a payload that also has a 'status' field in the root level", func() { + expectedStatusValue := "someValue" + payload := map[string]interface{}{ + statusDefaultPropertyName: expectedStatusValue, + } + statusField, err := r.getStatusValueFromPayload(payload) + Convey("Then the error returned should be nil", func() { + So(err, ShouldBeNil) + }) + Convey("Then the value returned should contain the name of the property 'status'", func() { + So(statusField, ShouldEqual, expectedStatusValue) + }) + }) + + Convey("When getStatusValueFromPayload method is called with a payload that does not have status field", func() { + payload := map[string]interface{}{ + "someOtherPropertyName": "arggg", + } + _, err := r.getStatusValueFromPayload(payload) + Convey("Then the error returned should NOT be nil", func() { + So(err, ShouldNotBeNil) + }) + Convey("Then the error message should be", func() { + So(err.Error(), ShouldEqual, "payload does not match resouce schema, could not find the status field: [status]") + }) + }) + + Convey("When getStatusValueFromPayload method is called with a payload that has a status field but the value is not supported", func() { + payload := map[string]interface{}{ + statusDefaultPropertyName: 12, // this value is not supported, only strings and maps (for nested properties within an object) are supported + } + _, err := r.getStatusValueFromPayload(payload) Convey("Then the error returned should NOT be nil", func() { So(err, ShouldNotBeNil) }) + Convey("Then the error message should be", func() { + So(err.Error(), ShouldEqual, "status property value '[status]' does not have a supported type [string/map]") + }) }) }) + + Convey("Given a swagger schema definition that has an status property that IS an object", t, func() { + expectedStatusProperty := "some-other-property-holding-status" + extensions := spec.Extensions{} + extensions.Add(extTfFieldStatus, true) + r := resourceInfo{ + schemaDefinition: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "id": { + VendorExtensible: spec.VendorExtensible{}, + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + }, + SwaggerSchemaProps: spec.SwaggerSchemaProps{ + ReadOnly: true, + }, + }, + statusDefaultPropertyName: { + VendorExtensible: spec.VendorExtensible{}, + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + expectedStatusProperty: { + VendorExtensible: spec.VendorExtensible{Extensions: extensions}, + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + }, + SwaggerSchemaProps: spec.SwaggerSchemaProps{ + ReadOnly: true, + }, + }, + }, + }, + SwaggerSchemaProps: spec.SwaggerSchemaProps{ + ReadOnly: true, + }, + }, + }, + }, + }, + } + Convey("When getStatusValueFromPayload method is called with a payload that has an status object property inside which there's an status property", func() { + expectedStatusValue := "someStatusValue" + payload := map[string]interface{}{ + statusDefaultPropertyName: map[string]interface{}{ + expectedStatusProperty: expectedStatusValue, + }, + } + statusField, err := r.getStatusValueFromPayload(payload) + Convey("Then the error returned should be nil", func() { + So(err, ShouldBeNil) + }) + Convey("Then the value returned should contain the name of the property 'status'", func() { + So(statusField, ShouldEqual, expectedStatusValue) + }) + }) + }) + } func TestIsIDProperty(t *testing.T) { From 101be69078bf93701d1312d78289f1d6f60579f2 Mon Sep 17 00:00:00 2001 From: dikhan Date: Tue, 25 Sep 2018 15:58:34 -0700 Subject: [PATCH 7/7] add documentation --- docs/how_to.md | 73 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/docs/how_to.md b/docs/how_to.md index feec45141..10ef5f1ef 100644 --- a/docs/how_to.md +++ b/docs/how_to.md @@ -430,6 +430,34 @@ definitions: - deleted ```` +Alternatively, the status field can also be of 'object' type in which case the nested properties can be defined in place or +the $ref attribute can be used to link to the corresponding status schema definition. The nested properties are considered +computed automatically even if they are not marked as readOnly. + +```` +definitions: + LBV1: + type: "object" + ... + properties: + newStatus: + $ref: "#/definitions/Status" + x-terraform-field-status: true # identifies the field that should be used as status for async operations. This is handy when the field name is not status but some other name the service provider might have chosen and enables the provider to identify the field as the status field that will be used to track progress for the async operations + readOnly: true + timeToProcess: # time that the resource will take to be processed in seconds + type: integer + default: 60 # it will take two minute to process the resource operation (POST/PUT/READ/DELETE) + simulate_failure: # allows user to set it to true and force an error on the API when the given operation (POST/PUT/READ/DELETE) is being performed + type: boolean + Status: + type: object + properties: + message: + type: string + status: + type: string +```` + *Note: This extension is only supported at the operation's response level.* @@ -590,6 +618,51 @@ string | Type: schema.TypeString | string value integer | schema.TypeInt | int value number | schema.TypeFloat | float value boolean | schema.TypeBool | boolean value +object | schema.TypeMap | map value + +Object types can be defined in two fashions: + +###### Nested properties + +Properties can have their schema definition in place or nested; and they must be of type 'object'. + +```` +definitions: + ContentDeliveryNetworkV1: + type: "object" + ... + properties: + ... + object_nested_scheme_property: + type: object # nested properties required type equal object to be considered as object + properties: + name: + type: string +```` + +###### Ref schema definition + +A property that has a $ref attribute is considered automatically and object so defining the type is optional (although +it's recommended). + +```` +definitions: + ContentDeliveryNetworkV1: + type: "object" + ... + properties: + ... + object_property: + #type: object - type is optional for properties of object type that use $ref + $ref: "#/definitions/ObjectProperty" + ObjectProperty: + type: object + required: + - message + properties: + message: + type: string +```` Additionally, properties can be flagged as required as follows: