Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add bigtable instance IAM resources #3939

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions 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 @@ -173,6 +174,13 @@ type Config struct {
clientStorageTransfer *storagetransfer.Service

bigtableClientFactory *BigtableClientFactory
BigtableAdminBasePath string
// Unlike other clients, the Bigtable Admin client doesn't use a single
// service. Instead, there are several distinct services created off
// the base service object. To imitate most other handwritten clients,
// we expose those directly instead of providing the `Service` object
// as a factory.
clientBigtableProjectsInstances *bigtableadmin.ProjectsInstancesService
}

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

bigtableAdminBasePath := removeBasePathVersion(c.BigtableAdminBasePath)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mind adding a logging message similar to other clients? This is mostly for consistency, it can also help with debugging if the value isn't being set.

log.Printf("[INFO] Instantiating Google Cloud BigtableAdmin for path %s", bigtableAdminBasePath)

clientBigtable, err := bigtableadmin.NewService(context, option.WithHTTPClient(client))
if err != nil {
return err
}
clientBigtable.UserAgent = userAgent
clientBigtable.BasePath = bigtableAdminBasePath
c.clientBigtableProjectsInstances = 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.clientBigtableProjectsInstances.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.clientBigtableProjectsInstances.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 bigtable 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 bigtable 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 = "bigtable_custom_endpoint"
var BigtableAdminCustomEndpointEntry = &schema.Schema{
Type: schema.TypeString,
Optional: true,
ValidateFunc: validateCustomEndpoint,
DefaultFunc: schema.MultiEnvDefaultFunc([]string{
"GOOGLE_BIGTABLE_CUSTOM_ENDPOINT",
}, BigtableAdminDefaultBasePath),
}

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