Skip to content

Commit

Permalink
Add support for parsing regional fields, and subnetworks specifically
Browse files Browse the repository at this point in the history
Currently unused because subnetworks may have a separate project from that
of the instance using them, which complicates looking up the project field.
  • Loading branch information
Nic Cope committed Nov 15, 2017
1 parent fd1e34a commit c16efb8
Show file tree
Hide file tree
Showing 2 changed files with 193 additions and 7 deletions.
96 changes: 89 additions & 7 deletions google/field_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@ import (
)

const (
globalLinkTemplate = "projects/%s/global/%s/%s"
globalLinkBasePattern = "projects/(.+)/global/%s/(.+)"
zonalLinkTemplate = "projects/%s/zones/%s/%s/%s"
zonalLinkBasePattern = "projects/(.+)/zones/(.+)/%s/(.+)"
zonalPartialLinkBasePattern = "zones/(.+)/%s/(.+)"
organizationLinkTemplate = "organizations/%s/%s/%s"
organizationBasePattern = "organizations/(.+)/%s/(.+)"
globalLinkTemplate = "projects/%s/global/%s/%s"
globalLinkBasePattern = "projects/(.+)/global/%s/(.+)"
zonalLinkTemplate = "projects/%s/zones/%s/%s/%s"
zonalLinkBasePattern = "projects/(.+)/zones/(.+)/%s/(.+)"
zonalPartialLinkBasePattern = "zones/(.+)/%s/(.+)"
regionalLinkTemplate = "projects/%s/regions/%s/%s/%s"
regionalLinkBasePattern = "projects/(.+)/regions/(.+)/%s/(.+)"
regionalPartialLinkBasePattern = "regions/(.+)/%s/(.+)"
organizationLinkTemplate = "organizations/%s/%s/%s"
organizationBasePattern = "organizations/(.+)/%s/(.+)"
)

// ------------------------------------------------------------
Expand All @@ -23,6 +26,10 @@ func ParseNetworkFieldValue(network string, d TerraformResourceData, config *Con
return parseGlobalFieldValue("networks", network, "project", d, config, true)
}

func ParseSubnetworkFieldValue(subnetwork string, d TerraformResourceData, config *Config) (*RegionalFieldValue, error) {
return parseRegionalFieldValue("subnetworks", subnetwork, "project", "region", d, config, true)
}

func ParseSslCertificateFieldValue(sslCertificate string, d TerraformResourceData, config *Config) (*GlobalFieldValue, error) {
return parseGlobalFieldValue("sslCertificates", sslCertificate, "project", d, config, false)
}
Expand Down Expand Up @@ -220,3 +227,78 @@ func parseOrganizationFieldValue(resourceType, fieldValue string, isEmptyValid b

return nil, fmt.Errorf("Invalid field format. Got '%s', expected format '%s'", fieldValue, fmt.Sprintf(organizationLinkTemplate, "{org_id}", resourceType, "{name}"))
}

type RegionalFieldValue struct {
Project string
Region string
Name string

resourceType string
}

func (f RegionalFieldValue) RelativeLink() string {
if len(f.Name) == 0 {
return ""
}

return fmt.Sprintf(regionalLinkTemplate, f.Project, f.Region, f.resourceType, f.Name)
}

// Parses a regional field supporting 5 different formats:
// - https://www.googleapis.com/compute/ANY_VERSION/projects/{my_project}/regions/{region}/{resource_type}/{resource_name}
// - projects/{my_project}/regions/{region}/{resource_type}/{resource_name}
// - regions/{region}/{resource_type}/{resource_name}
// - resource_name
// - "" (empty string). RelativeLink() returns empty if isEmptyValid is true.
//
// If the project is not specified, it first tries to get the project from the `projectSchemaField` and then fallback on the default project.
// If the region is not specified, it takes the value of `regionSchemaField`.
func parseRegionalFieldValue(resourceType, fieldValue, projectSchemaField, regionSchemaField string, d TerraformResourceData, config *Config, isEmptyValid bool) (*RegionalFieldValue, error) {
if len(fieldValue) == 0 {
if isEmptyValid {
return &RegionalFieldValue{resourceType: resourceType}, nil
}
return nil, fmt.Errorf("The regional field for resource %s cannot be empty.", resourceType)
}

r := regexp.MustCompile(fmt.Sprintf(regionalLinkBasePattern, resourceType))
if parts := r.FindStringSubmatch(fieldValue); parts != nil {
return &RegionalFieldValue{
Project: parts[1],
Region: parts[2],
Name: parts[3],
resourceType: resourceType,
}, nil
}

project, err := getProjectFromSchema(projectSchemaField, d, config)
if err != nil {
return nil, err
}

r = regexp.MustCompile(fmt.Sprintf(regionalPartialLinkBasePattern, resourceType))
if parts := r.FindStringSubmatch(fieldValue); parts != nil {
return &RegionalFieldValue{
Project: project,
Region: parts[1],
Name: parts[2],
resourceType: resourceType,
}, nil
}

if len(regionSchemaField) == 0 {
return nil, fmt.Errorf("Invalid field format. Got '%s', expected format '%s'", fieldValue, fmt.Sprintf(globalLinkTemplate, "{project}", resourceType, "{name}"))
}

region, ok := d.GetOk(regionSchemaField)
if !ok {
return nil, fmt.Errorf("A region must be specified")
}

return &RegionalFieldValue{
Project: project,
Region: region.(string),
Name: GetResourceNameFromSelfLink(fieldValue),
resourceType: resourceType,
}, nil
}
104 changes: 104 additions & 0 deletions google/field_helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -227,3 +227,107 @@ func TestParseOrganizationFieldValue(t *testing.T) {
}
}
}

func TestParseRegionalFieldValue(t *testing.T) {
const resourceType = "subnetworks"
cases := map[string]struct {
FieldValue string
ExpectedRelativeLink string
ExpectedError bool
IsEmptyValid bool
ProjectSchemaField string
ProjectSchemaValue string
RegionSchemaField string
RegionSchemaValue string
Config *Config
}{
"subnetwork is a full self link": {
FieldValue: "https://www.googleapis.com/compute/v1/projects/myproject/regions/us-central1/subnetworks/my-subnetwork",
ExpectedRelativeLink: "projects/myproject/regions/us-central1/subnetworks/my-subnetwork",
},
"subnetwork is a relative self link": {
FieldValue: "projects/myproject/regions/us-central1/subnetworks/my-subnetwork",
ExpectedRelativeLink: "projects/myproject/regions/us-central1/subnetworks/my-subnetwork",
},
"subnetwork is a partial relative self link": {
FieldValue: "regions/us-central1/subnetworks/my-subnetwork",
Config: &Config{Project: "default-project"},
ExpectedRelativeLink: "projects/default-project/regions/us-central1/subnetworks/my-subnetwork",
},
"subnetwork is the name only": {
FieldValue: "my-subnetwork",
RegionSchemaField: "region",
RegionSchemaValue: "us-east1",
Config: &Config{Project: "default-project"},
ExpectedRelativeLink: "projects/default-project/regions/us-east1/subnetworks/my-subnetwork",
},
"subnetwork is the name only and has a project set in schema": {
FieldValue: "my-subnetwork",
ProjectSchemaField: "project",
ProjectSchemaValue: "schema-project",
RegionSchemaField: "region",
RegionSchemaValue: "us-east1",
Config: &Config{Project: "default-project"},
ExpectedRelativeLink: "projects/schema-project/regions/us-east1/subnetworks/my-subnetwork",
},
"subnetwork is the name only and has a project set in schema but the field is not specified.": {
FieldValue: "my-subnetwork",
ProjectSchemaValue: "schema-project",
RegionSchemaField: "region",
RegionSchemaValue: "us-east1",
Config: &Config{Project: "default-project"},
ExpectedRelativeLink: "projects/default-project/regions/us-east1/subnetworks/my-subnetwork",
},
"subnetwork is the name only and no region field is specified": {
FieldValue: "my-subnetwork",
Config: &Config{Project: "default-project"},
ExpectedError: true,
},
"subnetwork is the name only and no value for region field is specified": {
FieldValue: "my-subnetwork",
RegionSchemaField: "region",
Config: &Config{Project: "default-project"},
ExpectedError: true,
},
"subnetwork is empty and it is valid": {
FieldValue: "",
IsEmptyValid: true,
ExpectedRelativeLink: "",
},
"subnetwork is empty and it is not valid": {
FieldValue: "",
IsEmptyValid: false,
ExpectedError: true,
},
}

for tn, tc := range cases {
t.Run(tn, func(t *testing.T) {
fieldsInSchema := make(map[string]interface{})

if len(tc.ProjectSchemaValue) > 0 && len(tc.ProjectSchemaField) > 0 {
fieldsInSchema[tc.ProjectSchemaField] = tc.ProjectSchemaValue
}

if len(tc.RegionSchemaValue) > 0 && len(tc.RegionSchemaField) > 0 {
fieldsInSchema[tc.RegionSchemaField] = tc.RegionSchemaValue
}

d := &ResourceDataMock{
FieldsInSchema: fieldsInSchema,
}

v, err := parseRegionalFieldValue(resourceType, tc.FieldValue, tc.ProjectSchemaField, tc.RegionSchemaField, d, tc.Config, tc.IsEmptyValid)

if err != nil {
if !tc.ExpectedError {
t.Errorf("bad: did not expect an error. Error: %s", err)
}
} else {
if v.RelativeLink() != tc.ExpectedRelativeLink {
t.Errorf("bad: expected relative link to be '%s' but got '%s'", tc.ExpectedRelativeLink, v.RelativeLink())
}
}
})
}
}

0 comments on commit c16efb8

Please sign in to comment.