Skip to content

Commit

Permalink
Add bigtable instance IAM resources
Browse files Browse the repository at this point in the history
Added relevant docs and tests
  • Loading branch information
sandeepsukhani committed Jun 27, 2019
1 parent 80e9ebe commit 1b9523c
Show file tree
Hide file tree
Showing 12 changed files with 10,668 additions and 1 deletion.
15 changes: 14 additions & 1 deletion google/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
googleoauth "golang.org/x/oauth2/google"
appengine "google.golang.org/api/appengine/v1"
"google.golang.org/api/bigquery/v2"
"google.golang.org/api/bigtableadmin/v2"
"google.golang.org/api/cloudbilling/v1"
"google.golang.org/api/cloudbuild/v1"
"google.golang.org/api/cloudfunctions/v1"
Expand Down Expand Up @@ -172,7 +173,9 @@ type Config struct {
StorageTransferBasePath string
clientStorageTransfer *storagetransfer.Service

bigtableClientFactory *BigtableClientFactory
bigtableClientFactory *BigtableClientFactory
BigtableAdminBasePath string
clientBigtableInstances *bigtableadmin.ProjectsInstancesService
}

var defaultClientScopes = []string{
Expand Down Expand Up @@ -427,6 +430,16 @@ func (c *Config) LoadAndValidate() error {
TokenSource: tokenSource,
}

bigtableAdminBasePath := removeBasePathVersion(c.BigtableAdminBasePath)

clientBigtable, err := bigtableadmin.NewService(context, option.WithHTTPClient(client))
if err != nil {
return err
}
clientBigtable.UserAgent = userAgent
clientBigtable.BasePath = bigtableAdminBasePath
c.clientBigtableInstances = bigtableadmin.NewProjectsInstancesService(clientBigtable)

sourceRepoClientBasePath := removeBasePathVersion(c.SourceRepoBasePath)
log.Printf("[INFO] Instantiating Google Cloud Source Repo client for path %s", sourceRepoClientBasePath)
c.clientSourceRepo, err = sourcerepo.NewService(context, option.WithHTTPClient(client))
Expand Down
50 changes: 50 additions & 0 deletions google/field_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ const (
regionalLinkTemplate = "projects/%s/regions/%s/%s/%s"
regionalLinkBasePattern = "projects/(.+)/regions/(.+)/%s/(.+)"
regionalPartialLinkBasePattern = "regions/(.+)/%s/(.+)"
projectLinkTemplate = "projects/%s/%s/%s"
projectBasePattern = "projects/(.+)/%s/(.+)"
organizationLinkTemplate = "organizations/%s/%s/%s"
organizationBasePattern = "organizations/(.+)/%s/(.+)"
)
Expand Down Expand Up @@ -355,3 +357,51 @@ func getRegionFromSchema(regionSchemaField, zoneSchemaField string, d TerraformR

return "", fmt.Errorf("Cannot determine region: set in this resource, or set provider-level 'region' or 'zone'.")
}

type ProjectFieldValue struct {
Project string
Name string

resourceType string
}

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

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

// Parses a project field with the following formats:
// - projects/{my_projects}/{resource_type}/{resource_name}
func parseProjectFieldValue(resourceType, fieldValue, projectSchemaField string, d TerraformResourceData, config *Config, isEmptyValid bool) (*ProjectFieldValue, error) {
if len(fieldValue) == 0 {
if isEmptyValid {
return &ProjectFieldValue{resourceType: resourceType}, nil
}
return nil, fmt.Errorf("The project field for resource %s cannot be empty", resourceType)
}

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

resourceType: resourceType,
}, nil
}

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

return &ProjectFieldValue{
Project: project,
Name: GetResourceNameFromSelfLink(fieldValue),

resourceType: resourceType,
}, nil
}
79 changes: 79 additions & 0 deletions google/field_helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -353,3 +353,82 @@ func TestParseRegionalFieldValue(t *testing.T) {
})
}
}

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

for tn, tc := range cases {
fieldsInSchema := make(map[string]interface{})

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

d := &ResourceDataMock{
FieldsInSchema: fieldsInSchema,
}

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

if err != nil {
if !tc.ExpectedError {
t.Errorf("bad: %s, did not expect an error. Error: %s", tn, err)
}
} else {
if v.RelativeLink() != tc.ExpectedRelativeLink {
t.Errorf("bad: %s, expected relative link to be '%s' but got '%s'", tn, tc.ExpectedRelativeLink, v.RelativeLink())
}
}
}
}
119 changes: 119 additions & 0 deletions google/iam_bigtable_instance.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package google

import (
"fmt"
"google.golang.org/api/bigtableadmin/v2"

"github.com/hashicorp/errwrap"
"github.com/hashicorp/terraform/helper/schema"
"google.golang.org/api/cloudresourcemanager/v1"
)

var IamBigtableInstanceSchema = map[string]*schema.Schema{
"instance": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"project": {
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},
}

type BigtableInstanceIamUpdater struct {
project string
instance string
Config *Config
}

func NewBigtableInstanceUpdater(d *schema.ResourceData, config *Config) (ResourceIamUpdater, error) {
project, err := getProject(d, config)
if err != nil {
return nil, err
}

d.Set("project", project)

return &BigtableInstanceIamUpdater{
project: project,
instance: d.Get("instance").(string),
Config: config,
}, nil
}

func BigtableInstanceIdParseFunc(d *schema.ResourceData, config *Config) error {
fv, err := parseProjectFieldValue("instances", d.Id(), "project", d, config, false)
if err != nil {
return err
}

d.Set("project", fv.Project)
d.Set("instance", fv.Name)

// Explicitly set the id so imported resources have the same ID format as non-imported ones.
d.SetId(fv.RelativeLink())
return nil
}

func (u *BigtableInstanceIamUpdater) GetResourceIamPolicy() (*cloudresourcemanager.Policy, error) {
req := &bigtableadmin.GetIamPolicyRequest{}
p, err := u.Config.clientBigtableInstances.GetIamPolicy(u.GetResourceId(), req).Do()
if err != nil {
return nil, errwrap.Wrapf(fmt.Sprintf("Error retrieving IAM policy for %s: {{err}}", u.DescribeResource()), err)
}

cloudResourcePolicy, err := bigtableToResourceManagerPolicy(p)
if err != nil {
return nil, errwrap.Wrapf(fmt.Sprintf("Invalid IAM policy for %s: {{err}}", u.DescribeResource()), err)
}

return cloudResourcePolicy, nil
}

func (u *BigtableInstanceIamUpdater) SetResourceIamPolicy(policy *cloudresourcemanager.Policy) error {
bigtablePolicy, err := resourceManagerToBigtablePolicy(policy)
if err != nil {
return errwrap.Wrapf(fmt.Sprintf("Invalid IAM policy for %s: {{err}}", u.DescribeResource()), err)
}

req := &bigtableadmin.SetIamPolicyRequest{Policy: bigtablePolicy}
_, err = u.Config.clientBigtableInstances.SetIamPolicy(u.GetResourceId(), req).Do()
if err != nil {
return errwrap.Wrapf(fmt.Sprintf("Error setting IAM policy for %s: {{err}}", u.DescribeResource()), err)
}

return nil
}

func (u *BigtableInstanceIamUpdater) GetResourceId() string {
return fmt.Sprintf("projects/%s/instances/%s", u.project, u.instance)
}

func (u *BigtableInstanceIamUpdater) GetMutexKey() string {
return fmt.Sprintf("iam-bigtable-instance-%s-%s", u.project, u.instance)
}

func (u *BigtableInstanceIamUpdater) DescribeResource() string {
return fmt.Sprintf("Bigtable Instance %s/%s", u.project, u.instance)
}

func resourceManagerToBigtablePolicy(p *cloudresourcemanager.Policy) (*bigtableadmin.Policy, error) {
out := &bigtableadmin.Policy{}
err := Convert(p, out)
if err != nil {
return nil, errwrap.Wrapf("Cannot convert a dataproc policy to a cloudresourcemanager policy: {{err}}", err)
}
return out, nil
}

func bigtableToResourceManagerPolicy(p *bigtableadmin.Policy) (*cloudresourcemanager.Policy, error) {
out := &cloudresourcemanager.Policy{}
err := Convert(p, out)
if err != nil {
return nil, errwrap.Wrapf("Cannot convert a cloudresourcemanager policy to a dataproc policy: {{err}}", err)
}
return out, nil
}
6 changes: 6 additions & 0 deletions google/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ func Provider() terraform.ResourceProvider {
CloudFunctionsCustomEndpointEntryKey: CloudFunctionsCustomEndpointEntry,
CloudIoTCustomEndpointEntryKey: CloudIoTCustomEndpointEntry,
StorageTransferCustomEndpointEntryKey: StorageTransferCustomEndpointEntry,
BigtableAdminCustomEndpointEntryKey: BigtableAdminCustomEndpointEntry,
},

DataSourcesMap: map[string]*schema.Resource{
Expand Down Expand Up @@ -209,6 +210,9 @@ func ResourceMapWithErrors() (map[string]*schema.Resource, error) {
"google_bigquery_dataset": resourceBigQueryDataset(),
"google_bigquery_table": resourceBigQueryTable(),
"google_bigtable_instance": resourceBigtableInstance(),
"google_bigtable_instance_iam_binding": ResourceIamBindingWithImport(IamBigtableInstanceSchema, NewBigtableInstanceUpdater, BigtableInstanceIdParseFunc),
"google_bigtable_instance_iam_member": ResourceIamMemberWithImport(IamBigtableInstanceSchema, NewBigtableInstanceUpdater, BigtableInstanceIdParseFunc),
"google_bigtable_instance_iam_policy": ResourceIamPolicyWithImport(IamBigtableInstanceSchema, NewBigtableInstanceUpdater, BigtableInstanceIdParseFunc),
"google_bigtable_table": resourceBigtableTable(),
"google_billing_account_iam_binding": ResourceIamBindingWithImport(IamBillingAccountSchema, NewBillingAccountIamUpdater, BillingAccountIdParseFunc),
"google_billing_account_iam_member": ResourceIamMemberWithImport(IamBillingAccountSchema, NewBillingAccountIamUpdater, BillingAccountIdParseFunc),
Expand Down Expand Up @@ -390,6 +394,7 @@ func providerConfigure(d *schema.ResourceData) (interface{}, error) {
config.CloudFunctionsBasePath = d.Get(CloudFunctionsCustomEndpointEntryKey).(string)
config.CloudIoTBasePath = d.Get(CloudIoTCustomEndpointEntryKey).(string)
config.StorageTransferBasePath = d.Get(StorageTransferCustomEndpointEntryKey).(string)
config.BigtableAdminBasePath = d.Get(BigtableAdminCustomEndpointEntryKey).(string)

if err := config.LoadAndValidate(); err != nil {
return nil, err
Expand Down Expand Up @@ -443,6 +448,7 @@ func ConfigureBasePaths(c *Config) {
c.CloudFunctionsBasePath = CloudFunctionsDefaultBasePath
c.CloudIoTBasePath = CloudIoTDefaultBasePath
c.StorageTransferBasePath = StorageTransferDefaultBasePath
c.BigtableAdminBasePath = BigtableAdminDefaultBasePath
}

func validateCredentials(v interface{}, k string) (warnings []string, errors []error) {
Expand Down
11 changes: 11 additions & 0 deletions google/provider_handwritten_endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,17 @@ var StorageTransferCustomEndpointEntry = &schema.Schema{
}, StorageTransferDefaultBasePath),
}

var BigtableAdminDefaultBasePath = "https://bigtableadmin.googleapis.com/v2/"
var BigtableAdminCustomEndpointEntryKey = "bigtableadmin_custom_endpoint"
var BigtableAdminCustomEndpointEntry = &schema.Schema{
Type: schema.TypeString,
Optional: true,
ValidateFunc: validateCustomEndpoint,
DefaultFunc: schema.MultiEnvDefaultFunc([]string{
"GOOGLE_BIGTABLE_ADMIN_CUSTOM_ENDPOINT",
}, BigtableAdminDefaultBasePath),
}

func validateCustomEndpoint(v interface{}, k string) (ws []string, errors []error) {
re := `.*/[^/]+/$`
return validateRegexp(re)(v, k)
Expand Down
Loading

0 comments on commit 1b9523c

Please sign in to comment.