Skip to content

Commit

Permalink
Merge pull request #60 from dikhan/feature/x-terraform-resource-name
Browse files Browse the repository at this point in the history
Feature/x terraform resource name
  • Loading branch information
dikhan authored Sep 14, 2018
2 parents bb41346 + f6c8cee commit 79d7edb
Show file tree
Hide file tree
Showing 10 changed files with 314 additions and 102 deletions.
4 changes: 2 additions & 2 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
## Proposed changes

Please add as many details as possible about the change here. Does this Pull Request resolve any open issue? If so, please
make sure to link to that issue:
Please add as many details as possible about the change here. Does this Pull Request resolve any open issue?
If so, please make sure to link to that issue:

Fixes: #

Expand Down
43 changes: 43 additions & 0 deletions docs/how_to.md
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,7 @@ Extension Name | Type | Description
[x-terraform-resource-timeout](#xTerraformResourceTimeout) | string | Only available in operation level. Defines the timeout for a given operation. This value overrides the default timeout operation value which is 10 minutes.
[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
###### <a name="xTerraformExcludeResource">x-terraform-exclude-resource</a>
Expand Down Expand Up @@ -429,6 +430,48 @@ definitions:
*Note: This extension is only supported at the operation's response level.*
###### <a name="xTerraformResourceName">x-terraform-resource-name</a>
This extension enables service providers to write a preferred resource name for the terraform configuration.
````
paths:
/cdns:
post:
x-terraform-resource-name: "cdn"
````
In the example above, the resource POST operation contains the extension ``x-terraform-resource-name`` with value ``cdn``.
This value will be the name used in the terraform configuration``cdn``.
````
resource "swaggercodegen_cdn" "my_cdn" {...} # ==> 'cdn' name is used as specified by the `x-terraform-resource-name` extension
````
The preferred name only applies to the name itself, if the resource is versioned like the example below
using version path ``/v1/cdns``, the appropriate postfix including the version will be attached automatically to the resource name.
````
paths:
/v1/cdns:
post:
x-terraform-resource-name: "cdn"
````
The corresponding terraform configuration in this case will be (note the ``_v1`` after the resource name):
````
resource "swaggercodegen_cdn_v1" "my_cdn" {...} # ==> 'cdn' name is used instead of 'cdns'
````
If the ``x-terraform-resource-name`` extension is not present in the resource root POST operation, the default resource
name will be picked from the resource root POST path. In the above example ``/v1/cdns`` would translate into ``cdns_v1``
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="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 @@ -48,6 +48,7 @@ paths:

/v1/cdns:
post:
x-terraform-resource-name: "cdn"
tags:
- "cdn"
summary: "Create cdn"
Expand Down
2 changes: 1 addition & 1 deletion examples/swaggercodegen/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ provider "swaggercodegen" {
x_request_id = "request header value for POST /v1/cdns"
}

resource "swaggercodegen_cdns_v1" "my_cdn" {
resource "swaggercodegen_cdn_v1" "my_cdn" {
label = "label" ## This is an immutable property (refer to swagger file)
ips = ["127.0.0.1"] ## This is a force-new property (refer to swagger file)
hostnames = ["origin.com"]
Expand Down
50 changes: 14 additions & 36 deletions openapi/api_spec_analyser.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ import (
"github.com/go-openapi/spec"
)

const resourceVersionRegex = "(/v[0-9]*/)"
const resourceNameRegex = "((/\\w*/){\\w*})+$"
const resourceInstanceRegex = "((?:.*)){.*}"
const swaggerResourcePayloadDefinitionRegex = "(\\w+)[^//]*$"

Expand All @@ -33,16 +31,11 @@ func (asa apiSpecAnalyser) getResourcesInfo() (resourcesInfo, error) {
for resourcePath, pathItem := range asa.d.Spec().Paths.Paths {
resourceRootPath, resourceRoot, resourcePayloadSchemaDef, err := asa.isEndPointFullyTerraformResourceCompliant(resourcePath)
if err != nil {
log.Printf("[DEBUG] resource paht '%s' not terraform compliant: %s", resourcePath, err)
continue
}
resourceName, err := asa.getResourceName(resourcePath)
if err != nil {
log.Printf("[DEBUG] resource not figure out valid terraform resource name for '%s': %s", resourcePath, err)
log.Printf("[DEBUG] resource path '%s' not terraform compliant: %s", resourcePath, err)
continue
}

r := resourceInfo{
name: resourceName,
basePath: asa.d.BasePath(),
path: resourceRootPath,
host: asa.d.Spec().Host,
Expand All @@ -51,9 +44,19 @@ func (asa apiSpecAnalyser) getResourcesInfo() (resourcesInfo, error) {
createPathInfo: *resourceRoot,
pathInfo: pathItem,
}
if !r.shouldIgnoreResource() {
resources[resourceName] = r

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() {
continue
}

log.Printf("[INFO] resource info created '%s'", resourceName)
resources[resourceName] = r
}
return resources, nil
}
Expand Down Expand Up @@ -268,31 +271,6 @@ func (asa apiSpecAnalyser) isResourceInstanceEndPoint(p string) (bool, error) {
return r.MatchString(p), nil
}

// getResourceName gets the name of the resource from a path /resource/{id}
func (asa apiSpecAnalyser) getResourceName(resourcePath string) (string, error) {
nameRegex, err := regexp.Compile(resourceNameRegex)
if err != nil {
return "", fmt.Errorf("an error occurred while compiling the resourceNameRegex regex '%s': %s", resourceNameRegex, err)
}
var resourceName string
matches := nameRegex.FindStringSubmatch(resourcePath)
if len(matches) < 2 {
return "", fmt.Errorf("could not find a valid name for resource instance path '%s'", resourcePath)
}
resourceName = strings.Replace(matches[len(matches)-1], "/", "", -1)
versionRegex, err := regexp.Compile(resourceVersionRegex)
if err != nil {
return "", fmt.Errorf("an error occurred while compiling the resourceVersionRegex regex '%s': %s", resourceVersionRegex, err)
}
versionMatches := versionRegex.FindStringSubmatch(resourcePath)
if len(versionMatches) != 0 {
version := strings.Replace(versionRegex.FindStringSubmatch(resourcePath)[1], "/", "", -1)
resourceNameWithVersion := fmt.Sprintf("%s_%s", resourceName, version)
return resourceNameWithVersion, nil
}
return resourceName, nil
}

// findMatchingResourceRootPath returns the corresponding POST root and path for a given end point
// Example: Given 'resourcePath' being "/users/{username}" the result could be "/users" or "/users/" depending on
// how the POST operation (resourceRootPath) of the given resource is defined in swagger.
Expand Down
52 changes: 0 additions & 52 deletions openapi/api_spec_analyser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -518,58 +518,6 @@ paths:

}

func TestGetResourceName(t *testing.T) {
Convey("Given an apiSpecAnalyser", t, func() {
a := apiSpecAnalyser{}
Convey("When getResourceName method is called with a valid resource instance path such as '/users/{id}'", func() {
resourceName, err := a.getResourceName("/users/{id}")
Convey("Then the error returned should be nil", func() {
So(err, ShouldBeNil)
})
Convey("And the value returned should be 'users'", func() {
So(resourceName, ShouldEqual, "users")
})
})

Convey("When getResourceName method is called with an invalid resource instance path such as '/resource/not/instance/path'", func() {
_, err := a.getResourceName("/resource/not/instance/path")
Convey("Then the error returned should not be nil", func() {
So(err, ShouldNotBeNil)
})
})

Convey("When getResourceName method is called with a valid resource instance path that is versioned such as '/v1/users/{id}'", func() {
resourceName, err := a.getResourceName("/v1/users/{id}")
Convey("Then the error returned should be nil", func() {
So(err, ShouldBeNil)
})
Convey("And the value returned should be 'users_v1'", func() {
So(resourceName, ShouldEqual, "users_v1")
})
})

Convey("When getResourceName method is called with a valid resource instance long path that is versioned such as '/v1/something/users/{id}'", func() {
resourceName, err := a.getResourceName("/v1/something/users/{id}")
Convey("Then the error returned should be nil", func() {
So(err, ShouldBeNil)
})
Convey("And the value returned should still be 'users_v1'", func() {
So(resourceName, ShouldEqual, "users_v1")
})
})

Convey("When getResourceName method is called with resource instance which has path parameters '/api/v1/nodes/{name}/proxy/{path}'", func() {
resourceName, err := a.getResourceName("/api/v1/nodes/{name}/proxy/{path}")
Convey("Then the error returned should be nil", func() {
So(err, ShouldBeNil)
})
Convey("And the value returned should still be 'proxy_v1'", func() {
So(resourceName, ShouldEqual, "proxy_v1")
})
})
})
}

func TestPostIsPresent(t *testing.T) {

Convey("Given an apiSpecAnalyser with a path '/users' that has a post operation", t, func() {
Expand Down
2 changes: 2 additions & 0 deletions openapi/provider_factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/go-openapi/loads"
"github.com/go-openapi/spec"
"github.com/hashicorp/terraform/helper/schema"
"log"
)

type providerFactory struct {
Expand Down Expand Up @@ -69,6 +70,7 @@ func (p providerFactory) generateProviderFromAPISpec(apiSpecAnalyser *apiSpecAna
return nil, err
}
resourceName := p.getProviderResourceName(resourceName)
log.Printf("[INFO] open api terraform compatible resource registered with the provider '%s'", resourceName)
resourceMap[resourceName] = resource
}
provider := &schema.Provider{
Expand Down
14 changes: 7 additions & 7 deletions openapi/resource_factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ func (r resourceFactory) create(resourceLocalData *schema.ResourceData, i interf
if err != nil {
return err
}
log.Printf("[INFO] Resource '%s' ID: %s", r.resourceInfo.name, resourceLocalData.Id())
log.Printf("[INFO] Resource '%s' ID: %s", r.resourceInfo.path, resourceLocalData.Id())

err = r.handlePollingIfConfigured(&responsePayload, resourceLocalData, providerConfig, operation.Responses, res.StatusCode, schema.TimeoutCreate)
if err != nil {
Expand Down Expand Up @@ -164,7 +164,7 @@ func (r resourceFactory) update(resourceLocalData *schema.ResourceData, i interf
providerConfig := i.(providerConfig)
operation := r.resourceInfo.pathInfo.Put
if operation == nil {
return fmt.Errorf("%s resource does not support PUT opperation, check the swagger file exposed on '%s'", r.resourceInfo.name, r.resourceInfo.host)
return fmt.Errorf("%s resource does not support PUT opperation, check the swagger file exposed on '%s'", r.resourceInfo.path, r.resourceInfo.host)
}
input := r.createPayloadFromLocalStateData(resourceLocalData)
responsePayload := map[string]interface{}{}
Expand Down Expand Up @@ -204,7 +204,7 @@ func (r resourceFactory) delete(resourceLocalData *schema.ResourceData, i interf
providerConfig := i.(providerConfig)
operation := r.resourceInfo.pathInfo.Delete
if operation == nil {
return fmt.Errorf("%s resource does not support DELETE opperation, check the swagger file exposed on '%s'", r.resourceInfo.name, r.resourceInfo.host)
return fmt.Errorf("%s resource does not support DELETE opperation, check the swagger file exposed on '%s'", r.resourceInfo.path, r.resourceInfo.host)
}
resourceIDURL, err := r.resourceInfo.getResourceIDURL(resourceLocalData.Id())
if err != nil {
Expand Down Expand Up @@ -264,7 +264,7 @@ func (r resourceFactory) handlePollingIfConfigured(responsePayload *map[string]i
}

log.Printf("[DEBUG] target statuses (%s); pending statuses (%s)", targetStatuses, pendingStatuses)
log.Printf("[INFO] Waiting for resource '%s' to reach a completion status (%s)", r.resourceInfo.name, targetStatuses)
log.Printf("[INFO] Waiting for resource '%s' to reach a completion status (%s)", r.resourceInfo.path, targetStatuses)

stateConf := &resource.StateChangeConf{
Pending: pendingStatuses,
Expand Down Expand Up @@ -297,20 +297,20 @@ func (r resourceFactory) resourceStateRefreshFunc(resourceLocalData *schema.Reso
return remoteData, defaultDestroyStatus, nil
}
}
return nil, "", fmt.Errorf("error on retrieving resource '%s' (%s) when waiting: %s", r.resourceInfo.name, resourceLocalData.Id(), err)
return nil, "", fmt.Errorf("error on retrieving resource '%s' (%s) when waiting: %s", r.resourceInfo.path, resourceLocalData.Id(), err)
}

statusIdentifier, err := r.resourceInfo.getStatusIdentifier()
if err != nil {
return nil, "", fmt.Errorf("error occurred while retrieving status identifier for resource '%s' (%s): %s", r.resourceInfo.name, resourceLocalData.Id(), err)
return nil, "", fmt.Errorf("error occurred while retrieving status identifier for resource '%s' (%s): %s", r.resourceInfo.path, resourceLocalData.Id(), err)
}

value, statusIdentifierPresentInResponse := remoteData[statusIdentifier]
if !statusIdentifierPresentInResponse {
return nil, "", fmt.Errorf("response payload received from GET /%s/%s missing the status identifier field", r.resourceInfo.path, resourceLocalData.Id())
}
newStatus := value.(string)
log.Printf("[DEBUG] resource '%s' status (%s): %s", r.resourceInfo.name, resourceLocalData.Id(), newStatus)
log.Printf("[DEBUG] resource '%s' status (%s): %s", r.resourceInfo.path, resourceLocalData.Id(), newStatus)
return remoteData, newStatus, nil
}
}
Expand Down
Loading

0 comments on commit 79d7edb

Please sign in to comment.