Skip to content

Commit

Permalink
Merge pull request #58 from dikhan/feature/override-host-resource-level
Browse files Browse the repository at this point in the history
add support for multi region resources
  • Loading branch information
dikhan authored Sep 14, 2018
2 parents 79d7edb + 5ec0362 commit b0bd04d
Show file tree
Hide file tree
Showing 8 changed files with 616 additions and 4 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,8 @@ 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 @@ -472,6 +474,82 @@ resource name.
*Note: This extension is only interpreted and handled in resource root POST operations (e,g: /v1/resource) in the
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
1 change: 1 addition & 0 deletions examples/swaggercodegen/api/resources/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ paths:
/v1/cdns:
post:
x-terraform-resource-name: "cdn"
x-terraform-resource-host: localhost:8443
tags:
- "cdn"
summary: "Create cdn"
Expand Down
29 changes: 27 additions & 2 deletions openapi/api_spec_analyser.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,17 +45,42 @@ func (asa apiSpecAnalyser) getResourcesInfo() (resourcesInfo, error) {
pathInfo: pathItem,
}

if r.shouldIgnoreResource() {
continue
}

resourceName, err := r.getResourceName()
if err != nil {
log.Printf("[DEBUG] could not figure out the resource name for '%s': %s", resourcePath, err)
continue
}

if r.shouldIgnoreResource() {

isMultiRegion, regions := r.isMultiRegionResource(asa.d.Spec().Extensions)
if isMultiRegion {
log.Printf("[INFO] resource '%s' is configured with host override AND multi region; creating reasource per region", r.name)
for regionName, regionHost := range regions {
resourceRegionName := fmt.Sprintf("%s_%s", resourceName, regionName)
regionResource := resourceInfo{}
regionResource = r
regionResource.name = resourceRegionName
regionResource.host = regionHost
log.Printf("[INFO] multi region resource: name = %s, region = %s, host = %s", regionName, resourceRegionName, regionHost)
resources[resourceRegionName] = regionResource
}
continue
}

log.Printf("[INFO] resource info created '%s'", resourceName)
hostOverride := r.getResourceOverrideHost()
// if the override host is multi region then something must be wrong with the multi region configuration, failing to let the user know so they can fix the configuration
if isMultiRegionHost, _ := r.isMultiRegionHost(hostOverride); isMultiRegionHost {
return nil, fmt.Errorf("multi region configuration for resource '%s' is wrong, please check the multi region configuration in the swagger file is right for that resource", resourceName)
}
// Fall back to override the host if value is not empty; otherwise global host will be used as usual
if hostOverride != "" {
log.Printf("[INFO] resource '%s' is configured with host override, API calls will be made against '%s' instead of '%s'", r.name, hostOverride, asa.d.Spec().Host)
r.host = hostOverride
}
resources[resourceName] = r
}
return resources, nil
Expand Down
206 changes: 204 additions & 2 deletions openapi/api_spec_analyser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package openapi

import (
"encoding/json"
"fmt"
"github.com/go-openapi/loads"
"github.com/go-openapi/spec"
. "github.com/smartystreets/goconvey/convey"
Expand Down Expand Up @@ -1054,7 +1055,9 @@ paths:

func TestGetResourcesInfo(t *testing.T) {
Convey("Given an apiSpecAnalyser loaded with a swagger file containing a compliant terraform resource /v1/cdns and some non compliant paths", t, func() {
swaggerContent := `swagger: "2.0"
expectedHost := "some.api.domain.com"
swaggerContent := fmt.Sprintf(`swagger: "2.0"
host: %s
paths:
/v1/cdns:
post:
Expand Down Expand Up @@ -1134,7 +1137,7 @@ definitions:
properties:
id:
type: "string"
readOnly: true`
readOnly: true`, expectedHost)
a := initAPISpecAnalyser(swaggerContent)
Convey("When getResourcesInfo method is called ", func() {
resourcesInfo, err := a.getResourcesInfo()
Expand All @@ -1145,6 +1148,9 @@ definitions:
So(len(resourcesInfo), ShouldEqual, 1)
So(resourcesInfo, ShouldContainKey, "cdns_v1")
})
Convey("And the resources info map only element should have global host", func() {
So(resourcesInfo["cdns_v1"].host, ShouldEqual, expectedHost)
})
})
})

Expand Down Expand Up @@ -1243,6 +1249,202 @@ definitions:
})
})
})

// Tests that if the override host is present for a given resource and it is not multi region, the host for that given resource should be the override one
Convey("Given an apiSpecAnalyser loaded with a swagger file containing a compliant terraform resource that has the 'x-terraform-resource-host' extension and therefore overrides the global host value", t, func() {
expectedHost := "some.api.domain.com"
var swaggerJSON = fmt.Sprintf(`
{
"swagger":"2.0",
"paths":{
"/v1/cdns":{
"post":{
"x-terraform-resource-host": "%s",
"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"
}
}
}
}
}`, expectedHost)
a := initAPISpecAnalyser(swaggerJSON)
Convey("When getResourcesInfo method is called ", func() {
resourceInfo, err := a.getResourcesInfo()
expectedResourceName := "cdns_v1"
Convey("Then the error returned should be nil", func() {
So(err, ShouldBeNil)
})
Convey("And the resourceInfo map should not be empty", func() {
So(resourceInfo, ShouldNotBeEmpty)
})
Convey(fmt.Sprintf("And the resourceInfo map should contain key %s", expectedResourceName), func() {
So(resourceInfo, ShouldContainKey, expectedResourceName)
})
Convey(fmt.Sprintf("And the host value for that resource should be '%s'", expectedHost), func() {
So(resourceInfo[expectedResourceName].host, ShouldEqual, expectedHost)
})
})
})

// Tests that if the override host is present for a given resource and multi region configuration is correct, the appropriate resources per region should be created
Convey("Given an apiSpecAnalyser loaded with a swagger file containing a compliant terraform resource that has the 'x-terraform-resource-host' extension with a parametrized value some.api.${serviceProviderName}.domain.com and a matching root level 'x-terraform-resource-regions-serviceProviderName' extension populated with the different API regions", t, func() {
serviceProviderName := "serviceProviderName"
expectedHost := fmt.Sprintf("some.api.${%s}.domain.com", serviceProviderName)
var swaggerJSON = fmt.Sprintf(`
{
"swagger":"2.0",
"x-terraform-resource-regions-%s": "uswest, useast",
"paths":{
"/v1/cdns":{
"post":{
"x-terraform-resource-host": "%s",
"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"
}
}
}
}
}`, serviceProviderName, expectedHost)
a := initAPISpecAnalyser(swaggerJSON)
Convey("When getResourcesInfo method is called ", func() {
resourceInfo, err := a.getResourcesInfo()
expectedResourceName1 := "cdns_v1_uswest"
expectedResourceName2 := "cdns_v1_useast"
Convey("Then the error returned should be nil", func() {
So(err, ShouldBeNil)
})
Convey("And the resourceInfo map should not be empty", func() {
So(resourceInfo, ShouldNotBeEmpty)
})
Convey(fmt.Sprintf("And the resourceInfo map should contain key %s", expectedResourceName1), func() {
So(resourceInfo, ShouldContainKey, expectedResourceName1)
})
Convey(fmt.Sprintf("And the host value for that resource should be '%s'", "some.api.uswest.domain.com"), func() {
So(resourceInfo[expectedResourceName1].host, ShouldEqual, "some.api.uswest.domain.com")
})
Convey(fmt.Sprintf("And the resourceInfo map should contain key %s", expectedResourceName2), func() {
So(resourceInfo, ShouldContainKey, expectedResourceName2)
})
Convey(fmt.Sprintf("And the host value for that resource should be '%s'", "some.api.useast.domain.com"), func() {
So(resourceInfo[expectedResourceName2].host, ShouldEqual, "some.api.useast.domain.com")
})
})
})

// Tests that if the override host is present and the value is multi region but multi region requirements are not met, an error should be expected (so the user can fix the swagger config accordingly)
Convey("Given an apiSpecAnalyser loaded with a swagger file containing a compliant terraform resource that has the 'x-terraform-resource-host' extension with a parametrized value some.api.${serviceProviderName}.domain.com and NON matching root level 'x-terraform-resource-regions-serviceProviderName' extension", t, func() {
serviceProviderName := "serviceProviderName"
expectedHost := fmt.Sprintf("some.api.${%s}.domain.com", serviceProviderName)
var swaggerJSON = fmt.Sprintf(`
{
"swagger":"2.0",
"x-terraform-resource-regions-%s": "uswest, useast",
"paths":{
"/v1/cdns":{
"post":{
"x-terraform-resource-host": "%s",
"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"
}
}
}
}
}`, "someOtherProviderName", expectedHost)
a := initAPISpecAnalyser(swaggerJSON)
Convey("When getResourcesInfo method is called ", func() {
_, err := a.getResourcesInfo()
Convey("Then the error returned should be nil", func() {
So(err, ShouldNotBeNil)
})
})
})
}

func initAPISpecAnalyser(swaggerContent string) apiSpecAnalyser {
Expand Down
Loading

0 comments on commit b0bd04d

Please sign in to comment.