Skip to content

Commit

Permalink
forklift host override and multi-region functionality
Browse files Browse the repository at this point in the history
- update documentation
- add unit test
- add a monitor example to example swagger to be able to run int tests
- add int test (monitor) simulating a multiregion resource. No implementation
was added to exmaple backend. But at least with the errors got back from terraform
asserts can be made to confirm Terraform accepts the resource name and it's available
in the tf configuration, the right resource name is used and also that the API
call is made against the right region domain
-
  • Loading branch information
dikhan committed Oct 3, 2018
1 parent 56775c4 commit 71dbd68
Show file tree
Hide file tree
Showing 12 changed files with 733 additions and 147 deletions.
78 changes: 78 additions & 0 deletions docs/how_to.md
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,9 @@ Extension Name | Type | Description
[x-terraform-header](#xTerraformHeader) | string | Only available in operation level parameters at the moment. Defines that he given header should be passed as part of the request.
[x-terraform-resource-poll-enabled](#xTerraformResourcePollEnabled) | bool | Only supported in operation responses (e,g: 202). Defines that if the API responds with the given HTTP Status code (e,g: 202), the polling mechanism will be enabled. This allows the OpenAPI Terraform provider to perform read calls to the remote API and check the resource state. The polling mechanism finalises if the remote resource state arrives at completion, failure state or times-out (60s)
[x-terraform-resource-name](#xTerraformResourceName) | string | Only available in resource root's POST operation. Defines the name that will be used for the resource in the Terraform configuration. If the extension is not preset, default value will be the name of the resource in the path. For instance, a path such as /v1/users will translate into a terraform resource name users_v1
[x-terraform-resource-host](#xTerraformResourceHost) | string | Only supported in resource root's POST operation. Defines the host that should be used when managing this specific resource. The value of this extension effectively overrides the global host configuration, making the OpenAPI Terraform provider client make thje API calls against the host specified in this extension value instead of the global host configuration. The protocols (HTTP/HTTPS) and base path (if anything other than "/") used when performing the API calls will still come from the global configuration.
[x-terraform-resource-regions-%s](#xTerraformResourceRegions) | string | Only supported in the root level. Defines the regions supported by a given resource identified by the %s variable. This extension only works if the ```x-terraform-resource-host``` extension contains a value that is parametrized and identifies the matching ```x-terraform-resource-regions-%s``` extension. The values of this extension must be comma separated strings.
###### <a name="xTerraformExcludeResource">x-terraform-exclude-resource</a>
Expand Down Expand Up @@ -473,6 +476,81 @@ resource name.
above example*
###### <a name="xTerraformResourceHost">x-terraform-resource-host</a>
This extension allows resources to override the global host configuration with a different host. This is handy when
a given swagger file may combine resources provided by different service providers.
````
swagger: "2.0"
host: "some.domain.com"
paths:
/v1/cdns:
post:
x-terraform-resource-host: cdn.api.otherdomain.com
````
The above configuration will make the OpenAPI Terraform provider client make API CRUD requests (POST/GET/PUT/DELETE) to
the overridden host instead, in this case ```cdn.api.otherdomain.com```.
*Note: This extension is only supported at the operation's POST operation level. The other operations available for the
resource such as GET/PUT/DELETE will used the overridden host value too.*
###### <a name="xTerraformResourceRegions">Multi-region resources</a>
Additionally, if the resource is using multi region domains, meaning there's one sub-domain for each region where the resource
can be created into (similar to how aws resources are created per region), this can be configured as follows:
````
swagger: "2.0"
host: "some.domain.com"
x-terraform-resource-regions-cdn: "dub1,sea1"
paths:
/v1/cdns:
post:
x-terraform-resource-host: cdn.${cdn}.api.otherdomain.com
````
If the ``x-terraform-resource-host`` extension has a value parametrised in the form where the following pattern ```${identifier}```
is found (identifier being any string with no whitspaces - spaces,tabs, line breaks, etc) AND there is a matching
extension 'x-terraform-resource-regions-**identifier**' defined in the root level that refers to the same identifier
then the resource will be considered multi region.
For instance, in the above example, the ```x-terraform-resource-host``` value is parametrised as the ```${identifier}``` pattern
is found, and the identifier in this case is ```cdn```. Moreover, there is a matching ```x-terraform-resource-regions-cdn```
extension containing a list of regions where this resource can be created in.
The regions found in the ```x-terraform-resource-regions-cdn``` will be used as follows:
- The OpenAPI Terraform provider will expose one resource per region enlisted in the extension. In the case above, the
following resources will become available in the Terraform configuration (the provider name chosen here is 'swaggercodegen'):
````
resource "swaggercodegen_cdn_v1_dub1" "my_cdn" {
label = "label"
ips = ["127.0.0.1"]
hostnames = ["origin.com"]
}
resource "swaggercodegen_cdn_v1_sea1" "my_cdn" {
label = "label"
ips = ["127.0.0.1"]
hostnames = ["origin.com"]
}
````
As shown above, the resources that are multi-region will have extra information in their name that identifies the region
where tha resource should be managed.
- The OpenAPI Terraform provider client will make the API call against the specific resource region when the resource
is configured with multi-region support.
- As far as the resource configuration is concerned, the swagger configuration remains the same for that specific resource
(parameters, operations, polling support, etc) and the same configuration will be applicable to all the regions that resource
supports.
*Note: This extension is only supported at the root level and can be used exclusively along with the 'x-terraform-resource-host'
extension*
#### <a name="swaggerDefinitions">Definitions</a>
- **Field Name:** definitions
Expand Down
78 changes: 78 additions & 0 deletions examples/swaggercodegen/api/resources/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,16 @@ tags:
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"
Expand All @@ -26,6 +36,8 @@ produces:
security:
- apikey_auth: []

x-terraform-resource-regions-monitor: "rst1,dub1"

paths:
/swagger.json:
get:
Expand All @@ -49,6 +61,7 @@ paths:
/v1/cdns:
post:
x-terraform-resource-name: "cdn"
x-terraform-resource-host: localhost:8443
tags:
- "cdn"
summary: "Create cdn"
Expand Down Expand Up @@ -255,6 +268,60 @@ paths:
404:
$ref: "#/responses/NotFound"


############################
##### Monitors Resource ####
############################

# The monitor resource is not implemented in the backed, it only serves as an example on how the global host can be overridden
# and how the resource can be configured with multi region setup

/v1/monitors:
post:
tags:
- "monitor"
summary: "Create monitor v1"
operationId: "MonitorV1"
x-terraform-resource-host: "some.api.${monitor}.domain.com"
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"
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"
schema:
$ref: "#/definitions/MonitorV1"
default:
description: "generic error response"
schema:
$ref: "#/definitions/Error"
/v1/monitors/{id}:
get:
tags:
- "monitor"
summary: "Get monitor by id"
description: ""
operationId: "MonitorV1"
parameters:
- name: "id"
in: "path"
description: "The monitor v1 id that needs to be fetched."
required: true
type: "string"
responses:
200:
description: "successful operation"
schema:
$ref: "#/definitions/MonitorV1"
400:
description: "Invalid monitor id supplied"
404:
description: "Monitor not found"

securityDefinitions:
apikey_auth:
type: "apiKey"
Expand Down Expand Up @@ -329,6 +396,17 @@ definitions:
simulate_failure: # allows user to set it to true and force an error on the API when the given operation (POST/PUT/READ/DELETE) is being performed
type: boolean

MonitorV1:
type: "object"
required:
- name
properties:
id:
type: "string"
readOnly: true
name:
type: "string"

# Schema for error response body
Error:
type: object
Expand Down
11 changes: 11 additions & 0 deletions openapi/openapi_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,9 +116,20 @@ func (o ProviderClient) getResourceURL(resource SpecResource) (string, error) {
if err != nil {
return "", err
}

basePath := o.openAPIBackendConfiguration.getBasePath()
resourceRelativePath := resource.getResourcePath()

// Fall back to override the host if value is not empty; otherwise global host will be used as usual
hostOverride, err := resource.getHost()
if err != nil {
return "", err
}
if hostOverride != "" {
log.Printf("[INFO] resource '%s' is configured with host override, API calls will be made against '%s' instead of '%s'", resourceRelativePath, hostOverride, host)
host = hostOverride
}

if host == "" || resourceRelativePath == "" {
return "", fmt.Errorf("host and path are mandatory attributes to get the resource URL - host['%s'], path['%s']", host, resourceRelativePath)
}
Expand Down
1 change: 1 addition & 0 deletions openapi/openapi_spec_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
// SpecResource defines the behaviour related to terraform compliant OpenAPI Resources.
type SpecResource interface {
getResourceName() string
getHost() (string, error)
getResourcePath() string
getResourceSchema() (*specSchemaDefinition, error)
shouldIgnoreResource() bool
Expand Down
4 changes: 4 additions & 0 deletions openapi/openapi_stub_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,7 @@ func (s *specStubResource) getResourceOperations() specResourceOperations {
func (s *specStubResource) getTimeouts() (*specTimeouts, error) {
return s.timeouts, nil
}

func (s *specStubResource) getHost() (string, error) {
return "", nil
}
60 changes: 59 additions & 1 deletion openapi/openapi_v2_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,12 @@ const extTfResourcePollTargetStatuses = "x-terraform-resource-poll-completed-sta
const extTfResourcePollPendingStatuses = "x-terraform-resource-poll-pending-statuses"
const extTfExcludeResource = "x-terraform-exclude-resource"
const extTfResourceName = "x-terraform-resource-name"
const extTfResourceURL = "x-terraform-resource-host"

// SpecV2Resource defines a struct that implements the SpecResource interface and it's based on OpenAPI v2 specification
type SpecV2Resource struct {
Name string
Name string
Region string
// Path contains the full relative path to the resource e,g: /v1/resource
Path string
// specSchemaDefinition definition represents the representational state (aka model) of the resource
Expand All @@ -43,12 +45,18 @@ type SpecV2Resource struct {
InstancePathItem spec.PathItem
}

// newSpecV2Resource creates a SpecV2Resource with no region and default host
func newSpecV2Resource(path string, schemaDefinition spec.Schema, rootPathItem, instancePathItem spec.PathItem) (*SpecV2Resource, error) {
return newSpecV2ResourceWithRegion("", path, schemaDefinition, rootPathItem, instancePathItem)
}

func newSpecV2ResourceWithRegion(region, path string, schemaDefinition spec.Schema, rootPathItem, instancePathItem spec.PathItem) (*SpecV2Resource, error) {
if path == "" {
return nil, fmt.Errorf("path must not be empty")
}
resource := &SpecV2Resource{
Path: path,
Region: region,
SchemaDefinition: schemaDefinition,
RootPathItem: rootPathItem,
InstancePathItem: instancePathItem,
Expand All @@ -62,6 +70,9 @@ func newSpecV2Resource(path string, schemaDefinition spec.Schema, rootPathItem,
}

func (o *SpecV2Resource) getResourceName() string {
if o.Region != "" {
return fmt.Sprintf("%s_%s", o.Name, o.Region)
}
return o.Name
}

Expand Down Expand Up @@ -104,6 +115,53 @@ func (o *SpecV2Resource) getResourcePath() string {
return o.Path
}

// getHost can return an empty host in which case the expectation is that the host used will be the one specified in the
// swagger host attribute or if not present the host used will be the host where the swagger file was served
func (o *SpecV2Resource) getHost() (string, error) {
overrideHost := getResourceOverrideHost(o.RootPathItem.Post)
if overrideHost == "" {
return "", nil
}
multiRegionHost, err := getMultiRegionHost(overrideHost, o.Region)
if err != nil {
return "", nil
}
if multiRegionHost != "" {
return multiRegionHost, nil
}
return overrideHost, nil
}

func getMultiRegionHost(overrideHost string, region string) (string, error) {
isMultiRegionHost, regex := isMultiRegionHost(overrideHost)
if isMultiRegionHost {
if region == "" {
return "", fmt.Errorf("region can not be empty for multiregion resources")
}
repStr := fmt.Sprintf("${1}%s$4", region)
return regex.ReplaceAllString(overrideHost, repStr), nil
}
return "", nil
}

// getResourceOverrideHost checks if the x-terraform-resource-host extension is present and if so returns its value. This
// value will override the global host value, and the API calls for this resource will be made against the value returned
func getResourceOverrideHost(rootPathItem *spec.Operation) string {
if resourceURL, exists := rootPathItem.Extensions.GetString(extTfResourceURL); exists && resourceURL != "" {
return resourceURL
}
return ""
}

func isMultiRegionHost(overrideHost string) (bool, *regexp.Regexp) {
regex, err := regexp.Compile("(\\S+)(\\$\\{(\\S+)\\})(\\S+)")
if err != nil {
log.Printf("[DEBUG] failed to compile region identifier regex: %s", err)
return false, nil
}
return len(regex.FindStringSubmatch(overrideHost)) != 0, regex
}

func (o *SpecV2Resource) getResourceOperations() specResourceOperations {
return specResourceOperations{
Post: o.createResourceOperation(o.RootPathItem.Post),
Expand Down
Loading

0 comments on commit 71dbd68

Please sign in to comment.