From e7f0725c33945ae616632a1fe73cca595a55830d Mon Sep 17 00:00:00 2001 From: The Magician Date: Tue, 25 Jun 2024 18:02:19 -0700 Subject: [PATCH] Add support for GCS managed folders (#10786) (#18555) [upstream:0836fbe77e64a07242c695326680a72bfe3861af] Signed-off-by: Modular Magician --- .changelog/10786.txt | 6 + google/provider/provider_mmv1_resources.go | 9 +- .../storage/iam_storage_managed_folder.go | 186 +++++ .../iam_storage_managed_folder_test.go | 653 ++++++++++++++++++ .../resource_storage_managed_folder.go | 294 ++++++++ ...e_storage_managed_folder_generated_test.go | 110 +++ .../r/storage_managed_folder.html.markdown | 129 ++++ .../storage_managed_folder_iam.html.markdown | 203 ++++++ 8 files changed, 1588 insertions(+), 2 deletions(-) create mode 100644 .changelog/10786.txt create mode 100644 google/services/storage/iam_storage_managed_folder.go create mode 100644 google/services/storage/iam_storage_managed_folder_test.go create mode 100644 google/services/storage/resource_storage_managed_folder.go create mode 100644 google/services/storage/resource_storage_managed_folder_generated_test.go create mode 100644 website/docs/r/storage_managed_folder.html.markdown create mode 100644 website/docs/r/storage_managed_folder_iam.html.markdown diff --git a/.changelog/10786.txt b/.changelog/10786.txt new file mode 100644 index 00000000000..85c5441107e --- /dev/null +++ b/.changelog/10786.txt @@ -0,0 +1,6 @@ +```release-note:new-resource +`google_storage_managed_folder` +``` +```release-note:new-resource +`google_storage_managed_folder_iam` +``` \ No newline at end of file diff --git a/google/provider/provider_mmv1_resources.go b/google/provider/provider_mmv1_resources.go index f5be713c36a..361d3af3ef5 100644 --- a/google/provider/provider_mmv1_resources.go +++ b/google/provider/provider_mmv1_resources.go @@ -406,6 +406,7 @@ var handwrittenIAMDatasources = map[string]*schema.Resource{ "google_kms_crypto_key_iam_policy": tpgiamresource.DataSourceIamPolicy(kms.IamKmsCryptoKeySchema, kms.NewKmsCryptoKeyIamUpdater), "google_spanner_instance_iam_policy": tpgiamresource.DataSourceIamPolicy(spanner.IamSpannerInstanceSchema, spanner.NewSpannerInstanceIamUpdater), "google_spanner_database_iam_policy": tpgiamresource.DataSourceIamPolicy(spanner.IamSpannerDatabaseSchema, spanner.NewSpannerDatabaseIamUpdater), + "google_storage_managed_folder_iam_policy": tpgiamresource.DataSourceIamPolicy(storage.StorageManagedFolderIamSchema, storage.StorageManagedFolderIamUpdaterProducer), "google_organization_iam_policy": tpgiamresource.DataSourceIamPolicy(resourcemanager.IamOrganizationSchema, resourcemanager.NewOrganizationIamUpdater), "google_project_iam_policy": tpgiamresource.DataSourceIamPolicy(resourcemanager.IamProjectSchema, resourcemanager.NewProjectIamUpdater), "google_pubsub_subscription_iam_policy": tpgiamresource.DataSourceIamPolicy(pubsub.IamPubsubSubscriptionSchema, pubsub.NewPubsubSubscriptionIamUpdater), @@ -414,9 +415,9 @@ var handwrittenIAMDatasources = map[string]*schema.Resource{ } // Resources -// Generated resources: 427 +// Generated resources: 428 // Generated IAM resources: 249 -// Total generated resources: 676 +// Total generated resources: 677 var generatedResources = map[string]*schema.Resource{ "google_folder_access_approval_settings": accessapproval.ResourceAccessApprovalFolderSettings(), "google_organization_access_approval_settings": accessapproval.ResourceAccessApprovalOrganizationSettings(), @@ -1033,6 +1034,7 @@ var generatedResources = map[string]*schema.Resource{ "google_storage_bucket_access_control": storage.ResourceStorageBucketAccessControl(), "google_storage_default_object_access_control": storage.ResourceStorageDefaultObjectAccessControl(), "google_storage_hmac_key": storage.ResourceStorageHmacKey(), + "google_storage_managed_folder": storage.ResourceStorageManagedFolder(), "google_storage_object_access_control": storage.ResourceStorageObjectAccessControl(), "google_storage_insights_report_config": storageinsights.ResourceStorageInsightsReportConfig(), "google_storage_transfer_agent_pool": storagetransfer.ResourceStorageTransferAgentPool(), @@ -1209,6 +1211,9 @@ var handwrittenIAMResources = map[string]*schema.Resource{ "google_spanner_database_iam_binding": tpgiamresource.ResourceIamBinding(spanner.IamSpannerDatabaseSchema, spanner.NewSpannerDatabaseIamUpdater, spanner.SpannerDatabaseIdParseFunc), "google_spanner_database_iam_member": tpgiamresource.ResourceIamMember(spanner.IamSpannerDatabaseSchema, spanner.NewSpannerDatabaseIamUpdater, spanner.SpannerDatabaseIdParseFunc), "google_spanner_database_iam_policy": tpgiamresource.ResourceIamPolicy(spanner.IamSpannerDatabaseSchema, spanner.NewSpannerDatabaseIamUpdater, spanner.SpannerDatabaseIdParseFunc), + "google_storage_managed_folder_iam_binding": tpgiamresource.ResourceIamBinding(storage.StorageManagedFolderIamSchema, storage.StorageManagedFolderIamUpdaterProducer, storage.StorageManagedFolderIdParseFunc), + "google_storage_managed_folder_iam_member": tpgiamresource.ResourceIamMember(storage.StorageManagedFolderIamSchema, storage.StorageManagedFolderIamUpdaterProducer, storage.StorageManagedFolderIdParseFunc), + "google_storage_managed_folder_iam_policy": tpgiamresource.ResourceIamPolicy(storage.StorageManagedFolderIamSchema, storage.StorageManagedFolderIamUpdaterProducer, storage.StorageManagedFolderIdParseFunc), "google_organization_iam_binding": tpgiamresource.ResourceIamBinding(resourcemanager.IamOrganizationSchema, resourcemanager.NewOrganizationIamUpdater, resourcemanager.OrgIdParseFunc), "google_organization_iam_member": tpgiamresource.ResourceIamMember(resourcemanager.IamOrganizationSchema, resourcemanager.NewOrganizationIamUpdater, resourcemanager.OrgIdParseFunc), "google_organization_iam_policy": tpgiamresource.ResourceIamPolicy(resourcemanager.IamOrganizationSchema, resourcemanager.NewOrganizationIamUpdater, resourcemanager.OrgIdParseFunc), diff --git a/google/services/storage/iam_storage_managed_folder.go b/google/services/storage/iam_storage_managed_folder.go new file mode 100644 index 00000000000..f09254363a0 --- /dev/null +++ b/google/services/storage/iam_storage_managed_folder.go @@ -0,0 +1,186 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 +package storage + +import ( + "fmt" + "net/url" + + "github.com/hashicorp/errwrap" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "google.golang.org/api/cloudresourcemanager/v1" + + "github.com/hashicorp/terraform-provider-google/google/tpgiamresource" + "github.com/hashicorp/terraform-provider-google/google/tpgresource" + transport_tpg "github.com/hashicorp/terraform-provider-google/google/transport" + "github.com/hashicorp/terraform-provider-google/google/verify" +) + +var StorageManagedFolderIamSchema = map[string]*schema.Schema{ + "bucket": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "managed_folder": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + DiffSuppressFunc: tpgresource.CompareSelfLinkOrResourceName, + ValidateFunc: verify.ValidateRegexp(`/$`), + }, +} + +type StorageManagedFolderIamUpdater struct { + bucket string + managedFolder string + d tpgresource.TerraformResourceData + Config *transport_tpg.Config +} + +func StorageManagedFolderIamUpdaterProducer(d tpgresource.TerraformResourceData, config *transport_tpg.Config) (tpgiamresource.ResourceIamUpdater, error) { + values := make(map[string]string) + + if v, ok := d.GetOk("bucket"); ok { + values["bucket"] = v.(string) + } + + if v, ok := d.GetOk("managed_folder"); ok { + values["managed_folder"] = v.(string) + } + + u := &StorageManagedFolderIamUpdater{ + bucket: values["bucket"], + managedFolder: values["managed_folder"], + d: d, + Config: config, + } + + if err := d.Set("bucket", u.bucket); err != nil { + return nil, fmt.Errorf("Error setting bucket: %s", err) + } + if err := d.Set("managed_folder", u.managedFolder); err != nil { + return nil, fmt.Errorf("Error setting managed_folder: %s", err) + } + + return u, nil +} + +func StorageManagedFolderIdParseFunc(d *schema.ResourceData, config *transport_tpg.Config) error { + values := make(map[string]string) + + m, err := tpgresource.GetImportIdQualifiers([]string{"(?P[^/]+)/managedFolders/(?P.+)", "(?P[^/]+)/(?P.+)"}, d, config, d.Id()) + if err != nil { + return err + } + + for k, v := range m { + values[k] = v + } + + u := &StorageManagedFolderIamUpdater{ + bucket: values["bucket"], + managedFolder: values["managed_folder"], + d: d, + Config: config, + } + if err := d.Set("bucket", u.bucket); err != nil { + return fmt.Errorf("Error setting bucket: %s", err) + } + if err := d.Set("managed_folder", u.managedFolder); err != nil { + return fmt.Errorf("Error setting managed_folder: %s", err) + } + d.SetId(u.GetResourceId()) + return nil +} + +func (u *StorageManagedFolderIamUpdater) GetResourceIamPolicy() (*cloudresourcemanager.Policy, error) { + url, err := u.qualifyManagedFolderUrl("iam") + if err != nil { + return nil, err + } + + var obj map[string]interface{} + url, err = transport_tpg.AddQueryParams(url, map[string]string{"optionsRequestedPolicyVersion": fmt.Sprintf("%d", tpgiamresource.IamPolicyVersion)}) + if err != nil { + return nil, err + } + + userAgent, err := tpgresource.GenerateUserAgentString(u.d, u.Config.UserAgent) + if err != nil { + return nil, err + } + + policy, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: u.Config, + Method: "GET", + RawURL: url, + UserAgent: userAgent, + Body: obj, + }) + if err != nil { + return nil, errwrap.Wrapf(fmt.Sprintf("Error retrieving IAM policy for %s: {{err}}", u.DescribeResource()), err) + } + + out := &cloudresourcemanager.Policy{} + err = tpgresource.Convert(policy, out) + if err != nil { + return nil, errwrap.Wrapf("Cannot convert a policy to a resource manager policy: {{err}}", err) + } + + return out, nil +} + +func (u *StorageManagedFolderIamUpdater) SetResourceIamPolicy(policy *cloudresourcemanager.Policy) error { + json, err := tpgresource.ConvertToMap(policy) + if err != nil { + return err + } + + obj := json + + url, err := u.qualifyManagedFolderUrl("iam") + if err != nil { + return err + } + + userAgent, err := tpgresource.GenerateUserAgentString(u.d, u.Config.UserAgent) + if err != nil { + return err + } + + _, err = transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: u.Config, + Method: "PUT", + RawURL: url, + UserAgent: userAgent, + Body: obj, + Timeout: u.d.Timeout(schema.TimeoutCreate), + }) + if err != nil { + return errwrap.Wrapf(fmt.Sprintf("Error setting IAM policy for %s: {{err}}", u.DescribeResource()), err) + } + + return nil +} + +func (u *StorageManagedFolderIamUpdater) qualifyManagedFolderUrl(methodIdentifier string) (string, error) { + urlTemplate := fmt.Sprintf("{{StorageBasePath}}b/%s/managedFolders/%s/%s", u.bucket, url.PathEscape(u.managedFolder), methodIdentifier) + url, err := tpgresource.ReplaceVars(u.d, u.Config, urlTemplate) + if err != nil { + return "", err + } + return url, nil +} + +func (u *StorageManagedFolderIamUpdater) GetResourceId() string { + return fmt.Sprintf("b/%s/managedFolders/%s", u.bucket, u.managedFolder) +} + +func (u *StorageManagedFolderIamUpdater) GetMutexKey() string { + return fmt.Sprintf("iam-storage-managedfolder-%s", u.GetResourceId()) +} + +func (u *StorageManagedFolderIamUpdater) DescribeResource() string { + return fmt.Sprintf("storage managedfolder %q", u.GetResourceId()) +} diff --git a/google/services/storage/iam_storage_managed_folder_test.go b/google/services/storage/iam_storage_managed_folder_test.go new file mode 100644 index 00000000000..5672b72b0f6 --- /dev/null +++ b/google/services/storage/iam_storage_managed_folder_test.go @@ -0,0 +1,653 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 +package storage_test + +import ( + "fmt" + "strings" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + + "github.com/hashicorp/terraform-provider-google/google/acctest" + "github.com/hashicorp/terraform-provider-google/google/envvar" + "github.com/hashicorp/terraform-provider-google/google/tpgresource" +) + +func TestAccStorageManagedFolderIamBindingGenerated(t *testing.T) { + t.Parallel() + + context := map[string]interface{}{ + "random_suffix": acctest.RandString(t, 10), + "role": "roles/storage.objectViewer", + "admin_role": "roles/storage.admin", + "condition_title": "expires_after_2019_12_31", + "condition_expr": `request.time < timestamp(\"2020-01-01T00:00:00Z\")`, + "condition_desc": "Expiring at midnight of 2019-12-31", + "condition_title_no_desc": "expires_after_2019_12_31-no-description", + "condition_expr_no_desc": `request.time < timestamp(\"2020-01-01T00:00:00Z\")`, + } + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + Steps: []resource.TestStep{ + { + Config: testAccStorageManagedFolderIamBinding_basicGenerated(context), + }, + { + ResourceName: "google_storage_managed_folder_iam_binding.foo", + ImportStateId: fmt.Sprintf("b/%s/managedFolders/managed/folder/name/ roles/storage.objectViewer", fmt.Sprintf("tf-test-my-bucket%s", context["random_suffix"])), + ImportState: true, + ImportStateVerify: true, + }, + { + // Test Iam Binding update + Config: testAccStorageManagedFolderIamBinding_updateGenerated(context), + }, + { + ResourceName: "google_storage_managed_folder_iam_binding.foo", + ImportStateId: fmt.Sprintf("b/%s/managedFolders/managed/folder/name/ roles/storage.objectViewer", fmt.Sprintf("tf-test-my-bucket%s", context["random_suffix"])), + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccStorageManagedFolderIamMemberGenerated(t *testing.T) { + t.Parallel() + + context := map[string]interface{}{ + "random_suffix": acctest.RandString(t, 10), + "role": "roles/storage.objectViewer", + "admin_role": "roles/storage.admin", + "condition_title": "expires_after_2019_12_31", + "condition_expr": `request.time < timestamp(\"2020-01-01T00:00:00Z\")`, + "condition_desc": "Expiring at midnight of 2019-12-31", + "condition_title_no_desc": "expires_after_2019_12_31-no-description", + "condition_expr_no_desc": `request.time < timestamp(\"2020-01-01T00:00:00Z\")`, + } + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + Steps: []resource.TestStep{ + { + // Test Iam Member creation (no update for member, no need to test) + Config: testAccStorageManagedFolderIamMember_basicGenerated(context), + }, + { + ResourceName: "google_storage_managed_folder_iam_member.foo", + ImportStateId: fmt.Sprintf("b/%s/managedFolders/managed/folder/name/ roles/storage.objectViewer user:admin@hashicorptest.com", fmt.Sprintf("tf-test-my-bucket%s", context["random_suffix"])), + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccStorageManagedFolderIamPolicyGenerated(t *testing.T) { + t.Parallel() + + // This may skip test, so do it first + sa := envvar.GetTestServiceAccountFromEnv(t) + context := map[string]interface{}{ + "random_suffix": acctest.RandString(t, 10), + "role": "roles/storage.objectViewer", + "admin_role": "roles/storage.admin", + "condition_title": "expires_after_2019_12_31", + "condition_expr": `request.time < timestamp(\"2020-01-01T00:00:00Z\")`, + "condition_desc": "Expiring at midnight of 2019-12-31", + "condition_title_no_desc": "expires_after_2019_12_31-no-description", + "condition_expr_no_desc": `request.time < timestamp(\"2020-01-01T00:00:00Z\")`, + } + context["service_account"] = sa + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + Steps: []resource.TestStep{ + { + Config: testAccStorageManagedFolderIamPolicy_basicGenerated(context), + Check: resource.TestCheckResourceAttrSet("data.google_storage_managed_folder_iam_policy.foo", "policy_data"), + }, + { + ResourceName: "google_storage_managed_folder_iam_policy.foo", + ImportStateId: fmt.Sprintf("b/%s/managedFolders/managed/folder/name/", fmt.Sprintf("tf-test-my-bucket%s", context["random_suffix"])), + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccStorageManagedFolderIamPolicy_emptyBinding(context), + }, + { + ResourceName: "google_storage_managed_folder_iam_policy.foo", + ImportStateId: fmt.Sprintf("b/%s/managedFolders/managed/folder/name/", fmt.Sprintf("tf-test-my-bucket%s", context["random_suffix"])), + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccStorageManagedFolderIamBindingGenerated_withCondition(t *testing.T) { + t.Parallel() + + context := map[string]interface{}{ + "random_suffix": acctest.RandString(t, 10), + "role": "roles/storage.objectViewer", + "admin_role": "roles/storage.admin", + "condition_title": "expires_after_2019_12_31", + "condition_expr": `request.time < timestamp(\"2020-01-01T00:00:00Z\")`, + "condition_desc": "Expiring at midnight of 2019-12-31", + "condition_title_no_desc": "expires_after_2019_12_31-no-description", + "condition_expr_no_desc": `request.time < timestamp(\"2020-01-01T00:00:00Z\")`, + } + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + Steps: []resource.TestStep{ + { + Config: testAccStorageManagedFolderIamBinding_withConditionGenerated(context), + }, + { + ResourceName: "google_storage_managed_folder_iam_binding.foo", + ImportStateId: fmt.Sprintf("b/%s/managedFolders/managed/folder/name/ roles/storage.objectViewer %s", fmt.Sprintf("tf-test-my-bucket%s", context["random_suffix"]), context["condition_title"]), + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccStorageManagedFolderIamBindingGenerated_withAndWithoutCondition(t *testing.T) { + // Multiple fine-grained resources + acctest.SkipIfVcr(t) + t.Parallel() + + context := map[string]interface{}{ + "random_suffix": acctest.RandString(t, 10), + "role": "roles/storage.objectViewer", + "admin_role": "roles/storage.admin", + "condition_title": "expires_after_2019_12_31", + "condition_expr": `request.time < timestamp(\"2020-01-01T00:00:00Z\")`, + "condition_desc": "Expiring at midnight of 2019-12-31", + "condition_title_no_desc": "expires_after_2019_12_31-no-description", + "condition_expr_no_desc": `request.time < timestamp(\"2020-01-01T00:00:00Z\")`, + } + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + Steps: []resource.TestStep{ + { + Config: testAccStorageManagedFolderIamBinding_withAndWithoutConditionGenerated(context), + }, + { + ResourceName: "google_storage_managed_folder_iam_binding.foo", + ImportStateId: fmt.Sprintf("b/%s/managedFolders/managed/folder/name/ roles/storage.objectViewer", fmt.Sprintf("tf-test-my-bucket%s", context["random_suffix"])), + ImportState: true, + ImportStateVerify: true, + }, + { + ResourceName: "google_storage_managed_folder_iam_binding.foo2", + ImportStateId: fmt.Sprintf("b/%s/managedFolders/managed/folder/name/ roles/storage.objectViewer %s", fmt.Sprintf("tf-test-my-bucket%s", context["random_suffix"]), context["condition_title"]), + ImportState: true, + ImportStateVerify: true, + }, + { + ResourceName: "google_storage_managed_folder_iam_binding.foo3", + ImportStateId: fmt.Sprintf("b/%s/managedFolders/managed/folder/name/ roles/storage.objectViewer %s", fmt.Sprintf("tf-test-my-bucket%s", context["random_suffix"]), context["condition_title_no_desc"]), + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccStorageManagedFolderIamMemberGenerated_withCondition(t *testing.T) { + t.Parallel() + + context := map[string]interface{}{ + "random_suffix": acctest.RandString(t, 10), + "role": "roles/storage.objectViewer", + "admin_role": "roles/storage.admin", + "condition_title": "expires_after_2019_12_31", + "condition_expr": `request.time < timestamp(\"2020-01-01T00:00:00Z\")`, + "condition_desc": "Expiring at midnight of 2019-12-31", + "condition_title_no_desc": "expires_after_2019_12_31-no-description", + "condition_expr_no_desc": `request.time < timestamp(\"2020-01-01T00:00:00Z\")`, + } + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + Steps: []resource.TestStep{ + { + Config: testAccStorageManagedFolderIamMember_withConditionGenerated(context), + }, + { + ResourceName: "google_storage_managed_folder_iam_member.foo", + ImportStateId: fmt.Sprintf("b/%s/managedFolders/managed/folder/name/ roles/storage.objectViewer user:admin@hashicorptest.com %s", fmt.Sprintf("tf-test-my-bucket%s", context["random_suffix"]), context["condition_title"]), + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccStorageManagedFolderIamMemberGenerated_withAndWithoutCondition(t *testing.T) { + // Multiple fine-grained resources + acctest.SkipIfVcr(t) + t.Parallel() + + context := map[string]interface{}{ + "random_suffix": acctest.RandString(t, 10), + "role": "roles/storage.objectViewer", + "admin_role": "roles/storage.admin", + "condition_title": "expires_after_2019_12_31", + "condition_expr": `request.time < timestamp(\"2020-01-01T00:00:00Z\")`, + "condition_desc": "Expiring at midnight of 2019-12-31", + "condition_title_no_desc": "expires_after_2019_12_31-no-description", + "condition_expr_no_desc": `request.time < timestamp(\"2020-01-01T00:00:00Z\")`, + } + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + Steps: []resource.TestStep{ + { + Config: testAccStorageManagedFolderIamMember_withAndWithoutConditionGenerated(context), + }, + { + ResourceName: "google_storage_managed_folder_iam_member.foo", + ImportStateId: fmt.Sprintf("b/%s/managedFolders/managed/folder/name/ roles/storage.objectViewer user:admin@hashicorptest.com", fmt.Sprintf("tf-test-my-bucket%s", context["random_suffix"])), + ImportState: true, + ImportStateVerify: true, + }, + { + ResourceName: "google_storage_managed_folder_iam_member.foo2", + ImportStateId: fmt.Sprintf("b/%s/managedFolders/managed/folder/name/ roles/storage.objectViewer user:admin@hashicorptest.com %s", fmt.Sprintf("tf-test-my-bucket%s", context["random_suffix"]), context["condition_title"]), + ImportState: true, + ImportStateVerify: true, + }, + { + ResourceName: "google_storage_managed_folder_iam_member.foo3", + ImportStateId: fmt.Sprintf("b/%s/managedFolders/managed/folder/name/ roles/storage.objectViewer user:admin@hashicorptest.com %s", fmt.Sprintf("tf-test-my-bucket%s", context["random_suffix"]), context["condition_title_no_desc"]), + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccStorageManagedFolderIamPolicyGenerated_withCondition(t *testing.T) { + t.Parallel() + + // This may skip test, so do it first + sa := envvar.GetTestServiceAccountFromEnv(t) + context := map[string]interface{}{ + "random_suffix": acctest.RandString(t, 10), + "role": "roles/storage.objectViewer", + "admin_role": "roles/storage.admin", + "condition_title": "expires_after_2019_12_31", + "condition_expr": `request.time < timestamp(\"2020-01-01T00:00:00Z\")`, + "condition_desc": "Expiring at midnight of 2019-12-31", + "condition_title_no_desc": "expires_after_2019_12_31-no-description", + "condition_expr_no_desc": `request.time < timestamp(\"2020-01-01T00:00:00Z\")`, + } + context["service_account"] = sa + + // Test should have 3 bindings: one with a description and one without, and a third for an admin role. Any < chars are converted to a unicode character by the API. + expectedPolicyData := acctest.Nprintf(`{"bindings":[{"members":["serviceAccount:%{service_account}"],"role":"%{admin_role}"},{"condition":{"description":"%{condition_desc}","expression":"%{condition_expr}","title":"%{condition_title}"},"members":["user:admin@hashicorptest.com"],"role":"%{role}"},{"condition":{"expression":"%{condition_expr}","title":"%{condition_title}-no-description"},"members":["user:admin@hashicorptest.com"],"role":"%{role}"}]}`, context) + expectedPolicyData = strings.Replace(expectedPolicyData, "<", "\\u003c", -1) + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + Steps: []resource.TestStep{ + { + Config: testAccStorageManagedFolderIamPolicy_withConditionGenerated(context), + Check: resource.ComposeAggregateTestCheckFunc( + // TODO(SarahFrench) - uncomment once https://github.com/GoogleCloudPlatform/magic-modules/pull/6466 merged + // resource.TestCheckResourceAttr("data.google_iam_policy.foo", "policy_data", expectedPolicyData), + resource.TestCheckResourceAttr("google_storage_managed_folder_iam_policy.foo", "policy_data", expectedPolicyData), + resource.TestCheckResourceAttrWith("data.google_iam_policy.foo", "policy_data", tpgresource.CheckGoogleIamPolicy), + ), + }, + { + ResourceName: "google_storage_managed_folder_iam_policy.foo", + ImportStateId: fmt.Sprintf("b/%s/managedFolders/managed/folder/name/", fmt.Sprintf("tf-test-my-bucket%s", context["random_suffix"])), + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccStorageManagedFolderIamMember_basicGenerated(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_storage_bucket" "bucket" { + name = "tf-test-my-bucket%{random_suffix}" + location = "EU" + uniform_bucket_level_access = true +} + +resource "google_storage_managed_folder" "folder" { + bucket = google_storage_bucket.bucket.name + name = "managed/folder/name/" +} + +resource "google_storage_managed_folder_iam_member" "foo" { + bucket = google_storage_managed_folder.folder.bucket + managed_folder = google_storage_managed_folder.folder.name + role = "%{role}" + member = "user:admin@hashicorptest.com" +} +`, context) +} + +func testAccStorageManagedFolderIamPolicy_basicGenerated(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_storage_bucket" "bucket" { + name = "tf-test-my-bucket%{random_suffix}" + location = "EU" + uniform_bucket_level_access = true +} + +resource "google_storage_managed_folder" "folder" { + bucket = google_storage_bucket.bucket.name + name = "managed/folder/name/" +} + +data "google_iam_policy" "foo" { + binding { + role = "%{role}" + members = ["user:admin@hashicorptest.com"] + } + binding { + role = "%{admin_role}" + members = ["serviceAccount:%{service_account}"] + } +} + +resource "google_storage_managed_folder_iam_policy" "foo" { + bucket = google_storage_managed_folder.folder.bucket + managed_folder = google_storage_managed_folder.folder.name + policy_data = data.google_iam_policy.foo.policy_data +} + +data "google_storage_managed_folder_iam_policy" "foo" { + bucket = google_storage_managed_folder.folder.bucket + managed_folder = google_storage_managed_folder.folder.name + depends_on = [ + google_storage_managed_folder_iam_policy.foo + ] +} +`, context) +} + +func testAccStorageManagedFolderIamPolicy_emptyBinding(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_storage_bucket" "bucket" { + name = "tf-test-my-bucket%{random_suffix}" + location = "EU" + uniform_bucket_level_access = true +} + +resource "google_storage_managed_folder" "folder" { + bucket = google_storage_bucket.bucket.name + name = "managed/folder/name/" +} + +data "google_iam_policy" "foo" { +} + +resource "google_storage_managed_folder_iam_policy" "foo" { + bucket = google_storage_managed_folder.folder.bucket + managed_folder = google_storage_managed_folder.folder.name + policy_data = data.google_iam_policy.foo.policy_data +} +`, context) +} + +func testAccStorageManagedFolderIamBinding_basicGenerated(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_storage_bucket" "bucket" { + name = "tf-test-my-bucket%{random_suffix}" + location = "EU" + uniform_bucket_level_access = true +} + +resource "google_storage_managed_folder" "folder" { + bucket = google_storage_bucket.bucket.name + name = "managed/folder/name/" +} + +resource "google_storage_managed_folder_iam_binding" "foo" { + bucket = google_storage_managed_folder.folder.bucket + managed_folder = google_storage_managed_folder.folder.name + role = "%{role}" + members = ["user:admin@hashicorptest.com"] +} +`, context) +} + +func testAccStorageManagedFolderIamBinding_updateGenerated(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_storage_bucket" "bucket" { + name = "tf-test-my-bucket%{random_suffix}" + location = "EU" + uniform_bucket_level_access = true +} + +resource "google_storage_managed_folder" "folder" { + bucket = google_storage_bucket.bucket.name + name = "managed/folder/name/" +} + +resource "google_storage_managed_folder_iam_binding" "foo" { + bucket = google_storage_managed_folder.folder.bucket + managed_folder = google_storage_managed_folder.folder.name + role = "%{role}" + members = ["user:admin@hashicorptest.com", "user:gterraformtest1@gmail.com"] +} +`, context) +} + +func testAccStorageManagedFolderIamBinding_withConditionGenerated(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_storage_bucket" "bucket" { + name = "tf-test-my-bucket%{random_suffix}" + location = "EU" + uniform_bucket_level_access = true +} + +resource "google_storage_managed_folder" "folder" { + bucket = google_storage_bucket.bucket.name + name = "managed/folder/name/" +} + +resource "google_storage_managed_folder_iam_binding" "foo" { + bucket = google_storage_managed_folder.folder.bucket + managed_folder = google_storage_managed_folder.folder.name + role = "%{role}" + members = ["user:admin@hashicorptest.com"] + condition { + title = "%{condition_title}" + description = "%{condition_desc}" + expression = "%{condition_expr}" + } +} +`, context) +} + +func testAccStorageManagedFolderIamBinding_withAndWithoutConditionGenerated(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_storage_bucket" "bucket" { + name = "tf-test-my-bucket%{random_suffix}" + location = "EU" + uniform_bucket_level_access = true +} + +resource "google_storage_managed_folder" "folder" { + bucket = google_storage_bucket.bucket.name + name = "managed/folder/name/" +} + +resource "google_storage_managed_folder_iam_binding" "foo" { + bucket = google_storage_managed_folder.folder.bucket + managed_folder = google_storage_managed_folder.folder.name + role = "%{role}" + members = ["user:admin@hashicorptest.com"] +} + +resource "google_storage_managed_folder_iam_binding" "foo2" { + bucket = google_storage_managed_folder.folder.bucket + managed_folder = google_storage_managed_folder.folder.name + role = "%{role}" + members = ["user:admin@hashicorptest.com"] + condition { + title = "%{condition_title}" + description = "%{condition_desc}" + expression = "%{condition_expr}" + } +} + +resource "google_storage_managed_folder_iam_binding" "foo3" { + bucket = google_storage_managed_folder.folder.bucket + managed_folder = google_storage_managed_folder.folder.name + role = "%{role}" + members = ["user:admin@hashicorptest.com"] + condition { + # Check that lack of description doesn't cause any issues + # Relates to issue : https://github.com/hashicorp/terraform-provider-google/issues/8701 + title = "%{condition_title_no_desc}" + expression = "%{condition_expr_no_desc}" + } +} +`, context) +} + +func testAccStorageManagedFolderIamMember_withConditionGenerated(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_storage_bucket" "bucket" { + name = "tf-test-my-bucket%{random_suffix}" + location = "EU" + uniform_bucket_level_access = true +} + +resource "google_storage_managed_folder" "folder" { + bucket = google_storage_bucket.bucket.name + name = "managed/folder/name/" +} + +resource "google_storage_managed_folder_iam_member" "foo" { + bucket = google_storage_managed_folder.folder.bucket + managed_folder = google_storage_managed_folder.folder.name + role = "%{role}" + member = "user:admin@hashicorptest.com" + condition { + title = "%{condition_title}" + description = "%{condition_desc}" + expression = "%{condition_expr}" + } +} +`, context) +} + +func testAccStorageManagedFolderIamMember_withAndWithoutConditionGenerated(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_storage_bucket" "bucket" { + name = "tf-test-my-bucket%{random_suffix}" + location = "EU" + uniform_bucket_level_access = true +} + +resource "google_storage_managed_folder" "folder" { + bucket = google_storage_bucket.bucket.name + name = "managed/folder/name/" +} + +resource "google_storage_managed_folder_iam_member" "foo" { + bucket = google_storage_managed_folder.folder.bucket + managed_folder = google_storage_managed_folder.folder.name + role = "%{role}" + member = "user:admin@hashicorptest.com" +} + +resource "google_storage_managed_folder_iam_member" "foo2" { + bucket = google_storage_managed_folder.folder.bucket + managed_folder = google_storage_managed_folder.folder.name + role = "%{role}" + member = "user:admin@hashicorptest.com" + condition { + title = "%{condition_title}" + description = "%{condition_desc}" + expression = "%{condition_expr}" + } +} + +resource "google_storage_managed_folder_iam_member" "foo3" { + bucket = google_storage_managed_folder.folder.bucket + managed_folder = google_storage_managed_folder.folder.name + role = "%{role}" + member = "user:admin@hashicorptest.com" + condition { + # Check that lack of description doesn't cause any issues + # Relates to issue : https://github.com/hashicorp/terraform-provider-google/issues/8701 + title = "%{condition_title_no_desc}" + expression = "%{condition_expr_no_desc}" + } +} +`, context) +} + +func testAccStorageManagedFolderIamPolicy_withConditionGenerated(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_storage_bucket" "bucket" { + name = "tf-test-my-bucket%{random_suffix}" + location = "EU" + uniform_bucket_level_access = true +} + +resource "google_storage_managed_folder" "folder" { + bucket = google_storage_bucket.bucket.name + name = "managed/folder/name/" +} + +data "google_iam_policy" "foo" { + binding { + role = "%{role}" + members = ["user:admin@hashicorptest.com"] + condition { + # Check that lack of description doesn't cause any issues + # Relates to issue : https://github.com/hashicorp/terraform-provider-google/issues/8701 + title = "%{condition_title_no_desc}" + expression = "%{condition_expr_no_desc}" + } + } + binding { + role = "%{role}" + members = ["user:admin@hashicorptest.com"] + condition { + title = "%{condition_title}" + description = "%{condition_desc}" + expression = "%{condition_expr}" + } + } + binding { + role = "%{admin_role}" + members = ["serviceAccount:%{service_account}"] + } +} + +resource "google_storage_managed_folder_iam_policy" "foo" { + bucket = google_storage_managed_folder.folder.bucket + managed_folder = google_storage_managed_folder.folder.name + policy_data = data.google_iam_policy.foo.policy_data +} +`, context) +} diff --git a/google/services/storage/resource_storage_managed_folder.go b/google/services/storage/resource_storage_managed_folder.go new file mode 100644 index 00000000000..86fce4baf82 --- /dev/null +++ b/google/services/storage/resource_storage_managed_folder.go @@ -0,0 +1,294 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +// ---------------------------------------------------------------------------- +// +// *** AUTO GENERATED CODE *** Type: MMv1 *** +// +// ---------------------------------------------------------------------------- +// +// This file is automatically generated by Magic Modules and manual +// changes will be clobbered when the file is regenerated. +// +// Please read more about how to change this file in +// .github/CONTRIBUTING.md. +// +// ---------------------------------------------------------------------------- + +package storage + +import ( + "fmt" + "log" + "net/http" + "reflect" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/hashicorp/terraform-provider-google/google/tpgresource" + transport_tpg "github.com/hashicorp/terraform-provider-google/google/transport" + "github.com/hashicorp/terraform-provider-google/google/verify" +) + +func ResourceStorageManagedFolder() *schema.Resource { + return &schema.Resource{ + Create: resourceStorageManagedFolderCreate, + Read: resourceStorageManagedFolderRead, + Delete: resourceStorageManagedFolderDelete, + + Importer: &schema.ResourceImporter{ + State: resourceStorageManagedFolderImport, + }, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(20 * time.Minute), + Delete: schema.DefaultTimeout(20 * time.Minute), + }, + + Schema: map[string]*schema.Schema{ + "bucket": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + DiffSuppressFunc: tpgresource.CompareSelfLinkOrResourceName, + Description: `The name of the bucket that contains the managed folder.`, + }, + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: verify.ValidateRegexp(`/$`), + Description: `The name of the managed folder expressed as a path. Must include +trailing '/'. For example, 'example_dir/example_dir2/'.`, + }, + "create_time": { + Type: schema.TypeString, + Computed: true, + Description: `The timestamp at which this managed folder was created.`, + }, + "metageneration": { + Type: schema.TypeString, + Computed: true, + Description: `The metadata generation of the managed folder.`, + }, + "update_time": { + Type: schema.TypeString, + Computed: true, + Description: `The timestamp at which this managed folder was most recently updated.`, + }, + "self_link": { + Type: schema.TypeString, + Computed: true, + }, + }, + UseJSONNumber: true, + } +} + +func resourceStorageManagedFolderCreate(d *schema.ResourceData, meta interface{}) error { + config := meta.(*transport_tpg.Config) + userAgent, err := tpgresource.GenerateUserAgentString(d, config.UserAgent) + if err != nil { + return err + } + + obj := make(map[string]interface{}) + bucketProp, err := expandStorageManagedFolderBucket(d.Get("bucket"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("bucket"); !tpgresource.IsEmptyValue(reflect.ValueOf(bucketProp)) && (ok || !reflect.DeepEqual(v, bucketProp)) { + obj["bucket"] = bucketProp + } + nameProp, err := expandStorageManagedFolderName(d.Get("name"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("name"); !tpgresource.IsEmptyValue(reflect.ValueOf(nameProp)) && (ok || !reflect.DeepEqual(v, nameProp)) { + obj["name"] = nameProp + } + + url, err := tpgresource.ReplaceVars(d, config, "{{StorageBasePath}}b/{{bucket}}/managedFolders") + if err != nil { + return err + } + + log.Printf("[DEBUG] Creating new ManagedFolder: %#v", obj) + billingProject := "" + + // err == nil indicates that the billing_project value was found + if bp, err := tpgresource.GetBillingProject(d, config); err == nil { + billingProject = bp + } + + headers := make(http.Header) + res, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "POST", + Project: billingProject, + RawURL: url, + UserAgent: userAgent, + Body: obj, + Timeout: d.Timeout(schema.TimeoutCreate), + Headers: headers, + }) + if err != nil { + return fmt.Errorf("Error creating ManagedFolder: %s", err) + } + + // Store the ID now + id, err := tpgresource.ReplaceVars(d, config, "{{bucket}}/{{name}}") + if err != nil { + return fmt.Errorf("Error constructing id: %s", err) + } + d.SetId(id) + + log.Printf("[DEBUG] Finished creating ManagedFolder %q: %#v", d.Id(), res) + + return resourceStorageManagedFolderRead(d, meta) +} + +func resourceStorageManagedFolderRead(d *schema.ResourceData, meta interface{}) error { + config := meta.(*transport_tpg.Config) + userAgent, err := tpgresource.GenerateUserAgentString(d, config.UserAgent) + if err != nil { + return err + } + + url, err := tpgresource.ReplaceVars(d, config, "{{StorageBasePath}}b/{{bucket}}/managedFolders/{{%name}}") + if err != nil { + return err + } + + billingProject := "" + + // err == nil indicates that the billing_project value was found + if bp, err := tpgresource.GetBillingProject(d, config); err == nil { + billingProject = bp + } + + headers := make(http.Header) + res, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "GET", + Project: billingProject, + RawURL: url, + UserAgent: userAgent, + Headers: headers, + }) + if err != nil { + return transport_tpg.HandleNotFoundError(err, d, fmt.Sprintf("StorageManagedFolder %q", d.Id())) + } + + if err := d.Set("create_time", flattenStorageManagedFolderCreateTime(res["createTime"], d, config)); err != nil { + return fmt.Errorf("Error reading ManagedFolder: %s", err) + } + if err := d.Set("update_time", flattenStorageManagedFolderUpdateTime(res["updateTime"], d, config)); err != nil { + return fmt.Errorf("Error reading ManagedFolder: %s", err) + } + if err := d.Set("metageneration", flattenStorageManagedFolderMetageneration(res["metageneration"], d, config)); err != nil { + return fmt.Errorf("Error reading ManagedFolder: %s", err) + } + if err := d.Set("bucket", flattenStorageManagedFolderBucket(res["bucket"], d, config)); err != nil { + return fmt.Errorf("Error reading ManagedFolder: %s", err) + } + if err := d.Set("name", flattenStorageManagedFolderName(res["name"], d, config)); err != nil { + return fmt.Errorf("Error reading ManagedFolder: %s", err) + } + if err := d.Set("self_link", tpgresource.ConvertSelfLinkToV1(res["selfLink"].(string))); err != nil { + return fmt.Errorf("Error reading ManagedFolder: %s", err) + } + + return nil +} + +func resourceStorageManagedFolderDelete(d *schema.ResourceData, meta interface{}) error { + config := meta.(*transport_tpg.Config) + userAgent, err := tpgresource.GenerateUserAgentString(d, config.UserAgent) + if err != nil { + return err + } + + billingProject := "" + + url, err := tpgresource.ReplaceVars(d, config, "{{StorageBasePath}}b/{{bucket}}/managedFolders/{{%name}}") + if err != nil { + return err + } + + var obj map[string]interface{} + + // err == nil indicates that the billing_project value was found + if bp, err := tpgresource.GetBillingProject(d, config); err == nil { + billingProject = bp + } + + headers := make(http.Header) + + log.Printf("[DEBUG] Deleting ManagedFolder %q", d.Id()) + res, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "DELETE", + Project: billingProject, + RawURL: url, + UserAgent: userAgent, + Body: obj, + Timeout: d.Timeout(schema.TimeoutDelete), + Headers: headers, + }) + if err != nil { + return transport_tpg.HandleNotFoundError(err, d, "ManagedFolder") + } + + log.Printf("[DEBUG] Finished deleting ManagedFolder %q: %#v", d.Id(), res) + return nil +} + +func resourceStorageManagedFolderImport(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + config := meta.(*transport_tpg.Config) + if err := tpgresource.ParseImportId([]string{ + "^(?P[^/]+)/managedFolders/(?P.+)$", + "^(?P[^/]+)/(?P.+)$", + }, d, config); err != nil { + return nil, err + } + + // Replace import id for the resource id + id, err := tpgresource.ReplaceVars(d, config, "{{bucket}}/{{name}}") + if err != nil { + return nil, fmt.Errorf("Error constructing id: %s", err) + } + d.SetId(id) + + return []*schema.ResourceData{d}, nil +} + +func flattenStorageManagedFolderCreateTime(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + return v +} + +func flattenStorageManagedFolderUpdateTime(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + return v +} + +func flattenStorageManagedFolderMetageneration(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + return v +} + +func flattenStorageManagedFolderBucket(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + if v == nil { + return v + } + return tpgresource.ConvertSelfLinkToV1(v.(string)) +} + +func flattenStorageManagedFolderName(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + return v +} + +func expandStorageManagedFolderBucket(v interface{}, d tpgresource.TerraformResourceData, config *transport_tpg.Config) (interface{}, error) { + return v, nil +} + +func expandStorageManagedFolderName(v interface{}, d tpgresource.TerraformResourceData, config *transport_tpg.Config) (interface{}, error) { + return v, nil +} diff --git a/google/services/storage/resource_storage_managed_folder_generated_test.go b/google/services/storage/resource_storage_managed_folder_generated_test.go new file mode 100644 index 00000000000..162bd8496a1 --- /dev/null +++ b/google/services/storage/resource_storage_managed_folder_generated_test.go @@ -0,0 +1,110 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +// ---------------------------------------------------------------------------- +// +// *** AUTO GENERATED CODE *** Type: MMv1 *** +// +// ---------------------------------------------------------------------------- +// +// This file is automatically generated by Magic Modules and manual +// changes will be clobbered when the file is regenerated. +// +// Please read more about how to change this file in +// .github/CONTRIBUTING.md. +// +// ---------------------------------------------------------------------------- + +package storage_test + +import ( + "fmt" + "strings" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + + "github.com/hashicorp/terraform-provider-google/google/acctest" + "github.com/hashicorp/terraform-provider-google/google/tpgresource" + transport_tpg "github.com/hashicorp/terraform-provider-google/google/transport" +) + +func TestAccStorageManagedFolder_storageManagedFolderBasicExample(t *testing.T) { + t.Parallel() + + context := map[string]interface{}{ + "random_suffix": acctest.RandString(t, 10), + } + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + CheckDestroy: testAccCheckStorageManagedFolderDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccStorageManagedFolder_storageManagedFolderBasicExample(context), + }, + { + ResourceName: "google_storage_managed_folder.folder", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"bucket"}, + }, + }, + }) +} + +func testAccStorageManagedFolder_storageManagedFolderBasicExample(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_storage_bucket" "bucket" { + name = "tf-test-my-bucket%{random_suffix}" + location = "EU" + uniform_bucket_level_access = true +} + +resource "google_storage_managed_folder" "folder" { + bucket = google_storage_bucket.bucket.name + name = "managed/folder/name/" +} +`, context) +} + +func testAccCheckStorageManagedFolderDestroyProducer(t *testing.T) func(s *terraform.State) error { + return func(s *terraform.State) error { + for name, rs := range s.RootModule().Resources { + if rs.Type != "google_storage_managed_folder" { + continue + } + if strings.HasPrefix(name, "data.") { + continue + } + + config := acctest.GoogleProviderConfig(t) + + url, err := tpgresource.ReplaceVarsForTest(config, rs, "{{StorageBasePath}}b/{{bucket}}/managedFolders/{{%name}}") + if err != nil { + return err + } + + billingProject := "" + + if config.BillingProject != "" { + billingProject = config.BillingProject + } + + _, err = transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "GET", + Project: billingProject, + RawURL: url, + UserAgent: config.UserAgent, + }) + if err == nil { + return fmt.Errorf("StorageManagedFolder still exists at %s", url) + } + } + + return nil + } +} diff --git a/website/docs/r/storage_managed_folder.html.markdown b/website/docs/r/storage_managed_folder.html.markdown new file mode 100644 index 00000000000..d664ea19315 --- /dev/null +++ b/website/docs/r/storage_managed_folder.html.markdown @@ -0,0 +1,129 @@ +--- +# ---------------------------------------------------------------------------- +# +# *** AUTO GENERATED CODE *** Type: MMv1 *** +# +# ---------------------------------------------------------------------------- +# +# This file is automatically generated by Magic Modules and manual +# changes will be clobbered when the file is regenerated. +# +# Please read more about how to change this file in +# .github/CONTRIBUTING.md. +# +# ---------------------------------------------------------------------------- +subcategory: "Cloud Storage" +description: |- + A Google Cloud Storage Managed Folder. +--- + +# google_storage_managed_folder + +A Google Cloud Storage Managed Folder. + +You can apply Identity and Access Management (IAM) policies to +managed folders to grant principals access only to the objects +within the managed folder, which lets you more finely control access +for specific data sets and tables within a bucket. You can nest +managed folders up to 15 levels deep, including the parent managed +folder. + +Managed folders can only be created in buckets that have uniform +bucket-level access enabled. + + +To get more information about ManagedFolder, see: + +* [API documentation](https://cloud.google.com/storage/docs/json_api/v1/managedFolder) +* How-to Guides + * [Official Documentation](https://cloud.google.com/storage/docs/managed-folders) + + +## Example Usage - Storage Managed Folder Basic + + +```hcl +resource "google_storage_bucket" "bucket" { + name = "my-bucket" + location = "EU" + uniform_bucket_level_access = true +} + +resource "google_storage_managed_folder" "folder" { + bucket = google_storage_bucket.bucket.name + name = "managed/folder/name/" +} +``` + +## Argument Reference + +The following arguments are supported: + + +* `bucket` - + (Required) + The name of the bucket that contains the managed folder. + +* `name` - + (Required) + The name of the managed folder expressed as a path. Must include + trailing '/'. For example, `example_dir/example_dir2/`. + + +- - - + + + +## Attributes Reference + +In addition to the arguments listed above, the following computed attributes are exported: + +* `id` - an identifier for the resource with format `{{bucket}}/{{name}}` + +* `create_time` - + The timestamp at which this managed folder was created. + +* `update_time` - + The timestamp at which this managed folder was most recently updated. + +* `metageneration` - + The metadata generation of the managed folder. +* `self_link` - The URI of the created resource. + + +## Timeouts + +This resource provides the following +[Timeouts](https://developer.hashicorp.com/terraform/plugin/sdkv2/resources/retries-and-customizable-timeouts) configuration options: + +- `create` - Default is 20 minutes. +- `delete` - Default is 20 minutes. + +## Import + + +ManagedFolder can be imported using any of these accepted formats: + +* `{{bucket}}/managedFolders/{{name}}` +* `{{bucket}}/{{name}}` + + +In Terraform v1.5.0 and later, use an [`import` block](https://developer.hashicorp.com/terraform/language/import) to import ManagedFolder using one of the formats above. For example: + +```tf +import { + id = "{{bucket}}/managedFolders/{{name}}" + to = google_storage_managed_folder.default +} +``` + +When using the [`terraform import` command](https://developer.hashicorp.com/terraform/cli/commands/import), ManagedFolder can be imported using one of the formats above. For example: + +``` +$ terraform import google_storage_managed_folder.default {{bucket}}/managedFolders/{{name}} +$ terraform import google_storage_managed_folder.default {{bucket}}/{{name}} +``` diff --git a/website/docs/r/storage_managed_folder_iam.html.markdown b/website/docs/r/storage_managed_folder_iam.html.markdown new file mode 100644 index 00000000000..92894e37245 --- /dev/null +++ b/website/docs/r/storage_managed_folder_iam.html.markdown @@ -0,0 +1,203 @@ +--- +subcategory: "Cloud Storage" +description: |- + Collection of resources to manage IAM policy for Cloud Storage ManagedFolder +--- + +# IAM policy for Cloud Storage ManagedFolder +Three different resources help you manage your IAM policy for Cloud Storage ManagedFolder. Each of these resources serves a different use case: + +* `google_storage_managed_folder_iam_policy`: Authoritative. Sets the IAM policy for the managedfolder and replaces any existing policy already attached. +* `google_storage_managed_folder_iam_binding`: Authoritative for a given role. Updates the IAM policy to grant a role to a list of members. Other roles within the IAM policy for the managedfolder are preserved. +* `google_storage_managed_folder_iam_member`: Non-authoritative. Updates the IAM policy to grant a role to a new member. Other members for the role for the managedfolder are preserved. + +A data source can be used to retrieve policy data in advent you do not need creation + +* `google_storage_managed_folder_iam_policy`: Retrieves the IAM policy for the managedfolder + +~> **Note:** `google_storage_managed_folder_iam_policy` **cannot** be used in conjunction with `google_storage_managed_folder_iam_binding` and `google_storage_managed_folder_iam_member` or they will fight over what your policy should be. + +~> **Note:** `google_storage_managed_folder_iam_binding` resources **can be** used in conjunction with `google_storage_managed_folder_iam_member` resources **only if** they do not grant privilege to the same role. + +~> **Note:** This resource supports IAM Conditions but they have some known limitations which can be found [here](https://cloud.google.com/iam/docs/conditions-overview#limitations). Please review this article if you are having issues with IAM Conditions. + + +## google_storage_managed_folder_iam_policy + +```hcl +data "google_iam_policy" "admin" { + binding { + role = "roles/storage.admin" + members = [ + "user:jane@example.com", + ] + } +} + +resource "google_storage_managed_folder_iam_policy" "policy" { + bucket = google_storage_managed_folder.folder.bucket + managed_folder = google_storage_managed_folder.folder.name + policy_data = data.google_iam_policy.admin.policy_data +} +``` + +With IAM Conditions: + +```hcl +data "google_iam_policy" "admin" { + binding { + role = "roles/storage.admin" + members = [ + "user:jane@example.com", + ] + + condition { + title = "expires_after_2019_12_31" + description = "Expiring at midnight of 2019-12-31" + expression = "request.time < timestamp(\"2020-01-01T00:00:00Z\")" + } + } +} + +resource "google_storage_managed_folder_iam_policy" "policy" { + bucket = google_storage_managed_folder.folder.bucket + managed_folder = google_storage_managed_folder.folder.name + policy_data = data.google_iam_policy.admin.policy_data +} +``` +## google_storage_managed_folder_iam_binding + +```hcl +resource "google_storage_managed_folder_iam_binding" "binding" { + bucket = google_storage_managed_folder.folder.bucket + managed_folder = google_storage_managed_folder.folder.name + role = "roles/storage.admin" + members = [ + "user:jane@example.com", + ] +} +``` + +With IAM Conditions: + +```hcl +resource "google_storage_managed_folder_iam_binding" "binding" { + bucket = google_storage_managed_folder.folder.bucket + managed_folder = google_storage_managed_folder.folder.name + role = "roles/storage.admin" + members = [ + "user:jane@example.com", + ] + + condition { + title = "expires_after_2019_12_31" + description = "Expiring at midnight of 2019-12-31" + expression = "request.time < timestamp(\"2020-01-01T00:00:00Z\")" + } +} +``` +## google_storage_managed_folder_iam_member + +```hcl +resource "google_storage_managed_folder_iam_member" "member" { + bucket = google_storage_managed_folder.folder.bucket + managed_folder = google_storage_managed_folder.folder.name + role = "roles/storage.admin" + member = "user:jane@example.com" +} +``` + +With IAM Conditions: + +```hcl +resource "google_storage_managed_folder_iam_member" "member" { + bucket = google_storage_managed_folder.folder.bucket + managed_folder = google_storage_managed_folder.folder.name + role = "roles/storage.admin" + member = "user:jane@example.com" + + condition { + title = "expires_after_2019_12_31" + description = "Expiring at midnight of 2019-12-31" + expression = "request.time < timestamp(\"2020-01-01T00:00:00Z\")" + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `bucket` - (Required) The name of the bucket that contains the managed folder. Used to find the parent resource to bind the IAM policy to +* `managed_folder` - (Required) Used to find the parent resource to bind the IAM policy to + +* `member/members` - (Required) Identities that will be granted the privilege in `role`. + Each entry can have one of the following values: + * **allUsers**: A special identifier that represents anyone who is on the internet; with or without a Google account. + * **allAuthenticatedUsers**: A special identifier that represents anyone who is authenticated with a Google account or a service account. + * **user:{emailid}**: An email address that represents a specific Google account. For example, alice@gmail.com or joe@example.com. + * **serviceAccount:{emailid}**: An email address that represents a service account. For example, my-other-app@appspot.gserviceaccount.com. + * **group:{emailid}**: An email address that represents a Google group. For example, admins@example.com. + * **domain:{domain}**: A G Suite domain (primary, instead of alias) name that represents all the users of that domain. For example, google.com or example.com. + * **projectOwner:projectid**: Owners of the given project. For example, "projectOwner:my-example-project" + * **projectEditor:projectid**: Editors of the given project. For example, "projectEditor:my-example-project" + * **projectViewer:projectid**: Viewers of the given project. For example, "projectViewer:my-example-project" + +* `role` - (Required) The role that should be applied. Only one + `google_storage_managed_folder_iam_binding` can be used per role. Note that custom roles must be of the format + `[projects|organizations]/{parent-name}/roles/{role-name}`. + +* `policy_data` - (Required only by `google_storage_managed_folder_iam_policy`) The policy data generated by + a `google_iam_policy` data source. + +* `condition` - (Optional) An [IAM Condition](https://cloud.google.com/iam/docs/conditions-overview) for a given binding. + Structure is documented below. + +--- + +The `condition` block supports: + +* `expression` - (Required) Textual representation of an expression in Common Expression Language syntax. + +* `title` - (Required) A title for the expression, i.e. a short string describing its purpose. + +* `description` - (Optional) An optional description of the expression. This is a longer text which describes the expression, e.g. when hovered over it in a UI. + +~> **Warning:** Terraform considers the `role` and condition contents (`title`+`description`+`expression`) as the + identifier for the binding. This means that if any part of the condition is changed out-of-band, Terraform will + consider it to be an entirely different resource and will treat it as such. +## Attributes Reference + +In addition to the arguments listed above, the following computed attributes are +exported: + +* `etag` - (Computed) The etag of the IAM policy. + +## Import + +For all import syntaxes, the "resource in question" can take any of the following forms: + +* b/{{bucket}}/managedFolders/{{managed_folder}} +* {{bucket}}/{{managed_folder}} + +Any variables not passed in the import command will be taken from the provider configuration. + +Cloud Storage managedfolder IAM resources can be imported using the resource identifiers, role, and member. + +IAM member imports use space-delimited identifiers: the resource in question, the role, and the member identity, e.g. +``` +$ terraform import google_storage_managed_folder_iam_member.editor "b/{{bucket}}/managedFolders/{{managed_folder}} roles/storage.objectViewer user:jane@example.com" +``` + +IAM binding imports use space-delimited identifiers: the resource in question and the role, e.g. +``` +$ terraform import google_storage_managed_folder_iam_binding.editor "b/{{bucket}}/managedFolders/{{managed_folder}} roles/storage.objectViewer" +``` + +IAM policy imports use the identifier of the resource in question, e.g. +``` +$ terraform import google_storage_managed_folder_iam_policy.editor b/{{bucket}}/managedFolders/{{managed_folder}} +``` + +-> **Custom Roles**: If you're importing a IAM resource with a custom role, make sure to use the + full name of the custom role, e.g. `[projects/my-project|organizations/my-org]/roles/my-custom-role`.