From d17cda42acee4807be96452154e37a2932fc8448 Mon Sep 17 00:00:00 2001 From: dikhan Date: Fri, 31 Aug 2018 11:54:43 -0700 Subject: [PATCH] add support for x-terraform-field-status extension --- openapi/resource_info.go | 48 +++++++- openapi/resource_info_test.go | 217 +++++++++++++++++++++++++++++++++- 2 files changed, 258 insertions(+), 7 deletions(-) diff --git a/openapi/resource_info.go b/openapi/resource_info.go index ffdb4d368..4b874cdb3 100644 --- a/openapi/resource_info.go +++ b/openapi/resource_info.go @@ -16,8 +16,12 @@ const extTfForceNew = "x-terraform-force-new" const extTfSensitive = "x-terraform-sensitive" const extTfExcludeResource = "x-terraform-exclude-resource" const extTfFieldName = "x-terraform-field-name" +const extTfFieldStatus = "x-terraform-field-status" const extTfID = "x-terraform-id" +const idDefaultPropertyName = "id" +const statusDefaultPropertyName = "status" + type resourcesInfo map[string]resourceInfo // resourceInfo serves as translator between swagger definitions and terraform schemas @@ -209,7 +213,7 @@ func (r resourceInfo) getResourceIDURL(id string) (string, error) { // getResourceIdentifier returns the property name that is supposed to be used as the identifier. The resource id // is selected as follows: -// 1.If the given schema definition contains a property configured with metadata 'x-terraform-id' set to true, that property value +// 1.If the given schema definition contains a property configured with metadata 'x-terraform-id' set to true, that property // will be used to set the state ID of the resource. Additionally, the value will be used when performing GET/PUT/DELETE requests to // identify the resource in question. // 2. If none of the properties of the given schema definition contain such metadata, it is expected that the payload @@ -237,6 +241,38 @@ func (r resourceInfo) getResourceIdentifier() (string, error) { return identifierProperty, nil } +// getStatusIdentifier returns the property name that is supposed to be used as the status field. The status field +// is selected as follows: +// 1.If the given schema definition contains a property configured with metadata 'x-terraform-field-status' set to true, that property +// will be used to check the different statues for the asynchronous pooling mechanism. +// 2. If none of the properties of the given schema definition contain such metadata, it is expected that the payload +// will have a property named 'status' +// 3. If none of the above requirements is met, an error will be returned +func (r resourceInfo) getStatusIdentifier() (string, error) { + statusProperty := "" + for propertyName, property := range r.schemaDefinition.Properties { + if r.isIDProperty(propertyName) { + continue + } + if r.isStatusProperty(propertyName) { + statusProperty = propertyName + continue + } + // field with extTfFieldStatus metadata takes preference over 'status' fields as the service provider is the one acknowledging + // the fact that this field should be used as identifier of the resource + if terraformID, ok := property.Extensions.GetBool(extTfFieldStatus); ok && terraformID { + statusProperty = propertyName + break + } + } + // if the id field is missing and there isn't any properties set with extTfFieldStatus, there is not way for the resource + // to be identified and therefore an error is returned + if statusProperty == "" { + return "", fmt.Errorf("could not find any status property in the resource swagger definition. Please make sure the resource definition has either one property named 'status' or one property that contains %s metadata", extTfFieldStatus) + } + return statusProperty, nil +} + // shouldIgnoreResource checks whether the POST operation for a given resource as the 'x-terraform-exclude-resource' extension // defined with true value. If so, the resource will not be exposed to the OpenAPI Terraform provder; otherwise it will // be exposed and users will be able to manage such resource via terraform. @@ -248,5 +284,13 @@ func (r resourceInfo) shouldIgnoreResource() bool { } func (r resourceInfo) isIDProperty(propertyName string) bool { - return terraformutils.ConvertToTerraformCompliantName(propertyName) == "id" + return r.propertyNameMatchesDefaultName(propertyName, idDefaultPropertyName) +} + +func (r resourceInfo) isStatusProperty(propertyName string) bool { + return r.propertyNameMatchesDefaultName(propertyName, statusDefaultPropertyName) +} + +func (r resourceInfo) propertyNameMatchesDefaultName(propertyName, expectedPropertyName string) bool { + return terraformutils.ConvertToTerraformCompliantName(propertyName) == expectedPropertyName } diff --git a/openapi/resource_info_test.go b/openapi/resource_info_test.go index 9d572d225..94f35d1f1 100644 --- a/openapi/resource_info_test.go +++ b/openapi/resource_info_test.go @@ -997,7 +997,7 @@ func TestGetResourceIdentifier(t *testing.T) { schemaDefinition: spec.Schema{ SchemaProps: spec.SchemaProps{ Properties: map[string]spec.Schema{ - "id": { + idDefaultPropertyName: { VendorExtensible: spec.VendorExtensible{}, SchemaProps: spec.SchemaProps{ Type: []string{"string"}, @@ -1013,14 +1013,14 @@ func TestGetResourceIdentifier(t *testing.T) { So(err, ShouldBeNil) }) Convey("Then the value returned should be 'id'", func() { - So(id, ShouldEqual, "id") + So(id, ShouldEqual, idDefaultPropertyName) }) }) }) Convey("Given a swagger schema definition that DOES NOT have an 'id' property but has a property configured with x-terraform-id set to TRUE", t, func() { extensions := spec.Extensions{} - extensions.Add("x-terraform-id", true) + extensions.Add(extTfID, true) r := resourceInfo{ schemaDefinition: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -1048,7 +1048,7 @@ func TestGetResourceIdentifier(t *testing.T) { Convey("Given a swagger schema definition that HAS BOTH an 'id' property AND ALSO a property configured with x-terraform-id set to true", t, func() { extensions := spec.Extensions{} - extensions.Add("x-terraform-id", true) + extensions.Add(extTfID, true) r := resourceInfo{ schemaDefinition: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -1082,7 +1082,7 @@ func TestGetResourceIdentifier(t *testing.T) { Convey("Given a swagger schema definition that DOES NOT have an 'id' property but has a property configured with x-terraform-id set to FALSE", t, func() { extensions := spec.Extensions{} - extensions.Add("x-terraform-id", false) + extensions.Add(extTfID, false) r := resourceInfo{ schemaDefinition: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -1135,6 +1135,213 @@ func TestGetResourceIdentifier(t *testing.T) { }) } +func TestGetStatusIdentifier(t *testing.T) { + Convey("Given a swagger schema definition that has an status property", t, func() { + r := resourceInfo{ + schemaDefinition: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Properties: map[string]spec.Schema{ + statusDefaultPropertyName: { + VendorExtensible: spec.VendorExtensible{}, + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + }, + }, + }, + }, + }, + } + Convey("When getStatusIdentifier method is called", func() { + status, err := r.getStatusIdentifier() + Convey("Then the error returned should be nil", func() { + So(err, ShouldBeNil) + }) + Convey("Then the value returned should be 'status'", func() { + So(status, ShouldEqual, statusDefaultPropertyName) + }) + }) + }) + + Convey("Given a swagger schema definition that DOES NOT have an 'status' property but has a property configured with x-terraform-field-status set to TRUE", t, func() { + extensions := spec.Extensions{} + extensions.Add(extTfFieldStatus, true) + expectedStatusProperty := "some-other-property-holding-status" + r := resourceInfo{ + schemaDefinition: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Properties: map[string]spec.Schema{ + expectedStatusProperty: { + VendorExtensible: spec.VendorExtensible{Extensions: extensions}, + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + }, + }, + }, + }, + }, + } + Convey("When getStatusIdentifier method is called", func() { + id, err := r.getStatusIdentifier() + Convey("Then the error returned should be nil", func() { + So(err, ShouldBeNil) + }) + Convey("Then the value returned should be 'some-other-property-holding-status'", func() { + So(id, ShouldEqual, expectedStatusProperty) + }) + }) + }) + + Convey("Given a swagger schema definition that HAS BOTH an 'status' property AND ALSO a property configured with 'x-terraform-field-status' set to true", t, func() { + extensions := spec.Extensions{} + extensions.Add(extTfFieldStatus, true) + expectedStatusProperty := "some-other-property-holding-status" + r := resourceInfo{ + schemaDefinition: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Properties: map[string]spec.Schema{ + "status": { + VendorExtensible: spec.VendorExtensible{}, + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + }, + }, + expectedStatusProperty: { + VendorExtensible: spec.VendorExtensible{Extensions: extensions}, + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + }, + }, + }, + }, + }, + } + Convey("When getStatusIdentifier method is called", func() { + id, err := r.getStatusIdentifier() + Convey("Then the error returned should be nil", func() { + So(err, ShouldBeNil) + }) + Convey("Then the value returned should be 'some-other-property-holding-status' as it takes preference over the default 'status' property", func() { + So(id, ShouldEqual, expectedStatusProperty) + }) + }) + }) + + Convey("Given a swagger schema definition that DOES NOT have an 'status' property but has a property configured with 'x-terraform-field-status' set to FALSE", t, func() { + extensions := spec.Extensions{} + extensions.Add(extTfFieldStatus, false) + expectedStatusProperty := "some-other-property-holding-status" + r := resourceInfo{ + schemaDefinition: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Properties: map[string]spec.Schema{ + expectedStatusProperty: { + VendorExtensible: spec.VendorExtensible{Extensions: extensions}, + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + }, + }, + }, + }, + }, + } + Convey("When getStatusIdentifier method is called", func() { + _, err := r.getStatusIdentifier() + Convey("Then the error returned should not be nil", func() { + So(err, ShouldNotBeNil) + }) + }) + }) + + Convey("Given a swagger schema definition that NEITHER HAS an 'status' property NOR a property configured with 'x-terraform-field-status' set to true", t, func() { + r := resourceInfo{ + schemaDefinition: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Properties: map[string]spec.Schema{ + "prop-that-is-not-status": { + VendorExtensible: spec.VendorExtensible{}, + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + }, + }, + "prop-that-is-not-status-and-does-not-have-status-metadata-either": { + VendorExtensible: spec.VendorExtensible{}, + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + }, + }, + }, + }, + }, + } + Convey("When getStatusIdentifier method is called", func() { + _, err := r.getStatusIdentifier() + Convey("Then the error returned should NOT be nil", func() { + So(err, ShouldNotBeNil) + }) + }) + }) +} + +func TestIsIDProperty(t *testing.T) { + Convey("Given a swagger schema definition", t, func() { + r := resourceInfo{} + Convey("When isIDProperty method is called with property named 'id'", func() { + isIDProperty := r.isIDProperty("id") + Convey("Then the error returned should be nil", func() { + So(isIDProperty, ShouldBeTrue) + }) + }) + Convey("When isIDProperty method is called with property NOT named 'id'", func() { + isIDProperty := r.isIDProperty("something_not_id") + Convey("Then the error returned should be nil", func() { + So(isIDProperty, ShouldBeFalse) + }) + }) + }) +} + +func TestIsStatusProperty(t *testing.T) { + Convey("Given a swagger schema definition", t, func() { + r := resourceInfo{} + Convey("When isStatusProperty method is called with property named 'status'", func() { + isStatusProperty := r.isStatusProperty("status") + Convey("Then the error returned should be nil", func() { + So(isStatusProperty, ShouldBeTrue) + }) + }) + Convey("When isStatusProperty method is called with property NOT named 'status'", func() { + isStatusProperty := r.isStatusProperty("something_not_status") + Convey("Then the error returned should be nil", func() { + So(isStatusProperty, ShouldBeFalse) + }) + }) + }) +} + +func TestPropertyNameMatchesDefaultName(t *testing.T) { + Convey("Given a swagger schema definition", t, func() { + r := resourceInfo{} + Convey("When propertyNameMatchesDefaultName method is called with property named 'status' and an expected name matching the property property name", func() { + propertyNameMatchesDefaultName := r.propertyNameMatchesDefaultName("status", "status") + Convey("Then the error returned should be nil", func() { + So(propertyNameMatchesDefaultName, ShouldBeTrue) + }) + }) + Convey("When propertyNameMatchesDefaultName method is called with property named 'ID' which is not terraform compliant name and an expected property name", func() { + propertyNameMatchesDefaultName := r.propertyNameMatchesDefaultName("ID", "id") + Convey("Then the error returned should be nil", func() { + So(propertyNameMatchesDefaultName, ShouldBeTrue) + }) + }) + Convey("When propertyNameMatchesDefaultName method is called with property NOT matching the expected property name", func() { + propertyNameMatchesDefaultName := r.propertyNameMatchesDefaultName("something_not_status", "") + Convey("Then the error returned should be nil", func() { + So(propertyNameMatchesDefaultName, ShouldBeFalse) + }) + }) + }) +} + func TestShouldIgnoreResource(t *testing.T) { Convey("Given a terraform compliant resource that has a POST operation containing the x-terraform-exclude-resource with value true", t, func() { r := resourceInfo{