diff --git a/Makefile b/Makefile index 4b2ac19f8..7be6167d2 100644 --- a/Makefile +++ b/Makefile @@ -75,6 +75,7 @@ show-terraform-version: # dockerhub-login logs into Docker if the environment variable PERFORM_DOCKER_LOGIN is set. This is used by Travis CI # to avoid Docker toomanyrequests: You have reached your pull rate limit. dockerhub-login: + @echo "[INFO] Logging into Docker Hub Enabled=$(PERFORM_DOCKER_LOGIN)" ifdef PERFORM_DOCKER_LOGIN echo $(DOCKER_PASSWORD) | docker login -u $(DOCKER_USERNAME) --password-stdin endif @@ -148,6 +149,18 @@ else @echo "Cancelling release due to new version $(RELEASE_TAG) <= latest release version $(CURRENT_RELEASE_TAG)" endif +# RELEASE_ALPHA_VERSION=2.1.0 make release-alpha +release-alpha: + @$(eval ALPHA_VERSION := v$(RELEASE_ALPHA_VERSION)-alpha.1) + git tag $(ALPHA_VERSION) + git push origin $(ALPHA_VERSION) + +# RELEASE_ALPHA_VERSION=2.1.0 make delete-release-alpha +delete-release-alpha: + @$(eval ALPHA_VERSION := v$(RELEASE_ALPHA_VERSION)-alpha.1) + git tag -d $(ALPHA_VERSION) + git push --delete origin $(ALPHA_VERSION) + define install_plugin @$(eval PROVIDER_NAME := $(1)) @./scripts/install.sh --provider-name $(PROVIDER_NAME) --provider-source-address "terraform.example.com/examplecorp" --compiled-plugin-path $(TF_OPENAPI_PROVIDER_PLUGIN_NAME) --debug diff --git a/docs/how_to.md b/docs/how_to.md index 105b63ae7..89a168904 100644 --- a/docs/how_to.md +++ b/docs/how_to.md @@ -208,31 +208,23 @@ paths: If a given resource is missing any of the aforementioned required operations, the resource will not be available as a terraform resource. -- Paths should be versioned as described in the [versioning](#versioning) document following ‘/v{number}/resource’ pattern -(e,g: ‘/v1/resource’). A version upgrade (e,g: v1 -> v2) will be needed when the interface of the resource changes, hence -the new version is non backwards compatible. See that only the 'Major' version is considered in the path, this is recommended -as the service provider will have less paths to maintain overall. if there are minor/patches applied to the backend that -should not affect the way consumer interacts with the APIs whatsoever and the namespace should remain as is. - -- POST operation may have a body payload referencing a schema object (see example below) defined at the root level -[definitions](#swaggerDefinitions) section. The $ref can be a link to a local model definition or a definition hosted -externally. Payload schema should not be defined inside the path’s configuration; however, if it is defined the schema -must be the same as the GET and PUT operations, including the expected input properties as well as the computed ones. -The reason for this is to make sure the model for the the resource state is shared across different operations (POST, GET, PUT) -ensuring no diffs with terraform will happen at runtime due to inconsistency with properties. It is suggested to use the same -definition shared across the resource operations for a given version (e,g: $ref: "#/definitions/resource) so consistency -in terms of data model for a given resource version is maintained throughout all the operations. This helps keeping the -swagger file well structured and encourages object definition re-usability. Different end point versions should their own -payload definitions as the example below, path ```/v1/resource``` has a corresponding ```resourceV1``` definition object: +- POST operation may have a request body payload either defined inside the path’s configuration or referencing a schema object (using $ref) +defined at the root level [definitions](#swaggerDefinitions) section. The $ref can be a link to a local model definition or a definition hosted +externally. The request payload schema may be the same as the response schema, including the expected input properties (required and optional) as well +as the computed ones (readOnly): ```` /v1/resource: post: + parameters: - in: "body" name: "body" schema: - $ref: "#/definitions/resourceV1" # this can be a link to an external definition hostead somewhere else (e.g: $ref:"http://another-host.com/#/definitions/ContentDeliveryNetwork") - + $ref: "#/definitions/resourceV1" + responses: + 201: + schema: + $ref: "#/definitions/resourceV1" definitions:     resourceV1:         type: object    @@ -246,11 +238,80 @@ definitions:           type: string ```` -- If the POST operation does not contain a body parameter, in order for the endpoint to be considered terraform compliant: - - the operation responses must contain at least one 'successful' response which can be either a 200, 201 or 202 response. The schema associated with +- If the POST operation does not share the same definition for the request payload and the response payload, in order for the endpoint to be considered terraform compliant: + - the POST request payload must contain only input properties, that is required and optional properties. + - the POST operation responses must contain at least one 'successful' response which can be either a 200, 201 or 202 response. The schema associated with + the successful response will need to contain all the input properties from the request payload (but configured as readOnly) as well as any other + computed property (readOnly) auto-generated by the API. + - the POST response schema must contain only readOnly properties. If the response schema properties are not explicitly configured as readOnly, the provider will automatically convert them as computed (readOnly). + - If the POST response schema does not contain any property called 'id', at least one property must contain the [x-terraform-id](#attributeDetails) extension which + serves as the resource identifier. + +The following shows an example of a compatible terraform resource that expects in the request body only the input properties (`label` required property and `optional_property` an optioanl property) + and returns in the response payload both the inputs as well as any other output (computed properties) generated by the API: + +```` + /v1/resource: + post: + parameters: + - in: "body" + name: "body" + required: true + schema: + $ref: "#/definitions/ResourceRequestPayload" + responses: + 201: + schema: + $ref: "#/definitions/ResourceResponsePayload" + /v1/resource/{resource_id}: + get: + parameters: + - name: "resource_id" + in: "path" + required: true + type: "string" + responses: + 200: + schema: + $ref: "#/definitions/ResourceResponsePayload" + +definitions: + ResourceRequestPayload: + type: "object" + required: + - label + properties: + label: + type: "string" + optional_property: + type: "string" + ResourceResponsePayload: + type: "object" + properties: + id: + type: "string" + readOnly: true + label: + type: "string" + readOnly: true + optional_property: + type: "string" + readOnly: true +```` + +The resulted resource's terraform schema configuration will contain the combination of the request and response schemas keeping +the corresponding input configurations as is (eg: required and optional properties will still be required and optional in the resulted final schema) as well as the output computed properties (readOnly properties from the response). +Note, if both the request and response schema properties contain extensions and their values are different, the extension value kept +for the property will be the one in the response. + +- If the POST operation does not contain a request body payload, in order for the endpoint to be considered terraform compliant: + - the POST operation responses must contain at least one 'successful' response which can be either a 200, 201 or 202 response. The schema associated with the successful response will be the one used as the resource schema. Note if more than one successful response is present in the responses the first one found (in no particular order) will be used. - - the response schema must contain only read only properties; otherwise the resource will not be considered terraform compatible. + - the POST response schema must contain only readOnly properties. + - If the POST response schema does not contain any property called 'id', at least one property must contain the [x-terraform-id](#attributeDetails) extension which + serves as the resource identifier. + The following shows an example of a compatible terraform resource that does not expect any input upon creation but does return computed data: @@ -288,20 +349,33 @@ definitions: Refer to [readOnly](#attributeDetails) attributes to learn more about how to define an object that has computed properties (value auto-generated by the API). -- The schema object definition should be described on the root level [definitions](#swaggerDefinitions) section and must -not be embedded within the API definition. This is enforced to keep the swagger file well structured and to encourage -object re-usability across the resource CRUD operations. Operations such as POST/GET/PUT are expected to have a 'schema' property -with a link to the same definition (e,g: `$ref: "#/definitions/resource`). The ref can be a link to an external source -as described in the [OpenAPI documentation for $ref](https://swagger.io/docs/specification/using-ref/). +- The resource's POST, GET and PUT (if exposed) operations must have the same response schema configuration. This is required to ensure +the resource state is consistent. +- The resource's PUT operation (if exposed) request payload must be the same as the resources POST's request schema. The OpenAPI provider expects this + to ensure the update operation enables replacement of the representation of the target resource. + +- The resource's PUT operation may return one of the following successful responses: + - 200 OK with a response payload containing the final state of the resource representation in accordance with the state of the enclosed representation and any other computed property. The response schema must be the same as the GET operation response schema. + - 202 Accepted for async resources. Refer to [asynchronous resources](https://github.com/dikhan/terraform-provider-openapi/blob/master/docs/how_to.md#xTerraformResourcePollEnabled) for more info. + - 204 No Content with an empty response payload. + - The schema object must have a property that uniquely identifies the resource instance. This can be done by either having a computed property (readOnly) called ```id``` or by adding the [x-terraform-id](#attributeDetails) extension to one of the existing properties. + +- The resource schema object definition should be described on the root level [definitions](#swaggerDefinitions) section and should not + be embedded within the API definition. This is recommended to keep the OpenAPI document well structured and to encourage +object re-usability across the resource CRUD operations. The resource POST/GET/PUT operations are expected to have a 'schema' property +for both the request and response payloads with a link to the same definition (e,g: `$ref: "#/definitions/resource`). The ref can +be a link to an external source as described in the [OpenAPI documentation for $ref](https://swagger.io/docs/specification/using-ref/). -- The PUT operation may return one of the the following successful responses: - - 200 OK with a response payload containing the final state of the resource representation in accordance with the state of the enclosed representation and any other computed property. - - 202 Accepted for async resources. Refer to [asynchronous resources](https://github.com/dikhan/terraform-provider-openapi/blob/master/docs/how_to.md#xTerraformResourcePollEnabled) for more info. - - 204 No Content with an empty response payload. +- Paths should be versioned as described in the [versioning](#versioning) document following ‘/v{number}/resource’ pattern +(e,g: ‘/v1/resource’). A version upgrade (e,g: v1 -> v2) will be needed when the interface of the resource changes, hence +the new version is non backwards compatible. See that only the 'Major' version is considered in the path, this is recommended +as the service provider will have less paths to maintain overall. if there are minor/patches applied to the backend that +should not affect the way consumer interacts with the APIs whatsoever and the namespace should remain as is. + - Different end point versions should their own payload definitions as the example below, path ```/v1/resource``` has a corresponding ```resourceV1``` definition object: ###### Data source instance diff --git a/docs/release.md b/docs/release.md index be08a974b..06b2d519a 100644 --- a/docs/release.md +++ b/docs/release.md @@ -43,4 +43,24 @@ Note - For new releases, the PR title should follow the below convention (replac With the above title, the new version would be v0.2.0. -The PR will need one admin approval. Once approved and merged, the release will be automatically performed by Travis CI. \ No newline at end of file +The PR will need one admin approval. Once approved and merged, the release will be automatically performed by Travis CI. + +## How to release a new alpha version + +Alpha means the features haven't been locked down, it's an exploratory phase. Releasing an alpha version enable users to +start early adopting the version even though it may not be production ready yet and functionality might still change until +the final version released. The following targets have been created to help create alpha release versions: + +- To create a new alpha release version run the following command: +```` +RELEASE_ALPHA_VERSION=2.1.0 make release-alpha +```` +This will create a local tag in the form v$(RELEASE_ALPHA_VERSION)-alpha.1. For the example above that would be `v2.1.0-alpha.1` and +push the tag to origin. + +- To delete a previously create alpha version run the following command: +```` +RELEASE_ALPHA_VERSION=2.1.0 make delete-release-alpha +```` +This will delete a local tag in the form v$(RELEASE_ALPHA_VERSION)-alpha.1. For the example above that would be `v2.1.0-alpha.1` and +delete the tag in origin. \ No newline at end of file diff --git a/examples/swaggercodegen/api/resources/swagger.yaml b/examples/swaggercodegen/api/resources/swagger.yaml index 89a2aa99c..9e0b3654b 100644 --- a/examples/swaggercodegen/api/resources/swagger.yaml +++ b/examples/swaggercodegen/api/resources/swagger.yaml @@ -9,29 +9,29 @@ info: #host: "localhost:8443" # If host is not specified, it is assumed to be the same host where the API documentation is being served. #basePath: "" tags: -- name: "cdn" - description: "Operations about cdns" - externalDocs: - description: "Find out more about cdn api" - url: "https://github.com/dikhan/terraform-provider-openapi/tree/master/examples/swaggercodegen" -- name: "lb" - description: "Operations about lbs" - externalDocs: - description: "Find out more about lb api" - url: "https://github.com/dikhan/terraform-provider-openapi/tree/master/examples/swaggercodegen" -- name: "monitor" - description: "Operations about monitors" - externalDocs: - description: "Find out more about monitor api" - url: "https://github.com/dikhan/terraform-provider-openapi/tree/master/examples/swaggercodegen" + - name: "cdn" + description: "Operations about cdns" + externalDocs: + description: "Find out more about cdn api" + url: "https://github.com/dikhan/terraform-provider-openapi/tree/master/examples/swaggercodegen" + - name: "lb" + description: "Operations about lbs" + externalDocs: + description: "Find out more about lb api" + url: "https://github.com/dikhan/terraform-provider-openapi/tree/master/examples/swaggercodegen" + - name: "monitor" + description: "Operations about monitors" + externalDocs: + description: "Find out more about monitor api" + url: "https://github.com/dikhan/terraform-provider-openapi/tree/master/examples/swaggercodegen" schemes: -- "http" -- "https" + - "http" + - "https" consumes: -- "application/json" + - "application/json" produces: -- "application/json" + - "application/json" security: - apikey_auth: [] @@ -39,7 +39,7 @@ security: # This make the provider multiregional, so API calls will be make against the specific region as per the value provided # provided by the user according to the 'x-terraform-provider-regions' regions. If non is provided, the default value will # be the first item in the 'x-terraform-provider-regions' list of strings. in the case below that will be 'rst1' -x-terraform-provider-multiregion-fqdn: "some.api.${region}.domain.com" +x-terraform-provider-multiregion-fqdn: "some.api.${region}.nonexistingrandomdomain.io" # Making it a bit more random to avoid resolving to an actual existing domain x-terraform-provider-regions: "rst1,dub1" # This is legacy configuration that will be deprecated soon @@ -70,21 +70,21 @@ paths: x-terraform-resource-name: "cdn" x-terraform-resource-host: localhost:8443 # If this extension is specified, it will override the global host and API calls will be made against this host instead tags: - - "cdn" + - "cdn" summary: "Create cdn" operationId: "ContentDeliveryNetworkCreateV1" parameters: - - in: "body" - name: "body" - description: "Created CDN" - required: true - schema: - $ref: "#/definitions/ContentDeliveryNetworkV1" - - in: "header" - x-terraform-header: x_request_id - name: "X-Request-ID" - type: "string" - required: true + - in: "body" + name: "body" + description: "Created CDN" + required: true + schema: + $ref: "#/definitions/ContentDeliveryNetworkV1" + - in: "header" + x-terraform-header: x_request_id + name: "X-Request-ID" + type: "string" + required: true x-terraform-resource-timeout: "30s" responses: 201: @@ -100,16 +100,16 @@ paths: /v1/cdns/{cdn_id}: get: tags: - - "cdn" + - "cdn" summary: "Get cdn by id" description: "" operationId: "ContentDeliveryNetworkGetV1" parameters: - - name: "cdn_id" - in: "path" - description: "The cdn id that needs to be fetched." - required: true - type: "string" + - name: "cdn_id" + in: "path" + description: "The cdn id that needs to be fetched." + required: true + type: "string" #x-terraform-resource-timeout: "30s" If a given operation does not have the 'x-terraform-resource-timeout' extension; the resource operation timeout will default to 10m (10 minutes) responses: 200: @@ -124,21 +124,21 @@ paths: - apikey_auth: [] put: tags: - - "cdn" + - "cdn" summary: "Updated cdn" operationId: "ContentDeliveryNetworkUpdateV1" parameters: - - name: "id" - in: "path" - description: "cdn that needs to be updated" - required: true - type: "string" - - in: "body" - name: "body" - description: "Updated cdn object" - required: true - schema: - $ref: "#/definitions/ContentDeliveryNetworkV1" + - name: "cdn_id" + in: "path" + description: "cdn that needs to be updated" + required: true + type: "string" + - in: "body" + name: "body" + description: "Updated cdn object" + required: true + schema: + $ref: "#/definitions/ContentDeliveryNetworkV1" responses: 200: description: "successful operation" @@ -152,15 +152,15 @@ paths: - apikey_auth: [] delete: tags: - - "cdn" + - "cdn" summary: "Delete cdn" operationId: "ContentDeliveryNetworkDeleteV1" parameters: - - name: "id" - in: "path" - description: "The cdn that needs to be deleted" - required: true - type: "string" + - name: "cdn_id" + in: "path" + description: "The cdn that needs to be deleted" + required: true + type: "string" responses: 204: description: "successful operation, no content is returned" @@ -177,21 +177,21 @@ paths: post: x-terraform-resource-host: localhost:8443 # If this extension is specified, it will override the global host and API calls will be made against this host instead tags: - - "cdn" + - "cdn" summary: "Create cdn firewall" operationId: "ContentDeliveryNetworkFirewallCreateV1" parameters: - - name: "cdn_id" - in: "path" - description: "The cdn id that contains the firewall to be fetched." - required: true - type: "string" - - in: "body" - name: "body" - description: "Created CDN firewall" - required: true - schema: - $ref: "#/definitions/ContentDeliveryNetworkFirewallV1" + - name: "cdn_id" + in: "path" + description: "The cdn id that contains the firewall to be fetched." + required: true + type: "string" + - in: "body" + name: "body" + description: "Created CDN firewall" + required: true + schema: + $ref: "#/definitions/ContentDeliveryNetworkFirewallV1" responses: 201: description: "successful operation" @@ -204,21 +204,21 @@ paths: /v1/cdns/{cdn_id}/v1/firewalls/{fw_id}: get: tags: - - "cdn" + - "cdn" summary: "Get cdn firewall by id" description: "" operationId: "ContentDeliveryNetworkFirewallGetV1" parameters: - - name: "cdn_id" - in: "path" - description: "The cdn id that contains the firewall to be fetched." - required: true - type: "string" - - name: "fw_id" - in: "path" - description: "The cdn firewall id that needs to be fetched." - required: true - type: "string" + - name: "cdn_id" + in: "path" + description: "The cdn id that contains the firewall to be fetched." + required: true + type: "string" + - name: "fw_id" + in: "path" + description: "The cdn firewall id that needs to be fetched." + required: true + type: "string" responses: 200: description: "successful operation" @@ -237,16 +237,16 @@ paths: post: x-terraform-resource-host: localhost:8443 tags: - - "lb" + - "lb" summary: "Create lb v1" operationId: "LBCreateV1" parameters: - - in: "body" - name: "body" - description: "LB v1 payload object to be posted as part of the POST request" - required: true - schema: - $ref: "#/definitions/LBV1" + - in: "body" + name: "body" + description: "LB v1 payload object to be posted as part of the POST request" + required: true + schema: + $ref: "#/definitions/LBV1" x-terraform-resource-timeout: "2s" responses: 202: # Accepted @@ -263,16 +263,16 @@ paths: /v1/lbs/{id}: get: tags: - - "lb" + - "lb" summary: "Get lb v1 by id" description: "" operationId: "LBGetV1" parameters: - - name: "id" - in: "path" - description: "The lb v1 id that needs to be fetched." - required: true - type: "string" + - name: "id" + in: "path" + description: "The lb v1 id that needs to be fetched." + required: true + type: "string" responses: 200: description: "successful operation" @@ -284,21 +284,21 @@ paths: description: "LB not found" put: tags: - - "lb" + - "lb" summary: "Updated cdn" operationId: "LBUpdateV1" parameters: - - name: "id" - in: "path" - description: "lb v1 that needs to be updated" - required: true - type: "string" - - in: "body" - name: "body" - description: "Updated cdn object" - required: true - schema: - $ref: "#/definitions/LBV1" + - name: "id" + in: "path" + description: "lb v1 that needs to be updated" + required: true + type: "string" + - in: "body" + name: "body" + description: "Updated cdn object" + required: true + schema: + $ref: "#/definitions/LBV1" # x-terraform-resource-timeout: "30s" If a given operation does not have the 'x-terraform-resource-timeout' extension; the resource operation timeout will default to 10m (10 minutes) responses: 202: # Accepted @@ -314,15 +314,15 @@ paths: description: "LB v1 not found" delete: tags: - - "lb" + - "lb" summary: "Delete lb v1" operationId: "LBDeleteV1" parameters: - - name: "id" - in: "path" - description: "The lb v1 that needs to be deleted" - required: true - type: "string" + - name: "id" + in: "path" + description: "The lb v1 that needs to be deleted" + required: true + type: "string" responses: 202: description: "LB v1 deletion" @@ -345,17 +345,17 @@ paths: /v1/monitors: post: tags: - - "monitor" + - "monitor" summary: "Create monitor v1" - operationId: "MonitorV1" - x-terraform-resource-host: "some.api.${monitor}.domain.com" + operationId: "CreateMonitorV1" + x-terraform-resource-host: "some.api.${monitor}.nonexistingrandomdomain.io" # Making it a bit more random to avoid resolving to an actual existing domain parameters: - - in: "body" - name: "body" - description: "Monitor v1 payload object to be posted as part of the POST request" - required: true - schema: - $ref: "#/definitions/MonitorV1" + - in: "body" + name: "body" + description: "Monitor v1 payload object to be posted as part of the POST request" + required: true + schema: + $ref: "#/definitions/MonitorV1" responses: 200: description: "this operation is asynchronous, to check the status of the deployment call GET operation and check the status field returned in the payload" @@ -368,16 +368,16 @@ paths: /v1/monitors/{id}: get: tags: - - "monitor" + - "monitor" summary: "Get monitor by id" description: "" - operationId: "MonitorV1" + operationId: "GetMonitorV1" parameters: - - name: "id" - in: "path" - description: "The monitor v1 id that needs to be fetched." - required: true - type: "string" + - name: "id" + in: "path" + description: "The monitor v1 id that needs to be fetched." + required: true + type: "string" responses: 200: description: "successful operation" @@ -398,16 +398,16 @@ paths: /v1/multiregionmonitors: post: tags: - - "multi_region_monitor" + - "multi_region_monitor" summary: "Create monitor v1" operationId: "MonitorV1" parameters: - - in: "body" - name: "body" - description: "Monitor v1 payload object to be posted as part of the POST request" - required: true - schema: - $ref: "#/definitions/MonitorV1" + - in: "body" + name: "body" + description: "Monitor v1 payload object to be posted as part of the POST request" + required: true + schema: + $ref: "#/definitions/MonitorV1" responses: 200: description: "this operation is asynchronous, to check the status of the deployment call GET operation and check the status field returned in the payload" @@ -420,16 +420,16 @@ paths: /v1/multiregionmonitors/{id}: get: tags: - - "multi_region_monitor" + - "multi_region_monitor" summary: "Get monitor by id" description: "" - operationId: "MonitorV1" + operationId: "GetMultiRegionMonitorV1" parameters: - - name: "id" - in: "path" - description: "The monitor v1 id that needs to be fetched." - required: true - type: "string" + - name: "id" + in: "path" + description: "The monitor v1 id that needs to be fetched." + required: true + type: "string" responses: 200: description: "successful operation" @@ -475,7 +475,7 @@ definitions: id: type: "string" readOnly: true # This property will not be considered when creating a new resource, however, it is expected to - # to be returned from the api, and will be saved as computed value in the terraform state file + # to be returned from the api, and will be saved as computed value in the terraform state file label: type: "string" x-terraform-immutable: true @@ -538,11 +538,11 @@ definitions: ObjectProperty: type: object required: - - message - - detailedMessage - - exampleInt - - exampleNumber - - example_boolean + - message + - detailedMessage + - exampleInt + - exampleNumber + - example_boolean properties: message: type: string @@ -559,8 +559,8 @@ definitions: LBV1: type: "object" required: - - name - - backends + - name + - backends properties: id: type: "string" @@ -573,19 +573,19 @@ definitions: 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 + # 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 description: lb resource status type: string readOnly: true enum: # this is just for documentation purposes and to let the consumer know what statues should be expected - - deploy_pending - - deploy_in_progress - - deploy_failed - - deployed - - delete_pending - - delete_in_progress - - delete_failed - - deleted + - deploy_pending + - deploy_in_progress + - deploy_failed + - deployed + - delete_pending + - delete_in_progress + - delete_failed + - deleted 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) @@ -607,7 +607,7 @@ definitions: MonitorV1: type: "object" required: - - name + - name properties: id: type: "string" @@ -618,7 +618,7 @@ definitions: ContentDeliveryNetworkFirewallV1: type: "object" required: - - name + - name properties: id: type: "string" @@ -630,8 +630,8 @@ definitions: Error: type: object required: - - code - - message + - code + - message properties: code: type: string diff --git a/go.mod b/go.mod index afc9d585e..47fdb7ab6 100644 --- a/go.mod +++ b/go.mod @@ -21,6 +21,6 @@ require ( github.com/stretchr/testify v1.6.1 golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a // indirect golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 // indirect - golang.org/x/tools v0.1.3 // indirect + golang.org/x/tools v0.1.5 // indirect gopkg.in/yaml.v2 v2.2.4 ) diff --git a/go.sum b/go.sum index 4736107e5..718ac98cb 100644 --- a/go.sum +++ b/go.sum @@ -586,6 +586,8 @@ golang.org/x/tools v0.0.0-20200713011307-fd294ab11aed/go.mod h1:njjCfa9FT2d7l9Bc golang.org/x/tools v0.0.0-20200717024301-6ddee64345a6/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.1.3 h1:L69ShwSZEyCsLKoAxDKeMvLDZkumEe8gXUZAjab0tX8= golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.5 h1:ouewzE6p+/VEB31YYnTbEJdi8pFqKp4P4n85vwo3DHA= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/openapi/openapi_v2_spec_analyser.go b/openapi/openapi_v2_spec_analyser.go index ad7bf589b..4c67eb053 100644 --- a/openapi/openapi_v2_spec_analyser.go +++ b/openapi/openapi_v2_spec_analyser.go @@ -228,9 +228,12 @@ func (specAnalyser *specV2Analyser) GetAPIBackendConfiguration() (SpecBackendCon // - The path given has GET operation defined (required). PUT and DELETE are optional // - The root path for the given path 'resourcePath' is found (e,g: "/users") // - The root path for the given path 'resourcePath' has mandatory POST operation defined -// - The root path for the given path 'resourcePath' has a parameter of type 'body' with a schema property referencing to an existing definition object -// - The root path POST payload definition and the returned object in the response matches. Similarly, the GET operation should also have the same return object +// - The root path POST operation for the given path 'resourcePath' has a parameter of type 'body' with a schema property referencing to an existing definition object or defining the schema inline (in which case the properties must be all input either required or optional properties) +// - The root path POST operation request payload definition and the response schema definitions may be the same (eg: they share the same definition model that declares both inputs like required/optional properties and the outputs as readOnly properties). +// - The root path POST operation request payload definition and the response schema definitions may be different (eg: the request schema contains only the inputs as required/optional properties and the response schema contains the inputs in the form of readOnly properties plus any other property that might be auth-generated by the API also configured as readOnly). +// - The path given GET operation schema must match the root path POST response schema // - The resource schema definition must contain a field that uniquely identifies the resource or have a field with the 'x-terraform-id' extension set to true +// For more info about the requirements: https://github.com/dikhan/terraform-provider-openapi/blob/master/docs/how_to.md#terraform-compliant-resource-requirements // For instance, if resourcePath was "/users/{id}" and paths contained the following entries and implementations: // paths: // /v1/users: @@ -314,10 +317,7 @@ func (specAnalyser *specV2Analyser) isEndPointTerraformDataSourceCompliant(path } func (specAnalyser *specV2Analyser) validateInstancePath(path string) error { - isResourceInstance, err := specAnalyser.isResourceInstanceEndPoint(path) - if err != nil { - return fmt.Errorf("error occurred while checking if path '%s' is a resource instance path", path) - } + isResourceInstance := specAnalyser.isResourceInstanceEndPoint(path) if !isResourceInstance { return fmt.Errorf("path '%s' is not a resource instance path", path) } @@ -342,7 +342,7 @@ func (specAnalyser *specV2Analyser) validateRootPath(resourcePath string) (strin resourceRootPathItem, _ := specAnalyser.d.Spec().Paths.Paths[resourceRootPath] resourceRootPostOperation := resourceRootPathItem.Post - resourceRootPostSchemaDef, err := specAnalyser.getBodyParameterBodySchema(resourceRootPostOperation) + resourceRootPostRequestSchemaDef, err := specAnalyser.getBodyParameterBodySchema(resourceRootPostOperation) if err != nil { bodyParam := specAnalyser.bodyParameterExists(resourceRootPostOperation) // Use case where resource does not expect any input as part of the POST root operation, and only produces computed properties @@ -360,7 +360,122 @@ func (specAnalyser *specV2Analyser) validateRootPath(resourcePath string) (strin return "", nil, nil, fmt.Errorf("resource root path '%s' POST operation validation error: %s", resourceRootPath, err) } - return resourceRootPath, &resourceRootPathItem, resourceRootPostSchemaDef, nil + resourceRootPostResponseSchemaDef, err := specAnalyser.getSuccessfulResponseDefinition(resourceRootPostOperation) + if err != nil { + log.Printf("[DEBUG] failed to get the resource '%s' root path POST successful response configuration: %s", resourceRootPath, err) + return "", nil, nil, fmt.Errorf("resource root path '%s' POST operation is missing a successful response definition: %s", resourceRootPath, err) + } + + if specAnalyser.schemaIsEqual(resourceRootPostRequestSchemaDef, resourceRootPostResponseSchemaDef) { + log.Printf("[DEBUG] resource '%s' root path POST's req and resp schema definitions are the same", resourceRootPath) + return resourceRootPath, &resourceRootPathItem, resourceRootPostRequestSchemaDef, nil + } + + // Use case where resource POST's request payload model is different than the response payload (eg: request payload does not contain the id property (or any computed properties) and the response payload contains the inputs (as computed props already) and any other computed property that might be returned by the POST operation + log.Printf("[DEBUG] resource '%s' root path POST's req and resp schemas not matching, checking if request schema is contained in the response schema and attemping to merge into one schema containing both the request and response schemas that contain both the required/optional inputs as well as all the computed properties", resourceRootPath) + // if response payload contains the request properties but readOnly then that's a valid use case too + mergedPostReqAndRespPayloadSchemas, err := specAnalyser.mergeRequestAndResponseSchemas(resourceRootPostRequestSchemaDef, resourceRootPostResponseSchemaDef) + if err != nil { + log.Printf("[DEBUG] failed to merge resource '%s' root path POST request and response schemas: %s", resourceRootPath, err) + return "", nil, nil, fmt.Errorf("resource root path '%s' POST operation does not meet any of the supported use cases", resourceRootPath) + } + log.Printf("[INFO] resource '%s' root path POST's req and resp merged into one: %+v", resourceRootPath, mergedPostReqAndRespPayloadSchemas) + return resourceRootPath, &resourceRootPathItem, mergedPostReqAndRespPayloadSchemas, nil +} + +// mergeRequestAndResponseSchemas attempts to merge the request schema and response schema and validates whether they compliant +// with the following specification: +// - the request schema must contain only properties that are either required or optional, not readOnly. If there are readOnly properties in the request schema, they will be ignored and not considered in the final schema. +// - the response schema must contain only properties that are readOnly, if they are not they will be converted automatically as readOnly in the final schema +// - the response schema must contain all the properties from the request schema but configured as readOnly +// If the above requirements are met, the resulted merged schema will be configured as follows: +// - All the properties from the request schema will be kept as is and integrated in the merged schema +// - All the properties from the response schema will be kept as is and integrated in the merged schema. The properties that are also in the request schema will not be integrated as the configuraiton in the request schema will be kept for those +// - The resulted merged properties will contain the extensions from both the request and response schema properties. However, if there +// is a matching property on both schema's but with a different value, the value in the response schema extension would take preference and will be the one kept in the final merged schema +func (specAnalyser *specV2Analyser) mergeRequestAndResponseSchemas(requestSchema *spec.Schema, responseSchema *spec.Schema) (*spec.Schema, error) { + if requestSchema == nil { + return nil, fmt.Errorf("resource missing request schema") + } + if responseSchema == nil { + return nil, fmt.Errorf("resource missing response schema") + } + responseSchemaProps := responseSchema.Properties + requestSchemaProps := requestSchema.Properties + // responseSchema must contain at least 1 more property than requestSchema since it is expected that responseSchema will have the id readOnly property + if len(responseSchemaProps) < len(requestSchemaProps) { + return nil, fmt.Errorf("resource response schema contains less properties than the request schema, response schema must contain the request schema properties to be able to merge both schemas") + } + + // Init merged schema to empty + mergedSchema := &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Properties: map[string]spec.Schema{}, + }, + } + + // Copy response schema props into the merge schema. This avoids potential issues with pointers where when overriding + // the response schema with the requests it would override the original response schema property too + for responsePropName, responseProp := range responseSchemaProps { + if !responseProp.ReadOnly { + log.Printf("[WARN] resource's response schema property '%s' must be readOnly as response properties are considered computed (returned by the API). Therefore, the provider will automatically convert it to readOnly in the final resource schema", responsePropName) + responseProp.ReadOnly = true + } + mergedSchema.Properties[responsePropName] = responseProp + } + // Ensure only the request's required properties are kept as required too in the final merged schema + mergedSchema.Required = requestSchema.Required + for requestSchemaPropName, requestSchemaProp := range requestSchemaProps { + + // + // Ignoring the request property if it's readOnly. If the property is to be returned by the POST response it should be + // defined as part of the response schema and therefore it will be added to the final schema accordingly. + // This decision is made to ensure compliance with OpenAPI spec 2.0 but instead of failing is gracefully handling the 'badly' documented document. + // More info on readOnly here: https://swagger.io/specification/v2/#fixed-fields-13 (readOnly section) + // A "read only" property means that it MAY be sent as part of a response but MUST NOT be sent as part of the request. + // Properties marked as readOnly being true SHOULD NOT be in the required list of the defined schema. + if requestSchemaProp.ReadOnly { + continue + } + + _, exists := responseSchemaProps[requestSchemaPropName] + if !exists { + return nil, fmt.Errorf("resource's request schema property '%s' not contained in the response schema", requestSchemaPropName) + } + + // Override response property with request property so the property input configuration is kept as is + mergedSchema.Properties[requestSchemaPropName] = requestSchemaProp + + // Ensure the extensions from both the request and response schemas are kept. + // If the same extension is present in both the request and response but with different values, the extension value in the response schema takes preference + for extensionName, extensionValue := range responseSchemaProps[requestSchemaPropName].Extensions { + mergedProp := mergedSchema.Properties[requestSchemaPropName] + if mergedProp.Extensions == nil { + mergedProp.Extensions = map[string]interface{}{} + mergedSchema.Properties[requestSchemaPropName] = mergedProp + } + mergedProp.Extensions[extensionName] = extensionValue + } + } + return mergedSchema, nil +} + +func (specAnalyser *specV2Analyser) schemaIsEqual(requestSchema *spec.Schema, responseSchema *spec.Schema) bool { + if requestSchema == responseSchema { + return true + } + if requestSchema != nil && responseSchema != nil { + requestSchemaJSON, err := requestSchema.MarshalJSON() + if err == nil { + responseSchemaJSON, err := responseSchema.MarshalJSON() + if err == nil { + if string(requestSchemaJSON) == string(responseSchemaJSON) { + return true + } + } + } + } + return false } // getSuccessfulResponseDefinition is responsible for getting the model definition from the response that matches a successful @@ -449,9 +564,9 @@ func (specAnalyser *specV2Analyser) getBodyParameterBodySchema(resourceRootPostO } // isResourceInstanceEndPoint checks if the given path is of form /resource/{id} -func (specAnalyser *specV2Analyser) isResourceInstanceEndPoint(p string) (bool, error) { +func (specAnalyser *specV2Analyser) isResourceInstanceEndPoint(p string) bool { r, _ := regexp.Compile("^.*{.+}[\\/]?$") - return r.MatchString(p), nil + return r.MatchString(p) } // findMatchingResourceRootPath returns the corresponding POST root and path for a given end point diff --git a/openapi/openapi_v2_spec_analyser_test.go b/openapi/openapi_v2_spec_analyser_test.go index 16445b410..dfdf8afe3 100644 --- a/openapi/openapi_v2_spec_analyser_test.go +++ b/openapi/openapi_v2_spec_analyser_test.go @@ -217,6 +217,960 @@ func Test_bodyParameterExists(t *testing.T) { }) } +func Test_mergeRequestAndResponseSchemas(t *testing.T) { + testCases := []struct { + name string + requestSchema *spec.Schema + responseSchema *spec.Schema + expectedMergedSchema *spec.Schema + expectedError string + }{ + { + name: "request schema is nil", + requestSchema: nil, + responseSchema: &spec.Schema{}, + expectedMergedSchema: nil, + expectedError: "resource missing request schema", + }, + { + name: "response schema is nil", + requestSchema: &spec.Schema{}, + responseSchema: nil, + expectedMergedSchema: nil, + expectedError: "resource missing response schema", + }, + { + name: "request schema contains more properties than response schema, this is not valid as response should always contain the request properties plus any other computed that is computed", + requestSchema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Required: []string{"required_prop"}, + Properties: map[string]spec.Schema{ + "optional_prop": { + SchemaProps: spec.SchemaProps{ + Type: spec.StringOrArray{"string"}, + }, + }, + "required_prop": { + SwaggerSchemaProps: spec.SwaggerSchemaProps{}, + SchemaProps: spec.SchemaProps{ + Type: spec.StringOrArray{"string"}, + }, + }, + }, + }, + }, + responseSchema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Required: []string{"required_prop"}, + Properties: map[string]spec.Schema{ + "required_prop": { + SwaggerSchemaProps: spec.SwaggerSchemaProps{}, + SchemaProps: spec.SchemaProps{ + Type: spec.StringOrArray{"string"}, + }, + }, + }, + }, + }, + expectedMergedSchema: nil, + expectedError: "resource response schema contains less properties than the request schema, response schema must contain the request schema properties to be able to merge both schemas", + }, + { + name: "response schema is missing request schema properties", + requestSchema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Required: []string{"required_prop"}, + Properties: map[string]spec.Schema{ + "required_prop": { + SwaggerSchemaProps: spec.SwaggerSchemaProps{}, + SchemaProps: spec.SchemaProps{ + Type: spec.StringOrArray{"string"}, + }, + }, + }, + }, + }, + responseSchema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Properties: map[string]spec.Schema{ + "id": { + SwaggerSchemaProps: spec.SwaggerSchemaProps{ + ReadOnly: true, + }, + SchemaProps: spec.SchemaProps{ + Type: spec.StringOrArray{"string"}, + }, + }, + }, + }, + }, + expectedMergedSchema: nil, + expectedError: "resource's request schema property 'required_prop' not contained in the response schema", + }, + { + name: "request properties contain readOnly properties and the response schema contains the request input properties (required/optional) as well as any other computed property", + requestSchema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Required: []string{"required_prop"}, + Properties: map[string]spec.Schema{ + "required_prop": { + SwaggerSchemaProps: spec.SwaggerSchemaProps{}, + SchemaProps: spec.SchemaProps{ + Type: spec.StringOrArray{"string"}, + }, + }, + "some_computed_property": { // readOnly props from the request schema are not considered in the final merged schema + SwaggerSchemaProps: spec.SwaggerSchemaProps{ + ReadOnly: true, + }, + SchemaProps: spec.SchemaProps{ + Type: spec.StringOrArray{"string"}, + }, + }, + }, + }, + }, + responseSchema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Properties: map[string]spec.Schema{ + "required_prop": { + SwaggerSchemaProps: spec.SwaggerSchemaProps{ + ReadOnly: true, + }, + SchemaProps: spec.SchemaProps{ + Type: spec.StringOrArray{"string"}, + }, + }, + "some_computed_property": { // since the response schema also contains the some_computed_property it will be included in the final merged schema + SwaggerSchemaProps: spec.SwaggerSchemaProps{ + ReadOnly: true, + }, + SchemaProps: spec.SchemaProps{ + Type: spec.StringOrArray{"string"}, + }, + }, + }, + }, + }, + expectedMergedSchema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Required: []string{"required_prop"}, + Properties: map[string]spec.Schema{ + "some_computed_property": { + SwaggerSchemaProps: spec.SwaggerSchemaProps{ + ReadOnly: true, + }, + SchemaProps: spec.SchemaProps{ + Type: spec.StringOrArray{"string"}, + }, + }, + "required_prop": { + SchemaProps: spec.SchemaProps{ + Type: spec.StringOrArray{"string"}, + }, + }, + }, + }, + }, + expectedError: "", + }, + { + name: "response contains properties that are not readOnly and the provide will automatically configure them as readOnly in the final merged schema", + requestSchema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Properties: map[string]spec.Schema{ + "some_property": { + SchemaProps: spec.SchemaProps{ + Type: spec.StringOrArray{"string"}, + }, + }, + }, + }, + }, + responseSchema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Properties: map[string]spec.Schema{ + "some_property": { + SchemaProps: spec.SchemaProps{ + Type: spec.StringOrArray{"string"}, + }, + SwaggerSchemaProps: spec.SwaggerSchemaProps{ + ReadOnly: true, + }, + }, + "some_computed_property": { + SwaggerSchemaProps: spec.SwaggerSchemaProps{}, // Not readOnly although it should + SchemaProps: spec.SchemaProps{ + Type: spec.StringOrArray{"string"}, + }, + }, + }, + }, + }, + expectedMergedSchema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Properties: map[string]spec.Schema{ + "some_property": { + SchemaProps: spec.SchemaProps{ + Type: spec.StringOrArray{"string"}, + }, + }, + "some_computed_property": { // The merged schema converted automatically the response property as readOnly + SchemaProps: spec.SchemaProps{ + Type: spec.StringOrArray{"string"}, + }, + SwaggerSchemaProps: spec.SwaggerSchemaProps{ + ReadOnly: true, + }, + }, + }, + }, + }, + expectedError: "", + }, + { + name: "request and response schemas are merged successfully, request's required properties are kept as is as well as the optional properties and any other response's computed property is merged into the final schema", + requestSchema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Required: []string{"required_prop"}, + Properties: map[string]spec.Schema{ + "required_prop": { + SchemaProps: spec.SchemaProps{ + Type: spec.StringOrArray{"string"}, + }, + }, + "optional_prop": { + SchemaProps: spec.SchemaProps{ + Type: spec.StringOrArray{"string"}, + }, + }, + }, + }, + }, + responseSchema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Required: []string{"id", "optional_prop", "required_prop"}, + Properties: map[string]spec.Schema{ + "id": { + SwaggerSchemaProps: spec.SwaggerSchemaProps{ + ReadOnly: true, + }, + SchemaProps: spec.SchemaProps{ + Type: spec.StringOrArray{"string"}, + }, + }, + "optional_prop": { + SwaggerSchemaProps: spec.SwaggerSchemaProps{ + ReadOnly: true, + }, + SchemaProps: spec.SchemaProps{ + Type: spec.StringOrArray{"string"}, + }, + }, + "required_prop": { + SwaggerSchemaProps: spec.SwaggerSchemaProps{ + ReadOnly: true, + }, + SchemaProps: spec.SchemaProps{ + Type: spec.StringOrArray{"string"}, + }, + }, + }, + }, + }, + expectedMergedSchema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Required: []string{"required_prop"}, + Properties: map[string]spec.Schema{ + "id": { + SwaggerSchemaProps: spec.SwaggerSchemaProps{ + ReadOnly: true, + }, + SchemaProps: spec.SchemaProps{ + Type: spec.StringOrArray{"string"}, + }, + }, + "optional_prop": { + SchemaProps: spec.SchemaProps{ + Type: spec.StringOrArray{"string"}, + }, + }, + "required_prop": { + SchemaProps: spec.SchemaProps{ + Type: spec.StringOrArray{"string"}, + }, + }, + }, + }, + }, + expectedError: "", + }, + { + name: "request and response schemas are merged successfully, extensions in the response schema are kept as is", + requestSchema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Required: []string{"required_prop"}, + Properties: map[string]spec.Schema{ + "required_prop": { + SchemaProps: spec.SchemaProps{ + Type: spec.StringOrArray{"string"}, + }, + }, + }, + }, + }, + responseSchema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Properties: map[string]spec.Schema{ + "identifier_property": { + SwaggerSchemaProps: spec.SwaggerSchemaProps{ + ReadOnly: true, + }, + SchemaProps: spec.SchemaProps{ + Type: spec.StringOrArray{"string"}, + }, + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + extTfID: true, + }, + }, + }, + "required_prop": { + SwaggerSchemaProps: spec.SwaggerSchemaProps{ + ReadOnly: true, + }, + SchemaProps: spec.SchemaProps{ + Type: spec.StringOrArray{"string"}, + }, + }, + }, + }, + }, + expectedMergedSchema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Required: []string{"required_prop"}, + Properties: map[string]spec.Schema{ + "identifier_property": { + SwaggerSchemaProps: spec.SwaggerSchemaProps{ + ReadOnly: true, + }, + SchemaProps: spec.SchemaProps{ + Type: spec.StringOrArray{"string"}, + }, + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + extTfID: true, + }, + }, + }, + "required_prop": { + SchemaProps: spec.SchemaProps{ + Type: spec.StringOrArray{"string"}, + }, + }, + }, + }, + }, + expectedError: "", + }, + { + name: "request and response schemas are merged successfully, extensions in the request schema are kept as is", + requestSchema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Required: []string{"required_prop"}, + Properties: map[string]spec.Schema{ + "required_prop": { + SchemaProps: spec.SchemaProps{ + Type: spec.StringOrArray{"string"}, + }, + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + extTfFieldName: "required_preferred_name_prop", + }, + }, + }, + }, + }, + }, + responseSchema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Required: []string{"id", "required_prop"}, + Properties: map[string]spec.Schema{ + "id": { + SwaggerSchemaProps: spec.SwaggerSchemaProps{ + ReadOnly: true, + }, + SchemaProps: spec.SchemaProps{ + Type: spec.StringOrArray{"string"}, + }, + }, + "required_prop": { + SwaggerSchemaProps: spec.SwaggerSchemaProps{ + ReadOnly: true, + }, + SchemaProps: spec.SchemaProps{ + Type: spec.StringOrArray{"string"}, + }, + }, + }, + }, + }, + expectedMergedSchema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Required: []string{"required_prop"}, + Properties: map[string]spec.Schema{ + "id": { + SwaggerSchemaProps: spec.SwaggerSchemaProps{ + ReadOnly: true, + }, + SchemaProps: spec.SchemaProps{ + Type: spec.StringOrArray{"string"}, + }, + }, + "required_prop": { + SchemaProps: spec.SchemaProps{ + Type: spec.StringOrArray{"string"}, + }, + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + extTfFieldName: "required_preferred_name_prop", + }, + }, + }, + }, + }, + }, + expectedError: "", + }, + { + name: "request and response schemas are merged successfully, extensions in the request schema is nil and the response does have an extension", + requestSchema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Required: []string{"required_prop"}, + Properties: map[string]spec.Schema{ + "required_prop": { + SchemaProps: spec.SchemaProps{ + Type: spec.StringOrArray{"string"}, + }, + }, + }, + }, + }, + responseSchema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Properties: map[string]spec.Schema{ + "id": { + SwaggerSchemaProps: spec.SwaggerSchemaProps{ + ReadOnly: true, + }, + SchemaProps: spec.SchemaProps{ + Type: spec.StringOrArray{"string"}, + }, + }, + "required_prop": { + SwaggerSchemaProps: spec.SwaggerSchemaProps{ + ReadOnly: true, + }, + SchemaProps: spec.SchemaProps{ + Type: spec.StringOrArray{"string"}, + }, + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + extTfFieldName: "required_preferred_name_prop", + }, + }, + }, + }, + }, + }, + expectedMergedSchema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Required: []string{"required_prop"}, + Properties: map[string]spec.Schema{ + "id": { + SwaggerSchemaProps: spec.SwaggerSchemaProps{ + ReadOnly: true, + }, + SchemaProps: spec.SchemaProps{ + Type: spec.StringOrArray{"string"}, + }, + }, + "required_prop": { + SchemaProps: spec.SchemaProps{ + Type: spec.StringOrArray{"string"}, + }, + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + extTfFieldName: "required_preferred_name_prop", + }, + }, + }, + }, + }, + }, + expectedError: "", + }, + { + name: "request and response schemas are merged successfully, extensions in the response schema is nil and the request does have an extension", + requestSchema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Required: []string{"required_prop"}, + Properties: map[string]spec.Schema{ + "required_prop": { + SchemaProps: spec.SchemaProps{ + Type: spec.StringOrArray{"string"}, + }, + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + extTfFieldName: "required_preferred_name_prop", + }, + }, + }, + }, + }, + }, + responseSchema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Properties: map[string]spec.Schema{ + "id": { + SwaggerSchemaProps: spec.SwaggerSchemaProps{ + ReadOnly: true, + }, + SchemaProps: spec.SchemaProps{ + Type: spec.StringOrArray{"string"}, + }, + }, + "required_prop": { + SwaggerSchemaProps: spec.SwaggerSchemaProps{ + ReadOnly: true, + }, + SchemaProps: spec.SchemaProps{ + Type: spec.StringOrArray{"string"}, + }, + }, + }, + }, + }, + expectedMergedSchema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Required: []string{"required_prop"}, + Properties: map[string]spec.Schema{ + "id": { + SwaggerSchemaProps: spec.SwaggerSchemaProps{ + ReadOnly: true, + }, + SchemaProps: spec.SchemaProps{ + Type: spec.StringOrArray{"string"}, + }, + }, + "required_prop": { + SchemaProps: spec.SchemaProps{ + Type: spec.StringOrArray{"string"}, + }, + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + extTfFieldName: "required_preferred_name_prop", + }, + }, + }, + }, + }, + }, + expectedError: "", + }, + { + name: "request and response schemas are merged successfully, response schema extensions take preference when both the request and response have the same extension in a property and with different values", + requestSchema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Required: []string{"required_prop"}, + Properties: map[string]spec.Schema{ + "required_prop": { + SchemaProps: spec.SchemaProps{ + Type: spec.StringOrArray{"string"}, + }, + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + extTfFieldName: "required_request_preferred_name_prop", + }, + }, + }, + }, + }, + }, + responseSchema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Required: []string{"id", "required_prop"}, + Properties: map[string]spec.Schema{ + "id": { + SwaggerSchemaProps: spec.SwaggerSchemaProps{ + ReadOnly: true, + }, + SchemaProps: spec.SchemaProps{ + Type: spec.StringOrArray{"string"}, + }, + }, + "required_prop": { + SwaggerSchemaProps: spec.SwaggerSchemaProps{ + ReadOnly: true, + }, + SchemaProps: spec.SchemaProps{ + Type: spec.StringOrArray{"string"}, + }, + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + extTfFieldName: "required_response_preferred_name_prop", + }, + }, + }, + }, + }, + }, + expectedMergedSchema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Required: []string{"required_prop"}, + Properties: map[string]spec.Schema{ + "id": { + SwaggerSchemaProps: spec.SwaggerSchemaProps{ + ReadOnly: true, + }, + SchemaProps: spec.SchemaProps{ + Type: spec.StringOrArray{"string"}, + }, + }, + "required_prop": { + SchemaProps: spec.SchemaProps{ + Type: spec.StringOrArray{"string"}, + }, + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + extTfFieldName: "required_response_preferred_name_prop", + }, + }, + }, + }, + }, + }, + expectedError: "", + }, + { + name: "request and response schemas are merged successfully, final merged schema only keeps in the required list the required properties in the request schema", + requestSchema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Required: []string{"required_prop"}, + Properties: map[string]spec.Schema{ + "required_prop": { + SchemaProps: spec.SchemaProps{ + Type: spec.StringOrArray{"string"}, + }, + }, + }, + }, + }, + responseSchema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Required: []string{"id"}, + Properties: map[string]spec.Schema{ + "id": { + SwaggerSchemaProps: spec.SwaggerSchemaProps{ + ReadOnly: true, + }, + SchemaProps: spec.SchemaProps{ + Type: spec.StringOrArray{"string"}, + }, + }, + "required_prop": { + SwaggerSchemaProps: spec.SwaggerSchemaProps{ + ReadOnly: true, + }, + SchemaProps: spec.SchemaProps{ + Type: spec.StringOrArray{"string"}, + }, + }, + }, + }, + }, + expectedMergedSchema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Required: []string{"required_prop"}, + Properties: map[string]spec.Schema{ + "id": { + SwaggerSchemaProps: spec.SwaggerSchemaProps{ + ReadOnly: true, + }, + SchemaProps: spec.SchemaProps{ + Type: spec.StringOrArray{"string"}, + }, + }, + "required_prop": { + SchemaProps: spec.SchemaProps{ + Type: spec.StringOrArray{"string"}, + }, + }, + }, + }, + }, + expectedError: "", + }, + } + + for _, tc := range testCases { + specV2Analyser := specV2Analyser{} + mergedSchema, err := specV2Analyser.mergeRequestAndResponseSchemas(tc.requestSchema, tc.responseSchema) + if tc.expectedError != "" { + assert.Equal(t, tc.expectedError, err.Error(), tc.name) + } else { + assert.Equal(t, tc.expectedMergedSchema, mergedSchema, tc.name) + } + } +} + +func Test_schemaIsEqual(t *testing.T) { + testSchema := &spec.Schema{} + testCases := []struct { + name string + requestSchema *spec.Schema + responseSchema *spec.Schema + expectedOutput bool + }{ + { + name: "request schema and response schema are equal (empty schemas)", + requestSchema: &spec.Schema{}, + responseSchema: &spec.Schema{}, + expectedOutput: true, + }, + { + name: "request schema and response schema are equal (same pointer)", + requestSchema: testSchema, + responseSchema: testSchema, + expectedOutput: true, + }, + { + name: "request schema and response schema are equal", + requestSchema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Required: []string{"required_prop"}, + Properties: map[string]spec.Schema{ + "id": { + SwaggerSchemaProps: spec.SwaggerSchemaProps{ + ReadOnly: true, + }, + SchemaProps: spec.SchemaProps{ + Type: spec.StringOrArray{"string"}, + }, + }, + "required_prop": { + SwaggerSchemaProps: spec.SwaggerSchemaProps{}, + SchemaProps: spec.SchemaProps{ + Type: spec.StringOrArray{"string"}, + }, + }, + }, + }, + }, + responseSchema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Required: []string{"required_prop"}, + Properties: map[string]spec.Schema{ + "id": { + SwaggerSchemaProps: spec.SwaggerSchemaProps{ + ReadOnly: true, + }, + SchemaProps: spec.SchemaProps{ + Type: spec.StringOrArray{"string"}, + }, + }, + "required_prop": { + SwaggerSchemaProps: spec.SwaggerSchemaProps{}, + SchemaProps: spec.SchemaProps{ + Type: spec.StringOrArray{"string"}, + }, + }, + }, + }, + }, + expectedOutput: true, + }, + { + name: "request schema and response schema are equal (though the properties are not in the same order)", + requestSchema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Required: []string{"required_prop"}, + Properties: map[string]spec.Schema{ + "required_prop": { // changing order here on purpose to see if it makes any difference + SwaggerSchemaProps: spec.SwaggerSchemaProps{}, + SchemaProps: spec.SchemaProps{ + Type: spec.StringOrArray{"string"}, + }, + }, + "id": { + SwaggerSchemaProps: spec.SwaggerSchemaProps{ + ReadOnly: true, + }, + SchemaProps: spec.SchemaProps{ + Type: spec.StringOrArray{"string"}, + }, + }, + }, + }, + }, + responseSchema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Required: []string{"required_prop"}, + Properties: map[string]spec.Schema{ + "id": { + SwaggerSchemaProps: spec.SwaggerSchemaProps{ + ReadOnly: true, + }, + SchemaProps: spec.SchemaProps{ + Type: spec.StringOrArray{"string"}, + }, + }, + "required_prop": { + SwaggerSchemaProps: spec.SwaggerSchemaProps{}, + SchemaProps: spec.SchemaProps{ + Type: spec.StringOrArray{"string"}, + }, + }, + }, + }, + }, + expectedOutput: true, + }, + { + name: "request schema and response schema are NOT equal (request schema contains required props while response schema does not)", + requestSchema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Required: []string{"required_prop"}, + Properties: map[string]spec.Schema{ + "id": { + SwaggerSchemaProps: spec.SwaggerSchemaProps{ + ReadOnly: true, + }, + SchemaProps: spec.SchemaProps{ + Type: spec.StringOrArray{"string"}, + }, + }, + "required_prop": { + SwaggerSchemaProps: spec.SwaggerSchemaProps{}, + SchemaProps: spec.SchemaProps{ + Type: spec.StringOrArray{"string"}, + }, + }, + }, + }, + }, + responseSchema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Properties: map[string]spec.Schema{ + "id": { + SwaggerSchemaProps: spec.SwaggerSchemaProps{ + ReadOnly: true, + }, + SchemaProps: spec.SchemaProps{ + Type: spec.StringOrArray{"string"}, + }, + }, + "required_prop": { + SwaggerSchemaProps: spec.SwaggerSchemaProps{}, + SchemaProps: spec.SchemaProps{ + Type: spec.StringOrArray{"string"}, + }, + }, + }, + }, + }, + expectedOutput: false, + }, + { + name: "request schema and response schema are NOT equal (they are completely different)", + requestSchema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Properties: map[string]spec.Schema{ + "some_property": { + SwaggerSchemaProps: spec.SwaggerSchemaProps{ + ReadOnly: true, + }, + SchemaProps: spec.SchemaProps{ + Type: spec.StringOrArray{"string"}, + }, + }, + }, + }, + }, + responseSchema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Properties: map[string]spec.Schema{ + "some_other_property": { + SwaggerSchemaProps: spec.SwaggerSchemaProps{}, + SchemaProps: spec.SchemaProps{ + Type: spec.StringOrArray{"string"}, + }, + }, + }, + }, + }, + expectedOutput: false, + }, + { + name: "request schema and response schema are NOT equal (request schema contains properties with extensions and response schema does not)", + requestSchema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Required: []string{"required_prop"}, + Properties: map[string]spec.Schema{ + "id": { + SwaggerSchemaProps: spec.SwaggerSchemaProps{ + ReadOnly: true, + }, + SchemaProps: spec.SchemaProps{ + Type: spec.StringOrArray{"string"}, + }, + }, + "required_prop": { + SwaggerSchemaProps: spec.SwaggerSchemaProps{}, + SchemaProps: spec.SchemaProps{ + Type: spec.StringOrArray{"string"}, + }, + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-terraform-field-name": "required_prop_preferred_name", + }, + }, + }, + }, + }, + }, + responseSchema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Required: []string{"required_prop"}, + Properties: map[string]spec.Schema{ + "id": { + SwaggerSchemaProps: spec.SwaggerSchemaProps{ + ReadOnly: true, + }, + SchemaProps: spec.SchemaProps{ + Type: spec.StringOrArray{"string"}, + }, + }, + "required_prop": { + SwaggerSchemaProps: spec.SwaggerSchemaProps{}, + SchemaProps: spec.SchemaProps{ + Type: spec.StringOrArray{"string"}, + }, + }, + }, + }, + }, + expectedOutput: false, + }, + } + + for _, tc := range testCases { + specV2Analyser := specV2Analyser{} + isEqual := specV2Analyser.schemaIsEqual(tc.requestSchema, tc.responseSchema) + assert.Equal(t, tc.expectedOutput, isEqual, tc.name) + } +} + func Test_getSuccessfulResponseDefinition(t *testing.T) { testCases := []struct { name string @@ -507,6 +1461,8 @@ func TestNewSpecAnalyserV2(t *testing.T) { var swaggerJSON = createSwaggerWithExternalRef(externalRefFile.Name()) + log.Println(swaggerJSON) + swaggerFile := initAPISpecFile(swaggerJSON) defer os.Remove(swaggerFile.Name()) Convey("When newSpecAnalyserV2 method is called", func() { @@ -1080,44 +2036,38 @@ func TestResourceInstanceEndPoint(t *testing.T) { Convey("Given an specV2Analyser", t, func() { a := specV2Analyser{} Convey("When isResourceInstanceEndPoint method is called with a valid resource path such as '/resource/{id}'", func() { - resourceInstance, err := a.isResourceInstanceEndPoint("/resource/{id}") + resourceInstance := a.isResourceInstanceEndPoint("/resource/{id}") Convey("And the value returned should be true", func() { - So(err, ShouldBeNil) So(resourceInstance, ShouldBeTrue) }) }) Convey("When isResourceInstanceEndPoint method is called with a long path such as '/very/long/path/{id}'", func() { - resourceInstance, err := a.isResourceInstanceEndPoint("/very/long/path/{id}") + resourceInstance := a.isResourceInstanceEndPoint("/very/long/path/{id}") Convey("And the value returned should be true", func() { - So(err, ShouldBeNil) So(resourceInstance, ShouldBeTrue) }) }) Convey("When isResourceInstanceEndPoint method is called with a path that has path parameters '/resource/{name}/subresource/{id}'", func() { - resourceInstance, err := a.isResourceInstanceEndPoint("/resource/{name}/subresource/{id}") + resourceInstance := a.isResourceInstanceEndPoint("/resource/{name}/subresource/{id}") Convey("And the value returned should be true", func() { - So(err, ShouldBeNil) So(resourceInstance, ShouldBeTrue) }) }) Convey("When isResourceInstanceEndPoint method is called with a path that has path parameters and ends with trailing slash '/resource/{name}/subresource/{id}/'", func() { - resourceInstance, err := a.isResourceInstanceEndPoint("/resource/{name}/subresource/{id}/") + resourceInstance := a.isResourceInstanceEndPoint("/resource/{name}/subresource/{id}/") Convey("And the value returned should be true", func() { - So(err, ShouldBeNil) So(resourceInstance, ShouldBeTrue) }) }) Convey("When isResourceInstanceEndPoint method is called with a path that is a root path of a subresource '/resource/{name}/subresource'", func() { - resourceInstance, err := a.isResourceInstanceEndPoint("/resource/{name}/subresource") + resourceInstance := a.isResourceInstanceEndPoint("/resource/{name}/subresource") Convey("And the value returned should be false since it's the sub-resource root endpoint", func() { - So(err, ShouldBeNil) So(resourceInstance, ShouldBeFalse) }) }) Convey("When isResourceInstanceEndPoint method is called with an invalid resource path such as '/resource/not/instance/path' not conforming with the expected pattern '/resource/{id}'", func() { - resourceInstance, err := a.isResourceInstanceEndPoint("/resource/not/valid/instance/path") + resourceInstance := a.isResourceInstanceEndPoint("/resource/not/valid/instance/path") Convey("And the value returned should be false", func() { - So(err, ShouldBeNil) So(resourceInstance, ShouldBeFalse) }) }) @@ -1408,6 +2358,18 @@ definitions: }) }) }) + + Convey("Given an apiSpecAnalyser", t, func() { + swaggerContent := `swagger: "2.0"` + a := initAPISpecAnalyser(swaggerContent) + Convey("When findMatchingResourceRootPath method is called with a non resource instance path", func() { + resourceRootPath, err := a.findMatchingResourceRootPath("/users") + Convey("Then the error returned should match the expected one", func() { + So(err.Error(), ShouldEqual, "resource instance path '/users' missing valid resource root path, more than two results returned from match '[]'") + So(resourceRootPath, ShouldEqual, "") + }) + }) + }) } func TestPostIsPresent(t *testing.T) { @@ -1571,27 +2533,141 @@ func TestValidateResourceSchemaDefWithOptions(t *testing.T) { err := a.validateResourceSchemaDefWithOptions(schema, false) Convey("Then error returned should be nil", func() { So(err, ShouldBeNil) - }) - }) - Convey("When validateResourceSchemaDefWithOptions method is called with shouldReadyOnlyProps set to true and contains not just read only props", func() { - schema := &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Properties: map[string]spec.Schema{ - "id": {SwaggerSchemaProps: spec.SwaggerSchemaProps{ReadOnly: true}}, - "name": {SwaggerSchemaProps: spec.SwaggerSchemaProps{ReadOnly: false}}, - }, - }, - } - err := a.validateResourceSchemaDefWithOptions(schema, true) - Convey("Then error returned should be as expected", func() { - So(err.Error(), ShouldEqual, "resource schema contains properties that are not just read only") + }) + }) + Convey("When validateResourceSchemaDefWithOptions method is called with shouldReadyOnlyProps set to true and contains not just read only props", func() { + schema := &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Properties: map[string]spec.Schema{ + "id": {SwaggerSchemaProps: spec.SwaggerSchemaProps{ReadOnly: true}}, + "name": {SwaggerSchemaProps: spec.SwaggerSchemaProps{ReadOnly: false}}, + }, + }, + } + err := a.validateResourceSchemaDefWithOptions(schema, true) + Convey("Then error returned should be as expected", func() { + So(err.Error(), ShouldEqual, "resource schema contains properties that are not just read only") + }) + }) + }) +} + +func TestValidateRootPath(t *testing.T) { + Convey("Given an specV2Analyser with a terraform compliant root path (and the schema has already been expanded)", t, func() { + swaggerContent := `swagger: "2.0" +paths: + /users: + post: + parameters: + - in: "body" + name: "body" + schema: + type: "object" + required: + - name + properties: + id: + type: "string" + readOnly: true + name: + type: "string" + responses: + 201: + schema: + $ref: "#/definitions/Users" + /users/{id}: + get: + parameters: + - name: "id" + in: "path" + description: "The cdn id that needs to be fetched." + required: true + type: "string" + responses: + 200: + schema: + $ref: "#/definitions/Users" +definitions: + Users: + type: "object" + required: + - name + properties: + id: + type: "string" + readOnly: true + name: + type: "string"` + a := initAPISpecAnalyser(swaggerContent) + Convey("When validateRootPath method is called with '/users/{id}'", func() { + resourceRootPath, _, resourceRootPostSchemaDef, err := a.validateRootPath("/users/{id}") + Convey("Then the result returned should be the expected one", func() { + So(err, ShouldBeNil) + So(resourceRootPath, ShouldContainSubstring, "/users") + So(resourceRootPostSchemaDef.Properties, ShouldContainKey, "id") + So(resourceRootPostSchemaDef.Properties, ShouldContainKey, "name") + }) + }) + }) + + Convey("Given an specV2Analyser with a terraform compliant root with a POST's request payload model without the id property and the returned payload and the GET operation have it", t, func() { + swaggerContent := `swagger: "2.0" +paths: + /users: + post: + parameters: + - in: "body" + name: "body" + schema: + $ref: "#/definitions/UsersInputPayload" + responses: + 201: + schema: + $ref: "#/definitions/UsersOutputPayload" + /users/{id}: + get: + parameters: + - name: "id" + in: "path" + required: true + type: "string" + responses: + 200: + schema: + $ref: "#/definitions/UsersOutputPayload" +definitions: + UsersInputPayload: # only used in POST request payload + type: "object" + required: + - label + properties: + label: + type: "string" + UsersOutputPayload: # used in both POST response payload and GET response payload (must return any input field plus all computed) + type: "object" + properties: + id: + type: "string" + readOnly: true + label: + type: "string" + readOnly: true` + a := initAPISpecAnalyser(swaggerContent) + Convey("When validateRootPath method is called with '/users/{id}'", func() { + resourceRootPath, _, resourceRootPostSchemaDef, err := a.validateRootPath("/users/{id}") + Convey("Then the result returned should be the expected one", func() { + So(err, ShouldBeNil) + So(resourceRootPath, ShouldContainSubstring, "/users") + So(resourceRootPostSchemaDef.Properties, ShouldContainKey, "id") + So(resourceRootPostSchemaDef.Properties["id"].ReadOnly, ShouldBeTrue) + So(resourceRootPostSchemaDef.Properties, ShouldContainKey, "label") + So(resourceRootPostSchemaDef.Required, ShouldResemble, []string{"label"}) + So(resourceRootPostSchemaDef.Properties["label"].ReadOnly, ShouldBeFalse) }) }) }) -} -func TestValidateRootPath(t *testing.T) { - Convey("Given an specV2Analyser with a terraform compliant root path (and the schema has already been expanded)", t, func() { + Convey("Given an specV2Analyser with a terraform compliant root with a POST's request payload model with some computed properties and the response payload containing both the expected input props (required/optional) as well as any other computed property part of the response payload", t, func() { swaggerContent := `swagger: "2.0" paths: /users: @@ -1600,42 +2676,42 @@ paths: - in: "body" name: "body" schema: - type: "object" - required: - - name - properties: - id: - type: "string" - readOnly: true - name: - type: "string" + $ref: "#/definitions/UsersInputPayload" responses: 201: schema: - $ref: "#/definitions/Users" + $ref: "#/definitions/UsersOutputPayload" /users/{id}: get: parameters: - name: "id" in: "path" - description: "The cdn id that needs to be fetched." required: true type: "string" responses: 200: schema: - $ref: "#/definitions/Users" + $ref: "#/definitions/UsersOutputPayload" definitions: - Users: + UsersInputPayload: # only used in POST request payload, readOnly properties will be ignored type: "object" required: - - name + - label properties: id: type: "string" readOnly: true - name: - type: "string"` + label: + type: "string" + UsersOutputPayload: # used in both POST response payload and GET response payload (must return any input field plus any other computed that may be autogenerated) + type: "object" + properties: + id: + type: "string" + readOnly: true + label: + type: "string" + readOnly: true` a := initAPISpecAnalyser(swaggerContent) Convey("When validateRootPath method is called with '/users/{id}'", func() { resourceRootPath, _, resourceRootPostSchemaDef, err := a.validateRootPath("/users/{id}") @@ -1643,7 +2719,10 @@ definitions: So(err, ShouldBeNil) So(resourceRootPath, ShouldContainSubstring, "/users") So(resourceRootPostSchemaDef.Properties, ShouldContainKey, "id") - So(resourceRootPostSchemaDef.Properties, ShouldContainKey, "name") + So(resourceRootPostSchemaDef.Properties["id"].ReadOnly, ShouldBeTrue) + So(resourceRootPostSchemaDef.Properties, ShouldContainKey, "label") + So(resourceRootPostSchemaDef.Required, ShouldResemble, []string{"label"}) + So(resourceRootPostSchemaDef.Properties["label"].ReadOnly, ShouldBeFalse) }) }) }) @@ -1691,7 +2770,108 @@ definitions: }) }) - Convey("Given an specV2Analyser with a terraform compliant root path that does not contain a body parameters and the response 201 schema is empty", t, func() { + Convey("Given an specV2Analyser with a non terraform compliant root with a POST's request payload model and the response payload containing less properties than the request schema (it's expected that the response contains the request properties as well as any other computed property)", t, func() { + swaggerContent := `swagger: "2.0" +paths: + /users: + post: + parameters: + - in: "body" + name: "body" + schema: + $ref: "#/definitions/UsersInputPayload" + responses: + 201: + schema: + $ref: "#/definitions/UsersOutputPayload" + /users/{id}: + get: + parameters: + - name: "id" + in: "path" + required: true + type: "string" + responses: + 200: + schema: + $ref: "#/definitions/UsersOutputPayload" +definitions: + UsersInputPayload: + type: "object" + required: + - label + properties: + id: + type: "string" + readOnly: true + label: + type: "string" + UsersOutputPayload: + type: "object" + properties: + id: + type: "string" + readOnly: true` + a := initAPISpecAnalyser(swaggerContent) + Convey("When validateRootPath method is called with '/users/{id}'", func() { + _, _, _, err := a.validateRootPath("/users/{id}") + Convey("Then the error returned should not be nil", func() { + So(err.Error(), ShouldEqual, "resource root path '/users' POST operation does not meet any of the supported use cases") + }) + }) + }) + + Convey("Given an specV2Analyser with a non terraform compliant root POST operation that is missing a successful response definition", t, func() { + swaggerContent := `swagger: "2.0" +paths: + /users: + post: + parameters: + - in: "body" + name: "body" + schema: + $ref: "#/definitions/UsersInputPayload" + responses: + 201: + schema: + /users/{id}: + get: + parameters: + - name: "id" + in: "path" + required: true + type: "string" + responses: + 200: + schema: + $ref: "#/definitions/UsersOutputPayload" +definitions: + UsersInputPayload: + type: "object" + required: + - label + properties: + label: + type: "string" + UsersOutputPayload: + type: "object" + properties: + id: + type: "string" + readOnly: true + label: + type: "string" + readOnly: true` + a := initAPISpecAnalyser(swaggerContent) + Convey("When validateRootPath method is called with '/users/{id}'", func() { + _, _, _, err := a.validateRootPath("/users/{id}") + Convey("Then the error returned should not be nil", func() { + So(err.Error(), ShouldEqual, "resource root path '/users' POST operation is missing a successful response definition: operation response '201' is missing the schema definition") + }) + }) + }) + + Convey("Given an specV2Analyser with a non terraform compliant root path because it does not contain a body parameters and the response 201 schema is empty", t, func() { swaggerContent := `swagger: "2.0" paths: /deployKey: @@ -1729,6 +2909,48 @@ definitions: }) }) + Convey("Given an specV2Analyser with a non terraform compliant root path because it does contain a body parameters but it's missing the schema", t, func() { + swaggerContent := `swagger: "2.0" +paths: + /deployKey: + post: + parameters: + - in: "body" + name: "body" + schema: + responses: + 201: + schema: # the schema is missing + /deployKey/{id}: + get: + parameters: + - name: "id" + in: "path" + required: true + type: "string" + responses: + 200: + schema: + $ref: "#/definitions/DeployKey" +definitions: + DeployKey: + type: "object" + properties: + id: + type: "string" + readOnly: true + deploy_key: + type: "string" + readOnly: true` + a := initAPISpecAnalyser(swaggerContent) + Convey("When validateRootPath method is called with '/deployKey/{id}'", func() { + _, _, _, err := a.validateRootPath("/deployKey/{id}") + Convey("Then the error returned should not be nil", func() { + So(err.Error(), ShouldEqual, "resource root path '/deployKey' POST operation validation error: resource root operation missing the schema for the POST operation body parameter") + }) + }) + }) + Convey("Given an apiSpecAnalyser with a resource instance path such as '/users/{id}' that is missing the root path", t, func() { swaggerContent := `swagger: "2.0" paths: @@ -2107,34 +3329,86 @@ paths: - in: "body" name: "body" schema: - $ref: "#/definitions/Users" + $ref: "#/definitions/Users" + responses: + 201: + schema: + $ref: "#/definitions/Users" + /users/{id}: + get: + parameters: + - name: "id" + in: "path" + description: "The cdn id that needs to be fetched." + required: true + type: "string" + responses: + 200: + schema: + $ref: "#/definitions/Users" +definitions: + Users: + type: "object" + required: + - name + properties: + id: + type: "string" + readOnly: true + name: + type: "string"` + a := initAPISpecAnalyser(swaggerContent) + Convey("When isEndPointFullyTerraformResourceCompliant method is called ", func() { + resourceRootPath, _, _, err := a.isEndPointFullyTerraformResourceCompliant("/users/{id}") + Convey("Then the result returned should be the expected one", func() { + So(err, ShouldBeNil) + So(resourceRootPath, ShouldEqual, "/users") + }) + }) + }) + + Convey("Given an specV2Analyser with a fully terraform compliant resource Users with a POST's request payload model without the id property and the returned payload and the GET operation have it", t, func() { + swaggerContent := `swagger: "2.0" +paths: + /users: + post: + parameters: + - in: "body" + name: "body" + schema: + $ref: "#/definitions/UsersInputPayload" responses: 201: schema: - $ref: "#/definitions/Users" + $ref: "#/definitions/UsersOutputPayload" /users/{id}: get: parameters: - name: "id" in: "path" - description: "The cdn id that needs to be fetched." required: true type: "string" responses: 200: schema: - $ref: "#/definitions/Users" + $ref: "#/definitions/UsersOutputPayload" definitions: - Users: + UsersInputPayload: # only used in POST request payload type: "object" required: - - name + - label + properties: + label: + type: "string" + UsersOutputPayload: # used in both POST response payload and GET response payload (must return any input field plus all computed) + type: "object" properties: id: type: "string" readOnly: true - name: - type: "string"` + label: + type: "string" + readOnly: true` a := initAPISpecAnalyser(swaggerContent) Convey("When isEndPointFullyTerraformResourceCompliant method is called ", func() { resourceRootPath, _, _, err := a.isEndPointFullyTerraformResourceCompliant("/users/{id}") @@ -2326,6 +3600,50 @@ definitions: }) }) }) + + Convey("Given an specV2Analyser with a resource that fails the schema validation (schema does not contain an id property)", t, func() { + swaggerContent := `swagger: "2.0" +paths: + /users: + post: + parameters: + - in: "body" + name: "body" + schema: + $ref: "#/definitions/Users" + responses: + 201: + schema: + $ref: "#/definitions/Users" + /users/{id}: + get: + parameters: + - name: "id" + in: "path" + description: "The cdn id that needs to be fetched." + required: true + type: "string" + responses: + 200: + schema: + $ref: "#/definitions/Users" +definitions: + Users: + type: "object" + required: + - name + properties: + name: + type: "string"` + a := initAPISpecAnalyser(swaggerContent) + Convey("When isEndPointFullyTerraformResourceCompliant method is called ", func() { + _, _, _, err := a.isEndPointFullyTerraformResourceCompliant("/users/{id}") + Convey("Then the error returned should NOT be nil", func() { + So(err, ShouldNotBeNil) + So(err.Error(), ShouldContainSubstring, "resource schema is missing a property that uniquely identifies the resource, either a property named 'id' or a property with the extension 'x-terraform-id' set to true") + }) + }) + }) } func getExpectedResource(terraformCompliantResources []SpecResource, expectedResourceName string) SpecResource { @@ -2640,54 +3958,54 @@ definitions: func TestGetTerraformCompliantResources(t *testing.T) { Convey("Given an specV2Analyser loaded with a swagger file containing a compliant terraform subresource /v1/cdns/{id}/v1/firewalls but missing the parent resource resource description", t, func() { swaggerContent := `swagger: "2.0" -host: 127.0.0.1 +host: 127.0.0.1 paths: - ###################### - ## CDN sub-resource - ###################### + ###################### + ## CDN sub-resource + ###################### - /v1/cdns/{parent_id}/v1/firewalls: - post: - parameters: - - name: "parent_id" - in: "path" - required: true - type: "string" - - in: "body" - name: "body" - required: true - schema: - $ref: "#/definitions/ContentDeliveryNetworkFirewallV1" - responses: - 201: - schema: - $ref: "#/definitions/ContentDeliveryNetworkFirewallV1" - /v1/cdns/{parent_id}/v1/firewalls/{id}: - get: - parameters: - - name: "parent_id" - in: "path" - required: true - type: "string" - - name: "id" - in: "path" - required: true - type: "string" - responses: - 200: - schema: - $ref: "#/definitions/ContentDeliveryNetworkFirewallV1" + /v1/cdns/{parent_id}/v1/firewalls: + post: + parameters: + - name: "parent_id" + in: "path" + required: true + type: "string" + - in: "body" + name: "body" + required: true + schema: + $ref: "#/definitions/ContentDeliveryNetworkFirewallV1" + responses: + 201: + schema: + $ref: "#/definitions/ContentDeliveryNetworkFirewallV1" + /v1/cdns/{parent_id}/v1/firewalls/{id}: + get: + parameters: + - name: "parent_id" + in: "path" + required: true + type: "string" + - name: "id" + in: "path" + required: true + type: "string" + responses: + 200: + schema: + $ref: "#/definitions/ContentDeliveryNetworkFirewallV1" definitions: - ContentDeliveryNetworkFirewallV1: - type: "object" - properties: - id: - type: "string" - readOnly: true - label: - type: "string"` + ContentDeliveryNetworkFirewallV1: + type: "object" + properties: + id: + type: "string" + readOnly: true + label: + type: "string"` a := initAPISpecAnalyser(swaggerContent) Convey("When GetTerraformCompliantResources method is called ", func() { @@ -2702,196 +4020,196 @@ definitions: Convey("Given an specV2Analyser loaded with a swagger file containing a resource where the name can not be computed", t, func() { swaggerContent := `swagger: "2.0" paths: - /^&: - post: - parameters: - - in: "body" - name: "body" - required: true - schema: - $ref: "#/definitions/ContentDeliveryNetworkFirewallV1" - responses: - 201: - schema: - $ref: "#/definitions/ContentDeliveryNetworkFirewallV1" - /^&/{id}: - get: - parameters: - - name: "id" - in: "path" - required: true - type: "string" - responses: - 200: - schema: - $ref: "#/definitions/ContentDeliveryNetworkFirewallV1" -definitions: - ContentDeliveryNetworkFirewallV1: - type: "object" - properties: - id: - type: "string" - readOnly: true - label: - type: "string"` - - a := initAPISpecAnalyser(swaggerContent) - Convey("When GetTerraformCompliantResources method is called ", func() { - terraformCompliantResources, err := a.GetTerraformCompliantResources() - Convey("Then the list of resources returned should be empty since the subresource is not considered compliant if the parent is missing", func() { - So(err, ShouldBeNil) - So(terraformCompliantResources, ShouldBeEmpty) - }) - }) - }) - - Convey("Given an specV2Analyser loaded with a swagger file containing a compliant terraform parent resource /v1/cdns that uses a preferred resource name and a terraform compatible subresource /v1/cdns/{id}/v1/firewalls", t, func() { - swaggerContent := `swagger: "2.0" -host: 127.0.0.1 -paths: - - ###################### - ## CDN parent resource - ###################### - - /v1/cdns: - post: - x-terraform-resource-name: "cdn" - parameters: - - in: "body" - name: "body" - schema: - $ref: "#/definitions/ContentDeliveryNetworkV1" - responses: - 201: - schema: - $ref: "#/definitions/ContentDeliveryNetworkV1" - /v1/cdns/{cdn_id}: - get: - parameters: - - name: "cdn_id" - in: "path" - description: "The cdn id that needs to be fetched." - required: true - type: "string" - responses: - 200: - schema: - $ref: "#/definitions/ContentDeliveryNetworkV1" - - ###################### - ## CDN sub-resource - ###################### - - /v1/cdns/{cdn_id}/v1/firewalls: - get: - summary: List cdns firewalls - parameters: - - name: cdn_id - in: path - required: true - type: string - responses: - '200': - description: OK + /^&: + post: + parameters: + - in: "body" + name: "body" + required: true + schema: + $ref: "#/definitions/ContentDeliveryNetworkFirewallV1" + responses: + 201: schema: - $ref: '#/definitions/ContentDeliveryNetworkFirewallV1Collection' - post: - x-terraform-resource-host: 178.168.3.4 - parameters: - - name: "cdn_id" - in: "path" - description: "The cdn id that contains the firewall to be fetched." - required: true - type: "string" - - in: "body" - name: "body" - description: "Created CDN firewall" - required: true - schema: - $ref: "#/definitions/ContentDeliveryNetworkFirewallV1" - responses: - 201: - schema: - $ref: "#/definitions/ContentDeliveryNetworkFirewallV1" - /v1/cdns/{cdn_id}/v1/firewalls/{id}: - get: - parameters: - - name: "cdn_id" - in: "path" - description: "The cdn id that contains the firewall to be fetched." - required: true - type: "string" - - name: "id" - in: "path" - description: "The cdn firewall id that needs to be fetched." - required: true - type: "string" - responses: - 200: - schema: - $ref: "#/definitions/ContentDeliveryNetworkFirewallV1" - delete: - parameters: - - description: "The cdn id that contains the firewall to be fetched." - in: path - name: parent_id - required: true - type: string - - description: "The cdn firewall id that needs to be fetched." - in: path - name: id - required: true - type: string - responses: - 204: - put: - x-terraform-resource-timeout: "300s" - parameters: - - name: "id" - in: "path" - description: "firewall that needs to be updated" - required: true - type: "string" - - name: "parent_id" - in: "path" - description: "cdn which this firewall belongs to" - required: true - type: "string" - - in: "body" - name: "body" - description: "Updated firewall object" - required: true + $ref: "#/definitions/ContentDeliveryNetworkFirewallV1" + /^&/{id}: + get: + parameters: + - name: "id" + in: "path" + required: true + type: "string" + responses: + 200: + schema: + $ref: "#/definitions/ContentDeliveryNetworkFirewallV1" +definitions: + ContentDeliveryNetworkFirewallV1: + type: "object" + properties: + id: + type: "string" + readOnly: true + label: + type: "string"` + + a := initAPISpecAnalyser(swaggerContent) + Convey("When GetTerraformCompliantResources method is called ", func() { + terraformCompliantResources, err := a.GetTerraformCompliantResources() + Convey("Then the list of resources returned should be empty since the subresource is not considered compliant if the parent is missing", func() { + So(err, ShouldBeNil) + So(terraformCompliantResources, ShouldBeEmpty) + }) + }) + }) + + Convey("Given an specV2Analyser loaded with a swagger file containing a compliant terraform parent resource /v1/cdns that uses a preferred resource name and a terraform compatible subresource /v1/cdns/{id}/v1/firewalls", t, func() { + swaggerContent := `swagger: "2.0" +host: 127.0.0.1 +paths: + + ###################### + ## CDN parent resource + ###################### + + /v1/cdns: + post: + x-terraform-resource-name: "cdn" + parameters: + - in: "body" + name: "body" + schema: + $ref: "#/definitions/ContentDeliveryNetworkV1" + responses: + 201: + schema: + $ref: "#/definitions/ContentDeliveryNetworkV1" + /v1/cdns/{cdn_id}: + get: + parameters: + - name: "cdn_id" + in: "path" + description: "The cdn id that needs to be fetched." + required: true + type: "string" + responses: + 200: + schema: + $ref: "#/definitions/ContentDeliveryNetworkV1" + + ###################### + ## CDN sub-resource + ###################### + + /v1/cdns/{cdn_id}/v1/firewalls: + get: + summary: List cdns firewalls + parameters: + - name: cdn_id + in: path + required: true + type: string + responses: + '200': + description: OK schema: - $ref: "#/definitions/ContentDeliveryNetworkFirewallV1" - responses: - 200: - schema: - $ref: "#/definitions/ContentDeliveryNetworkFirewallV1" + $ref: '#/definitions/ContentDeliveryNetworkFirewallV1Collection' + post: + x-terraform-resource-host: 178.168.3.4 + parameters: + - name: "cdn_id" + in: "path" + description: "The cdn id that contains the firewall to be fetched." + required: true + type: "string" + - in: "body" + name: "body" + description: "Created CDN firewall" + required: true + schema: + $ref: "#/definitions/ContentDeliveryNetworkFirewallV1" + responses: + 201: + schema: + $ref: "#/definitions/ContentDeliveryNetworkFirewallV1" + /v1/cdns/{cdn_id}/v1/firewalls/{id}: + get: + parameters: + - name: "cdn_id" + in: "path" + description: "The cdn id that contains the firewall to be fetched." + required: true + type: "string" + - name: "id" + in: "path" + description: "The cdn firewall id that needs to be fetched." + required: true + type: "string" + responses: + 200: + schema: + $ref: "#/definitions/ContentDeliveryNetworkFirewallV1" + delete: + parameters: + - description: "The cdn id that contains the firewall to be fetched." + in: path + name: parent_id + required: true + type: string + - description: "The cdn firewall id that needs to be fetched." + in: path + name: id + required: true + type: string + responses: + 204: + put: + x-terraform-resource-timeout: "300s" + parameters: + - name: "id" + in: "path" + description: "firewall that needs to be updated" + required: true + type: "string" + - name: "parent_id" + in: "path" + description: "cdn which this firewall belongs to" + required: true + type: "string" + - in: "body" + name: "body" + description: "Updated firewall object" + required: true + schema: + $ref: "#/definitions/ContentDeliveryNetworkFirewallV1" + responses: + 200: + schema: + $ref: "#/definitions/ContentDeliveryNetworkFirewallV1" definitions: - ContentDeliveryNetworkFirewallV1Collection: - type: array - items: - $ref: '#/definitions/ContentDeliveryNetworkFirewallV1' - ContentDeliveryNetworkFirewallV1: - type: "object" - properties: - id: - type: "string" - readOnly: true - label: - type: "string" - ContentDeliveryNetworkV1: - type: "object" - required: - - label - properties: - id: - type: "string" - readOnly: true - label: - type: "string"` + ContentDeliveryNetworkFirewallV1Collection: + type: array + items: + $ref: '#/definitions/ContentDeliveryNetworkFirewallV1' + ContentDeliveryNetworkFirewallV1: + type: "object" + properties: + id: + type: "string" + readOnly: true + label: + type: "string" + ContentDeliveryNetworkV1: + type: "object" + required: + - label + properties: + id: + type: "string" + readOnly: true + label: + type: "string"` a := initAPISpecAnalyser(swaggerContent) Convey("When GetTerraformCompliantResources method is called ", func() { terraformCompliantResources, err := a.GetTerraformCompliantResources() @@ -2949,37 +4267,37 @@ definitions: swaggerContent := `swagger: "2.0" x-terraform-resource-regions-keyword: "sea1" paths: - /v1/cdns: - post: - x-terraform-resource-host: some.subdomain.${keyword}.domain.com - parameters: - - in: "body" - name: "body" - schema: - $ref: "#/definitions/ContentDeliveryNetwork" - responses: - 201: - schema: - $ref: "#/definitions/ContentDeliveryNetwork" - /v1/cdns/{id}: - get: - parameters: - - name: "id" - in: "path" - description: "The cdn id that needs to be fetched." - required: true - type: "string" - responses: - 200: - schema: - $ref: "#/definitions/ContentDeliveryNetwork" + /v1/cdns: + post: + x-terraform-resource-host: some.subdomain.${keyword}.domain.com + parameters: + - in: "body" + name: "body" + schema: + $ref: "#/definitions/ContentDeliveryNetwork" + responses: + 201: + schema: + $ref: "#/definitions/ContentDeliveryNetwork" + /v1/cdns/{id}: + get: + parameters: + - name: "id" + in: "path" + description: "The cdn id that needs to be fetched." + required: true + type: "string" + responses: + 200: + schema: + $ref: "#/definitions/ContentDeliveryNetwork" definitions: - ContentDeliveryNetwork: - type: "object" - properties: - id: - type: "string" - readOnly: true` + ContentDeliveryNetwork: + type: "object" + properties: + id: + type: "string" + readOnly: true` a := initAPISpecAnalyser(swaggerContent) Convey("When GetTerraformCompliantResources method is called ", func() { terraformCompliantResources, err := a.GetTerraformCompliantResources() @@ -3019,22 +4337,20 @@ definitions: }) }) - Convey("Given an specV2Analyser loaded with a swagger file containing a compliant terraform resource /v1/cdns and some non compliant paths", t, func() { + Convey("Given an specV2Analyser loaded with a swagger file containing a compliant terraform resource /v1/cdns with a POST's request payload model without the id property and the returned payload and the GET operation have it'", t, func() { swaggerContent := `swagger: "2.0" paths: /v1/cdns: post: - x-terraform-resource-timeout: "5s" - x-terraform-resource-host: some-host.com parameters: - in: "body" name: "body" schema: - $ref: "#/definitions/ContentDeliveryNetwork" + $ref: "#/definitions/ContentDeliveryNetworkInputV1" responses: 201: schema: - $ref: "#/definitions/ContentDeliveryNetwork" + $ref: "#/definitions/ContentDeliveryNetworkOutputV1" /v1/cdns/{id}: get: parameters: @@ -3046,63 +4362,150 @@ paths: responses: 200: schema: - $ref: "#/definitions/ContentDeliveryNetwork" - put: - parameters: - - name: "id" - in: "path" + $ref: "#/definitions/ContentDeliveryNetworkOutputV1" +definitions: + ContentDeliveryNetworkInputV1: # only used in POST request payload + type: "object" + required: + - label + properties: + label: type: "string" - - in: "body" - name: "body" - schema: - $ref: "#/definitions/ContentDeliveryNetwork" - responses: - 200: - description: "successful operation" - schema: - $ref: "#/definitions/ContentDeliveryNetwork" - delete: - parameters: - - name: "id" - in: "path" + ContentDeliveryNetworkOutputV1: # used in both POST response payload and GET response payload (must return any input field plus all computed) + type: "object" + properties: + id: type: "string" - responses: - 204: - description: "successful operation, no content is returned" - /non/compliant: - post: # this path post operation is missing a reference to the schema definition (commented out) - parameters: - - in: "body" - name: "body" - # schema: - # $ref: "#/definitions/NonCompliant" - responses: - 201: - schema: - $ref: "#/definitions/NonCompliant" - /non/compliant/{id}: - get: - parameters: - - name: "id" - in: "path" + readOnly: true + label: type: "string" - responses: - 200: - schema: - $ref: "#/definitions/NonCompliant" + readOnly: true` + a := initAPISpecAnalyser(swaggerContent) + Convey("When GetTerraformCompliantResources method is called ", func() { + terraformCompliantResources, err := a.GetTerraformCompliantResources() + Convey("Then the result returned should be the expected one", func() { + So(err, ShouldBeNil) + // the resources info map should only contain a resource called cdns_v1 + So(len(terraformCompliantResources), ShouldEqual, 1) + So(terraformCompliantResources[0].GetResourceName(), ShouldEqual, "cdns_v1") + cndV1Resource := terraformCompliantResources[0] + // the cndV1Resource should not be considered a subresource + subRes := cndV1Resource.GetParentResourceInfo() + So(err, ShouldBeNil) + So(subRes, ShouldBeNil) + // the resource operations are attached to the resource schema (GET,POST,PUT,DELETE) as stated in the YAML + resOperation := cndV1Resource.getResourceOperations() + So(resOperation.Get.responses, ShouldContainKey, 200) + So(resOperation.Post.responses, ShouldContainKey, 201) + So(resOperation.Put, ShouldBeNil) + So(resOperation.Delete, ShouldBeNil) + // each operation exposed on the resource has a nil timeout + timeoutSpec, err := cndV1Resource.getTimeouts() + So(err, ShouldBeNil) + So(timeoutSpec.Post, ShouldBeNil) + So(timeoutSpec.Get, ShouldBeNil) + So(timeoutSpec.Put, ShouldBeNil) + So(timeoutSpec.Delete, ShouldBeNil) + // the host is correctly configured according to the swagger + host, err := cndV1Resource.getHost() + So(err, ShouldBeNil) + So(host, ShouldBeEmpty) + // the resource schema contains the one property specified in the ContentDeliveryNetwork model definition + actualResourceSchema, err := cndV1Resource.GetResourceSchema() + So(err, ShouldBeNil) + So(len(actualResourceSchema.Properties), ShouldEqual, 2) + exists, _ := assertPropertyExists(actualResourceSchema.Properties, "id") + So(exists, ShouldBeTrue) + exists, _ = assertPropertyExists(actualResourceSchema.Properties, "label") + So(exists, ShouldBeTrue) + }) + }) + }) + + Convey("Given an specV2Analyser loaded with a swagger file containing a compliant terraform resource /v1/cdns and some non compliant paths", t, func() { + swaggerContent := `swagger: "2.0" +paths: + /v1/cdns: + post: + x-terraform-resource-timeout: "5s" + x-terraform-resource-host: some-host.com + parameters: + - in: "body" + name: "body" + schema: + $ref: "#/definitions/ContentDeliveryNetwork" + responses: + 201: + schema: + $ref: "#/definitions/ContentDeliveryNetwork" + /v1/cdns/{id}: + get: + parameters: + - name: "id" + in: "path" + description: "The cdn id that needs to be fetched." + required: true + type: "string" + responses: + 200: + schema: + $ref: "#/definitions/ContentDeliveryNetwork" + put: + parameters: + - name: "id" + in: "path" + type: "string" + - in: "body" + name: "body" + schema: + $ref: "#/definitions/ContentDeliveryNetwork" + responses: + 200: + description: "successful operation" + schema: + $ref: "#/definitions/ContentDeliveryNetwork" + delete: + parameters: + - name: "id" + in: "path" + type: "string" + responses: + 204: + description: "successful operation, no content is returned" + /non/compliant: + post: # this path post operation is missing a reference to the schema definition (commented out) + parameters: + - in: "body" + name: "body" + # schema: + # $ref: "#/definitions/NonCompliant" + responses: + 201: + schema: + $ref: "#/definitions/NonCompliant" + /non/compliant/{id}: + get: + parameters: + - name: "id" + in: "path" + type: "string" + responses: + 200: + schema: + $ref: "#/definitions/NonCompliant" definitions: - ContentDeliveryNetwork: - type: "object" - properties: - id: - type: "string" - readOnly: true - NonCompliant: - type: "object" - properties: - id: - type: "string" - readOnly: true` + ContentDeliveryNetwork: + type: "object" + properties: + id: + type: "string" + readOnly: true + NonCompliant: + type: "object" + properties: + id: + type: "string" + readOnly: true` a := initAPISpecAnalyser(swaggerContent) Convey("When GetTerraformCompliantResources method is called ", func() { terraformCompliantResources, err := a.GetTerraformCompliantResources() @@ -3144,40 +4547,40 @@ definitions: Convey("Given an specV2Analyser loaded with a swagger file containing a compliant terraform resource /v1/cdns that has a property being an array of strings", t, func() { swaggerContent := `swagger: "2.0" paths: - /v1/cdns: - post: - parameters: - - in: "body" - name: "body" - schema: - $ref: "#/definitions/ContentDeliveryNetwork" - responses: - 201: - schema: - $ref: "#/definitions/ContentDeliveryNetwork" - /v1/cdns/{id}: - get: - parameters: - - name: "id" - in: "path" - description: "The cdn id that needs to be fetched." - required: true - type: "string" - responses: - 200: - schema: - $ref: "#/definitions/ContentDeliveryNetwork" + /v1/cdns: + post: + parameters: + - in: "body" + name: "body" + schema: + $ref: "#/definitions/ContentDeliveryNetwork" + responses: + 201: + schema: + $ref: "#/definitions/ContentDeliveryNetwork" + /v1/cdns/{id}: + get: + parameters: + - name: "id" + in: "path" + description: "The cdn id that needs to be fetched." + required: true + type: "string" + responses: + 200: + schema: + $ref: "#/definitions/ContentDeliveryNetwork" definitions: - ContentDeliveryNetwork: - type: "object" - properties: - id: - type: "string" - readOnly: true - listeners: - type: array - items: - type: "string"` + ContentDeliveryNetwork: + type: "object" + properties: + id: + type: "string" + readOnly: true + listeners: + type: array + items: + type: "string"` a := initAPISpecAnalyser(swaggerContent) Convey("When GetTerraformCompliantResources method is called ", func() { terraformCompliantResources, err := a.GetTerraformCompliantResources() @@ -3204,47 +4607,47 @@ definitions: Convey("Given an specV2Analyser loaded with a swagger file containing a compliant terraform resource /v1/cdns that has a property being an array objects (using ref)", t, func() { swaggerContent := `swagger: "2.0" paths: - /v1/cdns: - post: - parameters: - - in: "body" - name: "body" - schema: - $ref: "#/definitions/ContentDeliveryNetwork" - responses: - 201: - schema: - $ref: "#/definitions/ContentDeliveryNetwork" - /v1/cdns/{id}: - get: - parameters: - - name: "id" - in: "path" - description: "The cdn id that needs to be fetched." - required: true - type: "string" - responses: - 200: - schema: - $ref: "#/definitions/ContentDeliveryNetwork" + /v1/cdns: + post: + parameters: + - in: "body" + name: "body" + schema: + $ref: "#/definitions/ContentDeliveryNetwork" + responses: + 201: + schema: + $ref: "#/definitions/ContentDeliveryNetwork" + /v1/cdns/{id}: + get: + parameters: + - name: "id" + in: "path" + description: "The cdn id that needs to be fetched." + required: true + type: "string" + responses: + 200: + schema: + $ref: "#/definitions/ContentDeliveryNetwork" definitions: - ContentDeliveryNetwork: - type: "object" - properties: - id: - type: "string" - readOnly: true - listeners: - type: array - items: - $ref: '#/definitions/Listener' - Listener: - type: object - required: - - protocol - properties: - protocol: - type: string` + ContentDeliveryNetwork: + type: "object" + properties: + id: + type: "string" + readOnly: true + listeners: + type: array + items: + $ref: '#/definitions/Listener' + Listener: + type: object + required: + - protocol + properties: + protocol: + type: string` a := initAPISpecAnalyser(swaggerContent) Convey("When GetTerraformCompliantResources method is called ", func() { terraformCompliantResources, err := a.GetTerraformCompliantResources() @@ -3326,45 +4729,45 @@ definitions: Convey("Given an specV2Analyser loaded with a swagger file containing a compliant terraform resource /v1/cdns that has a property being an array objects (nested configuration)", t, func() { swaggerContent := `swagger: "2.0" paths: - /v1/cdns: - post: - parameters: - - in: "body" - name: "body" - schema: - $ref: "#/definitions/ContentDeliveryNetwork" - responses: - 201: - schema: - $ref: "#/definitions/ContentDeliveryNetwork" - /v1/cdns/{id}: - get: - parameters: - - name: "id" - in: "path" - description: "The cdn id that needs to be fetched." - required: true - type: "string" - responses: - 200: - schema: - $ref: "#/definitions/ContentDeliveryNetwork" + /v1/cdns: + post: + parameters: + - in: "body" + name: "body" + schema: + $ref: "#/definitions/ContentDeliveryNetwork" + responses: + 201: + schema: + $ref: "#/definitions/ContentDeliveryNetwork" + /v1/cdns/{id}: + get: + parameters: + - name: "id" + in: "path" + description: "The cdn id that needs to be fetched." + required: true + type: "string" + responses: + 200: + schema: + $ref: "#/definitions/ContentDeliveryNetwork" definitions: - ContentDeliveryNetwork: - type: "object" - properties: - id: - type: "string" - readOnly: true - listeners: - type: array - items: - type: object - required: - - protocol - properties: - protocol: - type: string` + ContentDeliveryNetwork: + type: "object" + properties: + id: + type: "string" + readOnly: true + listeners: + type: array + items: + type: object + required: + - protocol + properties: + protocol: + type: string` a := initAPISpecAnalyser(swaggerContent) Convey("When GetTerraformCompliantResources method is called ", func() { terraformCompliantResources, err := a.GetTerraformCompliantResources() @@ -3392,30 +4795,30 @@ definitions: Convey("Given an specV2Analyser loaded with a swagger file containing a non compliant terraform resource /v1/cdns because its missing the post operation", t, func() { var swaggerJSON = ` { - "swagger":"2.0", - "paths":{ - "/v1/cdns/{id}":{ - "get":{ - "summary":"Get cdn by id" - }, - "put":{ - "summary":"Updated cdn" - }, - "delete":{ - "summary":"Delete cdn" - } - } - }, - "definitions":{ - "ContentDeliveryNetwork":{ - "type":"object", - "properties":{ - "id":{ - "type":"string" - } - } - } - } + "swagger":"2.0", + "paths":{ + "/v1/cdns/{id}":{ + "get":{ + "summary":"Get cdn by id" + }, + "put":{ + "summary":"Updated cdn" + }, + "delete":{ + "summary":"Delete cdn" + } + } + }, + "definitions":{ + "ContentDeliveryNetwork":{ + "type":"object", + "properties":{ + "id":{ + "type":"string" + } + } + } + } }` a := initAPISpecAnalyser(swaggerJSON) Convey("When GetTerraformCompliantResources method is called ", func() { @@ -3430,46 +4833,53 @@ definitions: Convey("Given an specV2Analyser loaded with a swagger file containing a compliant terraform resource that has the 'x-terraform-exclude-resource' with value true", t, func() { var swaggerJSON = ` { - "swagger":"2.0", - "paths":{ - "/v1/cdns":{ - "post":{ - "x-terraform-exclude-resource": true, - "summary":"Create cdn", - "parameters":[ - { - "in":"body", - "name":"body", - "description":"Created CDN", - "schema":{ - "$ref":"#/definitions/ContentDeliveryNetwork" + "swagger":"2.0", + "paths":{ + "/v1/cdns":{ + "post":{ + "x-terraform-exclude-resource": true, + "summary":"Create cdn", + "parameters":[ + { + "in":"body", + "name":"body", + "description":"Created CDN", + "schema":{ + "$ref":"#/definitions/ContentDeliveryNetwork" + } + } + ], + "responses": { + "201": { + "schema": { + "$ref": "#/definitions/ContentDeliveryNetwork" } } - ] - } - }, - "/v1/cdns/{id}":{ - "get":{ - "summary":"Get cdn by id" - }, - "put":{ - "summary":"Updated cdn" - }, - "delete":{ - "summary":"Delete cdn" - } - } - }, - "definitions":{ - "ContentDeliveryNetwork":{ - "type":"object", - "properties":{ - "id":{ - "type":"string" } - } - } - } + } + }, + "/v1/cdns/{id}":{ + "get":{ + "summary":"Get cdn by id" + }, + "put":{ + "summary":"Updated cdn" + }, + "delete":{ + "summary":"Delete cdn" + } + } + }, + "definitions":{ + "ContentDeliveryNetwork":{ + "type":"object", + "properties":{ + "id":{ + "type":"string" + } + } + } + } }` a := initAPISpecAnalyser(swaggerJSON) Convey("When GetTerraformCompliantResources method is called ", func() { @@ -3484,46 +4894,51 @@ definitions: Convey("Given an specV2Analyser loaded with a swagger file containing a schema ref that is empty", t, func() { var swaggerJSON = ` { - "swagger":"2.0", - "paths":{ - "/v1/cdns":{ - "post":{ - "x-terraform-exclude-resource": true, - "summary":"Create cdn", - "parameters":[ - { - "in":"body", - "name":"body", - "description":"Created CDN", - "schema":{ - "$ref":"" - } + "swagger":"2.0", + "paths":{ + "/v1/cdns":{ + "post":{ + "x-terraform-exclude-resource": true, + "summary":"Create cdn", + "parameters":[ + { + "in":"body", + "name":"body", + "description":"Created CDN", + "schema":{ + "$ref":"" + } + } + ], + "responses": { + "201": { + "schema": "#/definitions/ContentDeliveryNetwork" } - ] - } - }, - "/v1/cdns/{id}":{ - "get":{ - "summary":"Get cdn by id" - }, - "put":{ - "summary":"Updated cdn" - }, - "delete":{ - "summary":"Delete cdn" - } - } - }, - "definitions":{ - "ContentDeliveryNetwork":{ - "type":"object", - "properties":{ - "id":{ - "type":"string" } - } - } - } + } + }, + "/v1/cdns/{id}":{ + "get":{ + "summary":"Get cdn by id" + }, + "put":{ + "summary":"Updated cdn" + }, + "delete":{ + "summary":"Delete cdn" + } + } + }, + "definitions":{ + "ContentDeliveryNetwork":{ + "type":"object", + "properties":{ + "id":{ + "type":"string" + } + } + } + } }` a := initAPISpecAnalyser(swaggerJSON) Convey("When GetTerraformCompliantResources method is called ", func() { @@ -3538,47 +4953,47 @@ definitions: Convey("Given a swagger doc that exposes a resource with not valid multi region configuration (x-terraform-resource-regions-serviceProviderName is missing", t, func() { var swaggerJSON = ` { - "swagger":"2.0", - "x-terraform-resource-regions-someOtherServiceProvider": "rst, dub", - "paths":{ - "/v1/cdns":{ - "post":{ - "x-terraform-resource-host": "some.api.${serviceProviderName}.domain.com", - "summary":"Create cdn", - "parameters":[ - { - "in":"body", - "name":"body", - "description":"Created CDN", - "schema":{ - "$ref":"#/definitions/ContentDeliveryNetwork" - } - } - ] - } - }, - "/v1/cdns/{id}":{ - "get":{ - "summary":"Get cdn by id" - }, - "put":{ - "summary":"Updated cdn" - }, - "delete":{ - "summary":"Delete cdn" - } - } - }, - "definitions":{ - "ContentDeliveryNetwork":{ - "type":"object", - "properties":{ - "id":{ - "type":"string" - } - } - } - } + "swagger":"2.0", + "x-terraform-resource-regions-someOtherServiceProvider": "rst, dub", + "paths":{ + "/v1/cdns":{ + "post":{ + "x-terraform-resource-host": "some.api.${serviceProviderName}.domain.com", + "summary":"Create cdn", + "parameters":[ + { + "in":"body", + "name":"body", + "description":"Created CDN", + "schema":{ + "$ref":"#/definitions/ContentDeliveryNetwork" + } + } + ] + } + }, + "/v1/cdns/{id}":{ + "get":{ + "summary":"Get cdn by id" + }, + "put":{ + "summary":"Updated cdn" + }, + "delete":{ + "summary":"Delete cdn" + } + } + }, + "definitions":{ + "ContentDeliveryNetwork":{ + "type":"object", + "properties":{ + "id":{ + "type":"string" + } + } + } + } }` a := initAPISpecAnalyser(swaggerJSON) Convey("When GetTerraformCompliantResources method is called", func() { @@ -3592,47 +5007,47 @@ definitions: Convey("Given a swagger doc that exposes a resource with a multi region configuration but missing region", t, func() { var swaggerJSON = ` { - "swagger":"2.0", - "x-terraform-resource-regions-serviceProviderName": "", - "paths":{ - "/v1/cdns":{ - "post":{ - "x-terraform-resource-host": "some.api.${serviceProviderName}.domain.com", - "summary":"Create cdn", - "parameters":[ - { - "in":"body", - "name":"body", - "description":"Created CDN", - "schema":{ - "$ref":"#/definitions/ContentDeliveryNetwork" - } - } - ] - } - }, - "/v1/cdns/{id}":{ - "get":{ - "summary":"Get cdn by id" - }, - "put":{ - "summary":"Updated cdn" - }, - "delete":{ - "summary":"Delete cdn" - } - } - }, - "definitions":{ - "ContentDeliveryNetwork":{ - "type":"object", - "properties":{ - "id":{ - "type":"string" - } - } - } - } + "swagger":"2.0", + "x-terraform-resource-regions-serviceProviderName": "", + "paths":{ + "/v1/cdns":{ + "post":{ + "x-terraform-resource-host": "some.api.${serviceProviderName}.domain.com", + "summary":"Create cdn", + "parameters":[ + { + "in":"body", + "name":"body", + "description":"Created CDN", + "schema":{ + "$ref":"#/definitions/ContentDeliveryNetwork" + } + } + ] + } + }, + "/v1/cdns/{id}":{ + "get":{ + "summary":"Get cdn by id" + }, + "put":{ + "summary":"Updated cdn" + }, + "delete":{ + "summary":"Delete cdn" + } + } + }, + "definitions":{ + "ContentDeliveryNetwork":{ + "type":"object", + "properties":{ + "id":{ + "type":"string" + } + } + } + } }` a := initAPISpecAnalyser(swaggerJSON) Convey("When GetTerraformCompliantResources method is called", func() { @@ -3680,7 +5095,14 @@ func createSwaggerWithExternalRef(filename string) string { "$ref":"#/definitions/ContentDeliveryNetwork" } } - ] + ], + "responses": { + "201": { + "schema":{ + "$ref":"#/definitions/ContentDeliveryNetwork" + } + } + } } }, "/v1/cdns/{id}":{ diff --git a/openapi/provider_test.go b/openapi/provider_test.go index 205ba7174..5a05896d5 100644 --- a/openapi/provider_test.go +++ b/openapi/provider_test.go @@ -3,14 +3,13 @@ package openapi import ( "errors" "fmt" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "log" "net/http" "net/http/httptest" "os" "testing" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - . "github.com/smartystreets/goconvey/convey" ) @@ -277,6 +276,118 @@ definitions: }) }) + Convey("Given a local server that exposes a swagger file containing a terraform compatible resource cdn where the POST (request) model only includes input properties (required/optional) and the POST (response) model includes the request properties as readOnly and any other computed property", t, func() { + swaggerContent := `swagger: "2.0" +host: "localhost:8443" +basePath: "/api" + +schemes: +- "https" + +paths: + /v1/cdns: + post: + summary: "Create cdn" + x-terraform-resource-name: "cdn" + parameters: + - in: "body" + name: "body" + description: "Created CDN" + required: true + schema: + $ref: "#/definitions/ContentDeliveryNetworkInputV1" + responses: + 201: + schema: + $ref: "#/definitions/ContentDeliveryNetworkOutputV1" + /v1/cdns/{id}: + get: + summary: "Get cdn by id" + parameters: + - name: "id" + in: "path" + description: "The cdn id that needs to be fetched." + required: true + type: "string" + responses: + 200: + schema: + $ref: "#/definitions/ContentDeliveryNetworkOutputV1" + +definitions: + ContentDeliveryNetworkInputV1: # only used in POST request payload + type: "object" + required: + - label + properties: + label: + type: "string" + ContentDeliveryNetworkOutputV1: # used in both POST response payload and GET response payload + type: "object" + properties: + id: + type: "string" + readOnly: true + label: + type: "string" + readOnly: true` + + swaggerServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(swaggerContent)) + })) + + Convey("When CreateSchemaProviderWithConfiguration method is called", func() { + providerName := "openapi" + p := ProviderOpenAPI{ProviderName: providerName} + tfProvider, err := p.CreateSchemaProviderFromServiceConfiguration(&ServiceConfigStub{SwaggerURL: swaggerServer.URL}) + + Convey("Then the error should be nil", func() { + So(err, ShouldBeNil) + }) + Convey("And the provider returned should be configured as expected and the the error should be nil", func() { + So(tfProvider, ShouldNotBeNil) + So(tfProvider.Schema, ShouldNotBeNil) + // the provider resource map should contain the cdn resource with the expected configuration + So(tfProvider.ResourcesMap, ShouldNotBeNil) + resourceName := fmt.Sprintf("%s_cdn_v1", providerName) + So(tfProvider.ResourcesMap, ShouldContainKey, resourceName) + So(tfProvider.ResourcesMap, ShouldNotBeNil) + resourceName = fmt.Sprintf("%s_cdn_v1", providerName) + So(tfProvider.ResourcesMap, ShouldContainKey, resourceName) + So(tfProvider.ResourcesMap[resourceName].Schema, ShouldContainKey, "label") + So(tfProvider.ResourcesMap[resourceName].Schema["label"].Type, ShouldEqual, schema.TypeString) + So(tfProvider.ResourcesMap[resourceName].Schema["label"].Required, ShouldBeTrue) + So(tfProvider.ResourcesMap[resourceName].Schema["label"].Computed, ShouldBeFalse) + So(tfProvider.ResourcesMap[resourceName].CreateContext, ShouldNotBeNil) + So(tfProvider.ResourcesMap[resourceName].ReadContext, ShouldNotBeNil) + So(tfProvider.ResourcesMap[resourceName].UpdateContext, ShouldNotBeNil) + So(tfProvider.ResourcesMap[resourceName].DeleteContext, ShouldNotBeNil) + So(tfProvider.ResourcesMap[resourceName].Importer, ShouldNotBeNil) + + // the provider data source map should contain the cdn data source instance with the expected configuration + So(tfProvider.DataSourcesMap, ShouldNotBeNil) + dataSourceInstanceName := fmt.Sprintf("%s_cdn_v1_instance", providerName) + So(tfProvider.DataSourcesMap, ShouldContainKey, dataSourceInstanceName) + So(tfProvider.DataSourcesMap[dataSourceInstanceName].Schema, ShouldContainKey, "id") + So(tfProvider.DataSourcesMap[dataSourceInstanceName].Schema["id"].Type, ShouldEqual, schema.TypeString) + So(tfProvider.DataSourcesMap[dataSourceInstanceName].Schema["id"].Required, ShouldBeTrue) + So(tfProvider.DataSourcesMap[dataSourceInstanceName].Schema["id"].Computed, ShouldBeFalse) + So(tfProvider.DataSourcesMap[dataSourceInstanceName].Schema, ShouldContainKey, "label") + So(tfProvider.DataSourcesMap[dataSourceInstanceName].Schema["label"].Type, ShouldEqual, schema.TypeString) + So(tfProvider.DataSourcesMap[dataSourceInstanceName].Schema["label"].Required, ShouldBeFalse) + So(tfProvider.DataSourcesMap[dataSourceInstanceName].Schema["label"].Computed, ShouldBeTrue) + So(tfProvider.DataSourcesMap[dataSourceInstanceName].CreateContext, ShouldBeNil) + So(tfProvider.DataSourcesMap[dataSourceInstanceName].ReadContext, ShouldNotBeNil) + So(tfProvider.DataSourcesMap[dataSourceInstanceName].UpdateContext, ShouldBeNil) + So(tfProvider.DataSourcesMap[dataSourceInstanceName].DeleteContext, ShouldBeNil) + So(tfProvider.DataSourcesMap[dataSourceInstanceName].Importer, ShouldBeNil) + + // the provider configuration function should not be nil + So(tfProvider.ConfigureFunc, ShouldNotBeNil) + }) + }) + }) + Convey("Given a local server that exposes a swagger file containing a terraform compatible data source (cdn_datasource) using a preferred name defined in the root path level", t, func() { swaggerContent := `swagger: "2.0" host: "localhost:8443" diff --git a/tests/e2e/gray_box_cdns_test.go b/tests/e2e/gray_box_cdns_test.go index 48bf955f9..fbaa5793b 100644 --- a/tests/e2e/gray_box_cdns_test.go +++ b/tests/e2e/gray_box_cdns_test.go @@ -620,6 +620,132 @@ func TestAccCDN_Create_and_UpdateSubResource(t *testing.T) { }) } +func TestAccCDN_POSTRequestSchemaContainsInputsAndResponseSchemaContainsOutputs(t *testing.T) { + expectedID := "some_id" + expectedLabel := "my_label" + swagger := `swagger: "2.0" +host: %s +schemes: +- "http" + +paths: + ###################### + #### CDN Resource #### + ###################### + + /v1/cdns: + x-terraform-resource-name: "cdn" + post: + summary: "Create cdn" + parameters: + - in: "body" + name: "body" + description: "Created CDN" + required: true + schema: + $ref: "#/definitions/ContentDeliveryNetworkInput" + responses: + 201: + description: "successful operation" + schema: + $ref: "#/definitions/ContentDeliveryNetworkOutput" + /v1/cdns/{cdn_id}: + get: + summary: "Get cdn by id" + parameters: + - name: "cdn_id" + in: "path" + description: "The cdn id that needs to be fetched." + required: true + type: "string" + responses: + 200: + description: "successful operation" + schema: + $ref: "#/definitions/ContentDeliveryNetworkOutput" + delete: + summary: "Delete cdn" + parameters: + - name: "id" + in: "path" + description: "The cdn that needs to be deleted" + required: true + type: "string" + responses: + 204: + description: "successful operation, no content is returned" +definitions: + ContentDeliveryNetworkInput: + type: "object" + required: + - label + properties: + label: + type: "string" + ContentDeliveryNetworkOutput: + type: "object" + properties: + id: + type: "string" + readOnly: true + label: + type: "string" + readOnly: true` + apiServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var responsePayload string + switch r.Method { + case http.MethodPost: + body, err := ioutil.ReadAll(r.Body) + assert.Nil(t, err) + bodyJSON := map[string]interface{}{} + err = json.Unmarshal(body, &bodyJSON) + assert.Nil(t, err) + assert.Equal(t, expectedLabel, bodyJSON["label"]) + w.WriteHeader(http.StatusCreated) + case http.MethodGet: + w.WriteHeader(http.StatusOK) + case http.MethodDelete: + w.WriteHeader(http.StatusNotFound) + return + } + responsePayload = fmt.Sprintf(`{"id": "%s", "label":"%s"}`, expectedID, expectedLabel) + w.Write([]byte(responsePayload)) + })) + apiHost := apiServer.URL[7:] + swaggerServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + swaggerReturned := fmt.Sprintf(swagger, apiHost) + w.Write([]byte(swaggerReturned)) + })) + + tfFileContents := fmt.Sprintf(`# URI /v1/cdns/ +resource "openapi_cdn_v1" "my_cdn" { + label = "%s" +}`, expectedLabel) + + p := openapi.ProviderOpenAPI{ProviderName: providerName} + provider, err := p.CreateSchemaProviderFromServiceConfiguration(&openapi.ServiceConfigStub{SwaggerURL: swaggerServer.URL}) + assert.NoError(t, err) + + resource.Test(t, resource.TestCase{ + IsUnitTest: true, + ProviderFactories: testAccProviders(provider), + PreCheck: func() { testAccPreCheck(t, swaggerServer.URL) }, + Steps: []resource.TestStep{ + { + ExpectNonEmptyPlan: false, + Config: tfFileContents, + Check: resource.ComposeTestCheckFunc( + // check resource + resource.TestCheckResourceAttr( + openAPIResourceStateCDN, "id", expectedID), + resource.TestCheckResourceAttr( + openAPIResourceStateCDN, "label", expectedLabel), + ), + }, + }, + }) +} + func TestAcc_Create_MissingRequiredParentPropertyInTFConfigurationFile(t *testing.T) { api := initAPI(t, cdnSwaggerYAMLTemplate) diff --git a/tests/integration/resource_monitors_test.go b/tests/integration/resource_monitors_test.go index c02b5faf6..230e7ee23 100644 --- a/tests/integration/resource_monitors_test.go +++ b/tests/integration/resource_monitors_test.go @@ -25,7 +25,7 @@ func init() { } func TestAccMonitor_CreateRst1(t *testing.T) { - expectedValidationError, _ := regexp.Compile(".*unable to unmarshal response body \\['invalid character '<' looking for beginning of value'\\] for request = 'POST https://some\\.api\\.rst1\\.domain\\.com/v1/monitors HTTP/1\\.1'\\. Response = '404 Not Found'.*") + expectedValidationError, _ := regexp.Compile(".*request POST https://some.api.rst1.nonexistingrandomdomain.io/v1/monitors HTTP/1.1 failed. Response Error: 'Post \"https://some.api.rst1.nonexistingrandomdomain.io/v1/monitors\": dial tcp: lookup some.api.rst1.nonexistingrandomdomain.io.*") resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, ProviderFactories: testAccProviders, @@ -41,7 +41,7 @@ func TestAccMonitor_CreateRst1(t *testing.T) { } func TestAccMonitor_CreateDub1(t *testing.T) { - expectedValidationError, _ := regexp.Compile(".*unable to unmarshal response body \\['invalid character '<' looking for beginning of value'\\] for request = 'POST https://some\\.api\\.dub1\\.domain\\.com/v1/monitors HTTP/1\\.1'\\. Response = '404 Not Found'.*") + expectedValidationError, _ := regexp.Compile(".*request POST https://some.api.dub1.nonexistingrandomdomain.io/v1/monitors HTTP/1.1 failed. Response Error: 'Post \"https://some.api.dub1.nonexistingrandomdomain.io/v1/monitors\": dial tcp: lookup some.api.dub1.nonexistingrandomdomain.io.*") resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, ProviderFactories: testAccProviders, @@ -58,7 +58,7 @@ func TestAccMonitor_CreateDub1(t *testing.T) { func TestAccMonitor_MultiRegion_CreateRst1(t *testing.T) { testCreateConfigMonitor = populateTemplateConfigurationMonitorServiceProvider("rst1") - expectedValidationError, _ := regexp.Compile(".*unable to unmarshal response body \\['invalid character '<' looking for beginning of value'\\] for request = 'POST https://some\\.api\\.rst1\\.domain\\.com/v1/multiregionmonitors HTTP/1\\.1'\\. Response = '404 Not Found'.*") + expectedValidationError, _ := regexp.Compile(".*request POST https://some.api.rst1.nonexistingrandomdomain.io/v1/multiregionmonitors HTTP/1.1 failed. Response Error: 'Post \"https://some.api.rst1.nonexistingrandomdomain.io/v1/multiregionmonitors\": dial tcp: lookup some.api.rst1.nonexistingrandomdomain.io.*") resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, ProviderFactories: testAccProviders, @@ -75,7 +75,7 @@ func TestAccMonitor_MultiRegion_CreateRst1(t *testing.T) { func TestAccMonitor_MultiRegion_CreateDub1(t *testing.T) { testCreateConfigMonitor = populateTemplateConfigurationMonitorServiceProvider("dub1") - expectedValidationError, _ := regexp.Compile(".*unable to unmarshal response body \\['invalid character '<' looking for beginning of value'\\] for request = 'POST https://some\\.api\\.dub1\\.domain\\.com/v1/multiregionmonitors HTTP/1\\.1'\\. Response = '404 Not Found'.*") + expectedValidationError, _ := regexp.Compile(".*request POST https://some.api.dub1.nonexistingrandomdomain.io/v1/multiregionmonitors HTTP/1.1 failed. Response Error: 'Post \"https://some.api.dub1.nonexistingrandomdomain.io/v1/multiregionmonitors\": dial tcp: lookup some.api.dub1.nonexistingrandomdomain.io.*") resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, ProviderFactories: testAccProviders, @@ -101,7 +101,7 @@ resource "openapi_multiregionmonitors_v1" "%s" { name = "someName" }`, providerName, openAPIResourceInstanceNameMonitor) - expectedValidationError, _ := regexp.Compile(".*unable to unmarshal response body \\['invalid character '<' looking for beginning of value'\\] for request = 'POST https://some\\.api\\.rst1\\.domain\\.com/v1/multiregionmonitors HTTP/1\\.1'\\. Response = '404 Not Found'.*") + expectedValidationError, _ := regexp.Compile(".*request POST https://some.api.rst1.nonexistingrandomdomain.io/v1/multiregionmonitors HTTP/1.1 failed. Response Error: 'Post \"https://some.api.rst1.nonexistingrandomdomain.io/v1/multiregionmonitors\": dial tcp: lookup some.api.rst1.nonexistingrandomdomain.io.*") resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, ProviderFactories: testAccProviders,