diff --git a/third_party/terraform/data_sources/data_source_google_project_services.go b/third_party/terraform/data_sources/data_source_google_project_services.go deleted file mode 100644 index 5c50559ee893..000000000000 --- a/third_party/terraform/data_sources/data_source_google_project_services.go +++ /dev/null @@ -1,30 +0,0 @@ -package google - -import ( - "github.com/hashicorp/terraform-plugin-sdk/helper/schema" -) - -func dataSourceGoogleProjectServices() *schema.Resource { - // Generate datasource schema from resource - dsSchema := datasourceSchemaFromResourceSchema(resourceGoogleProjectServices().Schema) - - // Set 'Optional' schema elements - addOptionalFieldsToSchema(dsSchema, "project") - - return &schema.Resource{ - Read: dataSourceGoogleProjectServicesRead, - Schema: dsSchema, - } -} - -func dataSourceGoogleProjectServicesRead(d *schema.ResourceData, meta interface{}) error { - config := meta.(*Config) - - project, err := getProject(d, config) - if err != nil { - return err - } - d.SetId(project) - - return resourceGoogleProjectServicesRead(d, meta) -} diff --git a/third_party/terraform/resources/resource_cloudiot_registry.go b/third_party/terraform/resources/resource_cloudiot_registry.go index 729ca345eee4..60ca3aa38d87 100644 --- a/third_party/terraform/resources/resource_cloudiot_registry.go +++ b/third_party/terraform/resources/resource_cloudiot_registry.go @@ -62,10 +62,10 @@ func resourceCloudIoTRegistry() *schema.Resource { Removed: "Please use event_notification_configs instead", }, "event_notification_configs": { - Type: schema.TypeList, - Optional: true, - Computed: true, - MaxItems: 10, + Type: schema.TypeList, + Optional: true, + Computed: true, + MaxItems: 10, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "pubsub_topic_name": { diff --git a/third_party/terraform/resources/resource_google_project.go b/third_party/terraform/resources/resource_google_project.go index a3d98ff81a93..c03c3cbb41a0 100644 --- a/third_party/terraform/resources/resource_google_project.go +++ b/third_party/terraform/resources/resource_google_project.go @@ -1,6 +1,7 @@ package google import ( + "context" "fmt" "log" "regexp" @@ -13,6 +14,7 @@ import ( "google.golang.org/api/cloudbilling/v1" "google.golang.org/api/cloudresourcemanager/v1" "google.golang.org/api/googleapi" + "google.golang.org/api/serviceusage/v1" ) // resourceGoogleProject returns a *schema.Resource that allows a customer @@ -492,6 +494,76 @@ func enableServiceUsageProjectServices(services []string, project string, config return waitForServiceUsageEnabledServices(services, project, config, timeout) } +func doEnableServicesRequest(services []string, project string, config *Config, timeout time.Duration) error { + var op *serviceusage.Operation + + err := retryTimeDuration(func() error { + var rerr error + if len(services) == 1 { + // BatchEnable returns an error for a single item, so just enable + // using service endpoint. + name := fmt.Sprintf("projects/%s/services/%s", project, services[0]) + req := &serviceusage.EnableServiceRequest{} + op, rerr = config.clientServiceUsage.Services.Enable(name, req).Do() + } else { + // Batch enable for multiple services. + name := fmt.Sprintf("projects/%s", project) + req := &serviceusage.BatchEnableServicesRequest{ServiceIds: services} + op, rerr = config.clientServiceUsage.Services.BatchEnable(name, req).Do() + } + return handleServiceUsageRetryableError(rerr) + }, timeout) + if err != nil { + return errwrap.Wrapf("failed to send enable services request: {{err}}", err) + } + // Poll for the API to return + waitErr := serviceUsageOperationWait(config, op, fmt.Sprintf("Enable Project %q Services: %+v", project, services)) + if waitErr != nil { + return waitErr + } + return nil +} + +// Retrieve a project's services from the API +func listCurrentlyEnabledServices(project string, config *Config, timeout time.Duration) (map[string]struct{}, error) { + // Verify project for services still exists + p, err := config.clientResourceManager.Projects.Get(project).Do() + if err != nil { + return nil, err + } + if p.LifecycleState == "DELETE_REQUESTED" { + // Construct a 404 error for handleNotFoundError + return nil, &googleapi.Error{ + Code: 404, + Message: "Project deletion was requested", + } + } + + log.Printf("[DEBUG] Listing enabled services for project %s", project) + apiServices := make(map[string]struct{}) + err = retryTimeDuration(func() error { + ctx := context.Background() + return config.clientServiceUsage.Services. + List(fmt.Sprintf("projects/%s", project)). + Fields("services/name,nextPageToken"). + Filter("state:ENABLED"). + Pages(ctx, func(r *serviceusage.ListServicesResponse) error { + for _, v := range r.Services { + // services are returned as "projects/PROJECT/services/NAME" + name := GetResourceNameFromSelfLink(v.Name) + if _, ok := ignoredProjectServicesSet[name]; !ok { + apiServices[name] = struct{}{} + } + } + return nil + }) + }, timeout) + if err != nil { + return nil, errwrap.Wrapf(fmt.Sprintf("Failed to list enabled services for project %s: {{err}}", project), err) + } + return apiServices, nil +} + // waitForServiceUsageEnabledServices doesn't resend enable requests - it just // waits for service enablement status to propagate. Essentially, it waits until // all services show up as enabled when listing services on the project. diff --git a/third_party/terraform/resources/resource_google_project_service.go b/third_party/terraform/resources/resource_google_project_service.go index 7e026db554f8..1d949abc8225 100644 --- a/third_party/terraform/resources/resource_google_project_service.go +++ b/third_party/terraform/resources/resource_google_project_service.go @@ -51,6 +51,8 @@ var renamedServicesByNewServiceNames = reverseStringMap(renamedServices) // renamedServices expressed as both old -> new and new -> old var renamedServicesByOldAndNewServiceNames = mergeStringMaps(renamedServices, renamedServicesByNewServiceNames) +const maxServiceUsageBatchSize = 20 + func resourceGoogleProjectService() *schema.Resource { return &schema.Resource{ Create: resourceGoogleProjectServiceCreate, diff --git a/third_party/terraform/resources/resource_google_project_services.go b/third_party/terraform/resources/resource_google_project_services.go deleted file mode 100644 index 30c8a0eb11d5..000000000000 --- a/third_party/terraform/resources/resource_google_project_services.go +++ /dev/null @@ -1,335 +0,0 @@ -package google - -import ( - "context" - "fmt" - "github.com/hashicorp/errwrap" - "github.com/hashicorp/terraform-plugin-sdk/helper/schema" - "google.golang.org/api/googleapi" - "google.golang.org/api/serviceusage/v1" - "log" - "strings" - "time" -) - -const maxServiceUsageBatchSize = 20 - -func resourceGoogleProjectServices() *schema.Resource { - return &schema.Resource{ - Create: resourceGoogleProjectServicesCreateUpdate, - Read: resourceGoogleProjectServicesRead, - Update: resourceGoogleProjectServicesCreateUpdate, - Delete: resourceGoogleProjectServicesDelete, - Importer: &schema.ResourceImporter{ - State: schema.ImportStatePassthrough, - }, - DeprecationMessage: "google_project_services is deprecated - many users reported " + - "issues with dependent services that were not resolvable. Please use google_project_service or the " + - "https://github.com/terraform-google-modules/terraform-google-project-factory/tree/master/modules/project_services" + - " module. It's recommended that you use a provider version of 2.13.0 or higher when you migrate so that requests are" + - " batched to the API, reducing the request rate. This resource will be removed in version 3.0.0.", - - Timeouts: &schema.ResourceTimeout{ - Create: schema.DefaultTimeout(20 * time.Minute), - Update: schema.DefaultTimeout(20 * time.Minute), - Read: schema.DefaultTimeout(10 * time.Minute), - Delete: schema.DefaultTimeout(20 * time.Minute), - }, - - Schema: map[string]*schema.Schema{ - "project": { - Type: schema.TypeString, - Optional: true, - ForceNew: true, - Computed: true, - }, - "services": { - Type: schema.TypeSet, - Required: true, - Set: schema.HashString, - Elem: &schema.Schema{ - Type: schema.TypeString, - ValidateFunc: StringNotInSlice(ignoredProjectServices, false), - }, - }, - "disable_on_destroy": { - Type: schema.TypeBool, - Optional: true, - Default: true, - }, - }, - } -} - -func resourceGoogleProjectServicesCreateUpdate(d *schema.ResourceData, meta interface{}) error { - config := meta.(*Config) - - project, err := getProject(d, config) - if err != nil { - return err - } - - // Get services from config - services, err := expandServiceUsageProjectServicesServices(d.Get("services"), d, config) - if err != nil { - return err - } - - log.Printf("[DEBUG]: Enabling Project Services for %s: %+v", d.Id(), services) - if err := setServiceUsageProjectEnabledServices(services, project, d, config); err != nil { - return fmt.Errorf("Error authoritatively enabling Project %s Services: %v", project, err) - } - log.Printf("[DEBUG]: Finished enabling Project Services for %s: %+v", d.Id(), services) - - d.SetId(project) - return resourceGoogleProjectServicesRead(d, meta) -} - -func resourceGoogleProjectServicesRead(d *schema.ResourceData, meta interface{}) error { - config := meta.(*Config) - - enabledSet, err := listCurrentlyEnabledServices(d.Id(), config, d.Timeout(schema.TimeoutRead)) - if err != nil { - return err - } - - // use old services to set the correct renamed service names in state - s, _ := expandServiceUsageProjectServicesServices(d.Get("services"), d, config) - log.Printf("[DEBUG] Saw services in state on Read: %s ", s) - sset := golangSetFromStringSlice(s) - for ov, nv := range renamedServices { - _, ook := sset[ov] - _, nok := sset[nv] - - // preserve the values set in prior state if they're identical. If none - // were set, we delete the new value if it exists. By doing that that - // we only store the old value if the service is enabled, and no value - // if it isn't. - if ook && nok { - continue - } else if ook { - delete(enabledSet, nv) - } else if nok { - delete(enabledSet, ov) - } else { - delete(enabledSet, nv) - } - } - - services := stringSliceFromGolangSet(enabledSet) - - d.Set("project", d.Id()) - d.Set("services", flattenServiceUsageProjectServicesServices(services, d)) - - return nil -} - -func resourceGoogleProjectServicesDelete(d *schema.ResourceData, meta interface{}) error { - if disable := d.Get("disable_on_destroy"); !(disable.(bool)) { - log.Printf("[WARN] Project Services disable_on_destroy set to false, skip disabling services for %s.", d.Id()) - d.SetId("") - return nil - } - - config := meta.(*Config) - - // Get services from config - services, err := expandServiceUsageProjectServicesServices(d.Get("services"), d, config) - if err != nil { - return err - } - project := d.Id() - - log.Printf("[DEBUG]: Disabling Project Services %s: %+v", project, services) - for _, s := range services { - if err := disableServiceUsageProjectService(s, project, d, config, true); err != nil { - return fmt.Errorf("Unable to destroy google_project_services for %s: %s", d.Id(), err) - } - } - log.Printf("[DEBUG] Finished disabling Project Services %s: %+v", project, services) - - d.SetId("") - return nil -} - -// *Authoritatively* sets enabled services. -func setServiceUsageProjectEnabledServices(services []string, project string, d *schema.ResourceData, config *Config) error { - currentlyEnabled, err := listCurrentlyEnabledServices(project, config, d.Timeout(schema.TimeoutRead)) - if err != nil { - return err - } - - toEnable := map[string]struct{}{} - for _, srv := range services { - // We don't have to enable a service if it's already enabled. - if _, ok := currentlyEnabled[srv]; !ok { - toEnable[srv] = struct{}{} - } - } - - if len(toEnable) > 0 { - log.Printf("[DEBUG] Enabling services: %s", toEnable) - if err := BatchRequestEnableServices(toEnable, project, d, config); err != nil { - return fmt.Errorf("unable to enable Project Services %s (%+v): %s", project, services, err) - } - } else { - log.Printf("[DEBUG] No services to enable.") - } - - srvSet := golangSetFromStringSlice(services) - - srvSetWithRenames := map[string]struct{}{} - - // we'll always list both names for renamed services, so allow both forms if - // we see both. - for k := range srvSet { - srvSetWithRenames[k] = struct{}{} - if v, ok := renamedServicesByOldAndNewServiceNames[k]; ok { - srvSetWithRenames[v] = struct{}{} - } - } - - for srv := range currentlyEnabled { - // Disable any services that are currently enabled for project but are not - // in our list of acceptable services. - if _, ok := srvSetWithRenames[srv]; !ok { - // skip deleting services by their new names and prefer the old name. - if _, ok := renamedServicesByNewServiceNames[srv]; ok { - continue - } - - log.Printf("[DEBUG] Disabling project %s service %s", project, srv) - err := disableServiceUsageProjectService(srv, project, d, config, true) - if err != nil { - log.Printf("[DEBUG] Saw error %s deleting service %s", err, srv) - - // if we got the right error and the service is renamed, delete by the new name - if n, ok := renamedServices[srv]; ok && strings.Contains(err.Error(), "not found or permission denied.") { - log.Printf("[DEBUG] Failed to delete service %s, it doesn't exist. Trying %s", srv, n) - err = disableServiceUsageProjectService(n, project, d, config, true) - if err == nil { - return nil - } - } - - return fmt.Errorf("unable to disable unwanted Project Service %s %s): %s", project, srv, err) - } - } - } - return nil -} - -func doEnableServicesRequest(services []string, project string, config *Config, timeout time.Duration) error { - var op *serviceusage.Operation - - err := retryTimeDuration(func() error { - var rerr error - if len(services) == 1 { - // BatchEnable returns an error for a single item, so just enable - // using service endpoint. - name := fmt.Sprintf("projects/%s/services/%s", project, services[0]) - req := &serviceusage.EnableServiceRequest{} - op, rerr = config.clientServiceUsage.Services.Enable(name, req).Do() - } else { - // Batch enable for multiple services. - name := fmt.Sprintf("projects/%s", project) - req := &serviceusage.BatchEnableServicesRequest{ServiceIds: services} - op, rerr = config.clientServiceUsage.Services.BatchEnable(name, req).Do() - } - return handleServiceUsageRetryableError(rerr) - }, timeout) - if err != nil { - return errwrap.Wrapf("failed to send enable services request: {{err}}", err) - } - - // Poll for the API to return - waitErr := serviceUsageOperationWait(config, op, fmt.Sprintf("Enable Project %q Services: %+v", project, services)) - if waitErr != nil { - return waitErr - } - return nil -} - -func handleServiceUsageRetryableError(err error) error { - if err == nil { - return nil - } - if gerr, ok := err.(*googleapi.Error); ok { - if (gerr.Code == 400 || gerr.Code == 412) && gerr.Message == "Precondition check failed." { - return &googleapi.Error{ - Code: 503, - Message: "api returned \"precondition failed\" while enabling service", - } - } - } - return err -} - -func flattenServiceUsageProjectServicesServices(v interface{}, d *schema.ResourceData) interface{} { - if v == nil { - return v - } - if strV, ok := v.([]string); ok { - v = convertStringArrToInterface(strV) - } - return schema.NewSet(schema.HashString, v.([]interface{})) -} - -func expandServiceUsageProjectServicesServices(v interface{}, d TerraformResourceData, config *Config) ([]string, error) { - if v == nil { - return nil, nil - } - return convertStringArr(v.(*schema.Set).List()), nil -} - -// Retrieve a project's services from the API -// if a service has been renamed, this function will list both the old and new -// forms of the service. LIST responses are expected to return only the old or -// new form, but we'll always return both. -func listCurrentlyEnabledServices(project string, config *Config, timeout time.Duration) (map[string]struct{}, error) { - // Verify project for services still exists - p, err := config.clientResourceManager.Projects.Get(project).Do() - if err != nil { - return nil, err - } - if p.LifecycleState == "DELETE_REQUESTED" { - // Construct a 404 error for handleNotFoundError - return nil, &googleapi.Error{ - Code: 404, - Message: "Project deletion was requested", - } - } - - log.Printf("[DEBUG] Listing enabled services for project %s", project) - apiServices := make(map[string]struct{}) - err = retryTimeDuration(func() error { - ctx := context.Background() - return config.clientServiceUsage.Services. - List(fmt.Sprintf("projects/%s", project)). - Fields("services/name,nextPageToken"). - Filter("state:ENABLED"). - Pages(ctx, func(r *serviceusage.ListServicesResponse) error { - for _, v := range r.Services { - // services are returned as "projects/{{project}}/services/{{name}}" - name := GetResourceNameFromSelfLink(v.Name) - - // if name not in ignoredProjectServicesSet - if _, ok := ignoredProjectServicesSet[name]; !ok { - apiServices[name] = struct{}{} - - // if a service has been renamed, set both. We'll deal - // with setting the right values later. - if v, ok := renamedServicesByOldAndNewServiceNames[name]; ok { - log.Printf("[DEBUG] Adding service alias for %s to enabled services: %s", name, v) - apiServices[v] = struct{}{} - } - } - } - return nil - }) - }, timeout) - if err != nil { - return nil, errwrap.Wrapf(fmt.Sprintf("Failed to list enabled services for project %s: {{err}}", project), err) - } - return apiServices, nil -} diff --git a/third_party/terraform/tests/data_source_google_project_services_test.go b/third_party/terraform/tests/data_source_google_project_services_test.go deleted file mode 100644 index 30ff56a849f5..000000000000 --- a/third_party/terraform/tests/data_source_google_project_services_test.go +++ /dev/null @@ -1,53 +0,0 @@ -package google - -import ( - "fmt" - "testing" - - "github.com/hashicorp/terraform-plugin-sdk/helper/acctest" - "github.com/hashicorp/terraform-plugin-sdk/helper/resource" -) - -func TestAccDataSourceGoogleProjectServices_basic(t *testing.T) { - t.Parallel() - org := getTestOrgFromEnv(t) - project := "terraform-" + acctest.RandString(10) - - resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, - Steps: []resource.TestStep{ - { - Config: testAccCheckGoogleProjectServicesConfig(project, org), - Check: resource.ComposeTestCheckFunc( - checkDataSourceStateMatchesResourceStateWithIgnores( - "data.google_project_services.project_services", - "google_project_services.project_services", - map[string]struct{}{ - // Virtual fields - "disable_on_destroy": {}, - }, - ), - ), - }, - }, - }) -} - -func testAccCheckGoogleProjectServicesConfig(project, org string) string { - return fmt.Sprintf(` -resource "google_project" "project" { - project_id = "%s" - name = "%s" - org_id = "%s" -} - -resource "google_project_services" "project_services" { - project = "${google_project.project.project_id}" - services = ["admin.googleapis.com"] -} - -data "google_project_services" "project_services" { - project = "${google_project_services.project_services.project}" -}`, project, project, org) -} diff --git a/third_party/terraform/tests/resource_google_project_services_test.go b/third_party/terraform/tests/resource_google_project_services_test.go deleted file mode 100644 index 3b351a1cef0c..000000000000 --- a/third_party/terraform/tests/resource_google_project_services_test.go +++ /dev/null @@ -1,440 +0,0 @@ -package google - -import ( - "bytes" - "fmt" - "reflect" - "sort" - "testing" - "time" - - "github.com/hashicorp/terraform-plugin-sdk/helper/acctest" - "github.com/hashicorp/terraform-plugin-sdk/helper/resource" - "github.com/hashicorp/terraform-plugin-sdk/terraform" -) - -// Test that services can be enabled and disabled on a project -func TestAccProjectServices_basic(t *testing.T) { - t.Parallel() - - org := getTestOrgFromEnv(t) - pid := "terraform-" + acctest.RandString(10) - services1 := []string{"logging.googleapis.com", "cloudresourcemanager.googleapis.com"} - services2 := []string{"cloudresourcemanager.googleapis.com"} - oobService := "logging.googleapis.com" - resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, - Steps: []resource.TestStep{ - // Create a new project with some services - { - Config: testAccProjectAssociateServicesBasic(services1, pid, pname, org), - Check: resource.ComposeTestCheckFunc( - testProjectServicesMatch(services1, pid), - ), - }, - // Update services to remove one - { - Config: testAccProjectAssociateServicesBasic(services2, pid, pname, org), - Check: resource.ComposeTestCheckFunc( - testProjectServicesMatch(services2, pid), - ), - }, - // Add a service out-of-band and ensure it is removed - { - PreConfig: func() { - config := testAccProvider.Meta().(*Config) - if err := enableServiceUsageProjectServices([]string{oobService}, pid, config, time.Minute*20); err != nil { - t.Fatalf("Error enabling %q: %v", oobService, err) - } - }, - Config: testAccProjectAssociateServicesBasic(services2, pid, pname, org), - Check: resource.ComposeTestCheckFunc( - testProjectServicesMatch(services2, pid), - ), - }, - { - ResourceName: "google_project_services.acceptance", - ImportState: true, - ImportStateId: pid, - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"disable_on_destroy"}, - }, - }, - }) -} - -// Test that services are authoritative when a project has existing -// services not represented in config -func TestAccProjectServices_authoritative(t *testing.T) { - t.Parallel() - - org := getTestOrgFromEnv(t) - pid := "terraform-" + acctest.RandString(10) - services := []string{"cloudresourcemanager.googleapis.com"} - oobService := "logging.googleapis.com" - resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, - Steps: []resource.TestStep{ - // Create a new project with no services - { - Config: testAccProject_create(pid, pname, org), - Check: resource.ComposeTestCheckFunc( - testAccCheckGoogleProjectExists("google_project.acceptance", pid), - ), - }, - // Add a service out-of-band, then apply a config that creates a service. - // It should remove the out-of-band service. - { - PreConfig: func() { - config := testAccProvider.Meta().(*Config) - if err := enableServiceUsageProjectServices([]string{oobService}, pid, config, time.Minute*20); err != nil { - t.Fatalf("Error enabling %q: %v", oobService, err) - } - }, - Config: testAccProjectAssociateServicesBasic(services, pid, pname, org), - Check: resource.ComposeTestCheckFunc( - testProjectServicesMatch(services, pid), - ), - }, - }, - }) -} - -// Test that services are authoritative when a project has existing -// services, some which are represented in the config and others -// that are not -func TestAccProjectServices_authoritative2(t *testing.T) { - t.Parallel() - - org := getTestOrgFromEnv(t) - pid := "terraform-" + acctest.RandString(10) - oobServices := []string{"logging.googleapis.com", "cloudresourcemanager.googleapis.com"} - services := []string{"logging.googleapis.com"} - - resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, - Steps: []resource.TestStep{ - // Create a new project with no services - { - Config: testAccProject_create(pid, pname, org), - Check: resource.ComposeTestCheckFunc( - testAccCheckGoogleProjectExists("google_project.acceptance", pid), - ), - }, - // Add a service out-of-band, then apply a config that creates a service. - // It should remove the out-of-band service. - { - PreConfig: func() { - config := testAccProvider.Meta().(*Config) - for _, s := range oobServices { - if err := enableServiceUsageProjectServices([]string{s}, pid, config, time.Minute*20); err != nil { - t.Fatalf("Error enabling %q: %v", s, err) - } - } - }, - Config: testAccProjectAssociateServicesBasic(services, pid, pname, org), - Check: resource.ComposeTestCheckFunc( - testProjectServicesMatch(services, pid), - ), - }, - }, - }) -} - -// Test that services that can't be enabled on their own (such as dataproc-control.googleapis.com) -// don't end up causing diffs when they are enabled as a side-effect of a different service's -// enablement. -func TestAccProjectServices_ignoreUnenablableServices(t *testing.T) { - t.Parallel() - - org := getTestOrgFromEnv(t) - billingId := getTestBillingAccountFromEnv(t) - pid := "terraform-" + acctest.RandString(10) - services := []string{ - "dataproc.googleapis.com", - // The following services are enabled as a side-effect of dataproc's enablement - "storage-component.googleapis.com", - "deploymentmanager.googleapis.com", - "replicapool.googleapis.com", - "replicapoolupdater.googleapis.com", - "resourceviews.googleapis.com", - "compute.googleapis.com", - "container.googleapis.com", - "containerregistry.googleapis.com", - "storage-api.googleapis.com", - "pubsub.googleapis.com", - "oslogin.googleapis.com", - "bigquery-json.googleapis.com", - "bigquerystorage.googleapis.com", - "iam.googleapis.com", - "iamcredentials.googleapis.com", - } - - resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, - Steps: []resource.TestStep{ - { - Config: testAccProjectAssociateServicesBasic_withBilling(services, pid, pname, org, billingId), - Check: resource.ComposeTestCheckFunc(testProjectServicesMatch(services, pid)), - }, - }, - }) -} - -func TestAccProjectServices_pagination(t *testing.T) { - t.Parallel() - - org := getTestOrgFromEnv(t) - billingId := getTestBillingAccountFromEnv(t) - pid := "terraform-" + acctest.RandString(10) - - // we need at least 50 services (doesn't matter what they are) to exercise the - // pagination handling code. - services := []string{ - "actions.googleapis.com", - "appengine.googleapis.com", - "appengineflex.googleapis.com", - "bigquery-json.googleapis.com", - "bigquerydatatransfer.googleapis.com", - "bigquerystorage.googleapis.com", - "bigtableadmin.googleapis.com", - "bigtabletableadmin.googleapis.com", - "cloudbuild.googleapis.com", - "clouderrorreporting.googleapis.com", - "cloudfunctions.googleapis.com", - "cloudiot.googleapis.com", - "cloudkms.googleapis.com", - "cloudmonitoring.googleapis.com", - "cloudresourcemanager.googleapis.com", - "cloudtrace.googleapis.com", - "compute.googleapis.com", - "container.googleapis.com", - "containerregistry.googleapis.com", - "dataflow.googleapis.com", - "dataproc.googleapis.com", - "datastore.googleapis.com", - "deploymentmanager.googleapis.com", - "dialogflow.googleapis.com", - "dns.googleapis.com", - "endpoints.googleapis.com", - "firebaserules.googleapis.com", - "firestore.googleapis.com", - "genomics.googleapis.com", - "iam.googleapis.com", - "iamcredentials.googleapis.com", - "language.googleapis.com", - "logging.googleapis.com", - "ml.googleapis.com", - "monitoring.googleapis.com", - "oslogin.googleapis.com", - "pubsub.googleapis.com", - "replicapool.googleapis.com", - "replicapoolupdater.googleapis.com", - "resourceviews.googleapis.com", - "runtimeconfig.googleapis.com", - "servicecontrol.googleapis.com", - "servicemanagement.googleapis.com", - "sourcerepo.googleapis.com", - "spanner.googleapis.com", - "speech.googleapis.com", - "sql-component.googleapis.com", - "storage-api.googleapis.com", - "storage-component.googleapis.com", - "storagetransfer.googleapis.com", - "testing.googleapis.com", - "toolresults.googleapis.com", - "translate.googleapis.com", - "videointelligence.googleapis.com", - "vision.googleapis.com", - "zync.googleapis.com", - } - - resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, - Steps: []resource.TestStep{ - { - Config: testAccProjectAssociateServicesBasic_withBilling(services, pid, pname, org, billingId), - Check: resource.ComposeTestCheckFunc( - testProjectServicesMatch(services, pid), - ), - }, - }, - }) -} - -func TestAccProjectServices_renamedServices(t *testing.T) { - t.Parallel() - - org := getTestOrgFromEnv(t) - pid := "terraform-" + acctest.RandString(10) - resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, - Steps: []resource.TestStep{ - { - // create new - Config: testAccProjectAssociateServicesBasic([]string{ - "bigquery.googleapis.com", - "bigquerystorage.googleapis.com", - "iam.googleapis.com", - "iamcredentials.googleapis.com", - "oslogin.googleapis.com", - }, pid, pname, org), - }, - { - // transition to old - Config: testAccProjectAssociateServicesBasic([]string{ - "bigquery-json.googleapis.com", - "bigquerystorage.googleapis.com", - "iam.googleapis.com", - "iamcredentials.googleapis.com", - "oslogin.googleapis.com", - }, pid, pname, org), - }, - { - // transition to new - Config: testAccProjectAssociateServicesBasic([]string{ - "bigquery.googleapis.com", - "bigquerystorage.googleapis.com", - "iam.googleapis.com", - "iamcredentials.googleapis.com", - "oslogin.googleapis.com", - }, pid, pname, org), - }, - { - // remove new - Config: testAccProjectAssociateServicesBasic([]string{ - "iam.googleapis.com", - "iamcredentials.googleapis.com", - "oslogin.googleapis.com", - }, pid, pname, org), - }, - { - // create both - Config: testAccProjectAssociateServicesBasic([]string{ - "bigquery.googleapis.com", - "bigquery-json.googleapis.com", - "bigquerystorage.googleapis.com", - "iam.googleapis.com", - "iamcredentials.googleapis.com", - "oslogin.googleapis.com", - }, pid, pname, org), - }, - { - // remove new - Config: testAccProjectAssociateServicesBasic([]string{ - "bigquery-json.googleapis.com", - "bigquerystorage.googleapis.com", - "iam.googleapis.com", - "iamcredentials.googleapis.com", - "oslogin.googleapis.com", - }, pid, pname, org), - }, - { - // import imports old - ResourceName: "google_project_services.acceptance", - ImportState: true, - ImportStateId: pid, - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"disable_on_destroy"}, - }, - { - // transition to both - Config: testAccProjectAssociateServicesBasic([]string{ - "bigquery.googleapis.com", - "bigquery-json.googleapis.com", - "bigquerystorage.googleapis.com", - "iam.googleapis.com", - "iamcredentials.googleapis.com", - "oslogin.googleapis.com", - }, pid, pname, org), - }, - { - // remove both - Config: testAccProjectAssociateServicesBasic([]string{ - "iam.googleapis.com", - "iamcredentials.googleapis.com", - "oslogin.googleapis.com", - }, pid, pname, org), - }, - }, - }) -} - -func testAccProjectAssociateServicesBasic(services []string, pid, name, org string) string { - return fmt.Sprintf(` -resource "google_project" "acceptance" { - project_id = "%s" - name = "%s" - org_id = "%s" -} -resource "google_project_services" "acceptance" { - project = "${google_project.acceptance.project_id}" - services = [%s] - disable_on_destroy = true -} -`, pid, name, org, testStringsToString(services)) -} - -func testAccProjectAssociateServicesBasic_withBilling(services []string, pid, name, org, billing string) string { - return fmt.Sprintf(` -resource "google_project" "acceptance" { - project_id = "%s" - name = "%s" - org_id = "%s" - billing_account = "%s" -} -resource "google_project_services" "acceptance" { - project = "${google_project.acceptance.project_id}" - services = [%s] - disable_on_destroy = false -} -`, pid, name, org, billing, testStringsToString(services)) -} - -func testProjectServicesMatch(services []string, pid string) resource.TestCheckFunc { - return func(s *terraform.State) error { - config := testAccProvider.Meta().(*Config) - - currentlyEnabled, err := listCurrentlyEnabledServices(pid, config, time.Minute*10) - if err != nil { - return fmt.Errorf("Error listing services for project %q: %v", pid, err) - } - - servicesSet := golangSetFromStringSlice(services) - // add renamed service aliases because listCurrentlyEnabledServices will - // have both - for k := range servicesSet { - if v, ok := renamedServicesByOldAndNewServiceNames[k]; ok { - servicesSet[v] = struct{}{} - } - } - - services = stringSliceFromGolangSet(servicesSet) - - apiServices := stringSliceFromGolangSet(currentlyEnabled) - sort.Strings(services) - sort.Strings(apiServices) - if !reflect.DeepEqual(services, apiServices) { - return fmt.Errorf("Services in config (%v) do not exactly match services returned by API (%v)", services, apiServices) - } - - return nil - } -} - -func testStringsToString(s []string) string { - var b bytes.Buffer - for i, v := range s { - b.WriteString(fmt.Sprintf("\"%s\"", v)) - if i < len(s)-1 { - b.WriteString(",") - } - } - return b.String() -} diff --git a/third_party/terraform/tests/resource_storage_bucket_test.go b/third_party/terraform/tests/resource_storage_bucket_test.go index 94e2081522e4..acbd3ca2818c 100644 --- a/third_party/terraform/tests/resource_storage_bucket_test.go +++ b/third_party/terraform/tests/resource_storage_bucket_test.go @@ -1421,17 +1421,14 @@ resource "google_project" "acceptance" { billing_account = "%{billing_account}" } -resource "google_project_services" "acceptance" { +resource "google_project_service" "acceptance" { project = "${google_project.acceptance.project_id}" - - services = [ - "cloudkms.googleapis.com", - ] + service = "cloudkms.googleapis.com" } resource "google_kms_key_ring" "key_ring" { name = "tf-test-%{random_suffix}" - project = "${google_project_services.acceptance.project}" + project = "${google_project_service.acceptance.project}" location = "us" } diff --git a/third_party/terraform/utils/batcher.go b/third_party/terraform/utils/batcher.go index 12a3ac886088..f017c35f1fd9 100644 --- a/third_party/terraform/utils/batcher.go +++ b/third_party/terraform/utils/batcher.go @@ -123,7 +123,7 @@ func (b *RequestBatcher) stop() { // may choose to use a key with method if needed to diff GET/read and // POST/create) // -// As an example, for google_project_service and google_project_services, the +// As an example, for google_project_service, the // batcher is called to batch services.batchEnable() calls for a project // $PROJECT. The calling code uses the template // "serviceusage:projects/$PROJECT/services:batchEnable", which mirrors the HTTP request: diff --git a/third_party/terraform/utils/provider.go.erb b/third_party/terraform/utils/provider.go.erb index fc9fea327572..1b1e6ac611e6 100644 --- a/third_party/terraform/utils/provider.go.erb +++ b/third_party/terraform/utils/provider.go.erb @@ -190,7 +190,6 @@ func Provider() terraform.ResourceProvider { "google_project": dataSourceGoogleProject(), "google_projects": dataSourceGoogleProjects(), "google_project_organization_policy": dataSourceGoogleProjectOrganizationPolicy(), - "google_project_services": dataSourceGoogleProjectServices(), "google_service_account": dataSourceGoogleServiceAccount(), "google_service_account_access_token": dataSourceGoogleServiceAccountAccessToken(), "google_service_account_key": dataSourceGoogleServiceAccountKey(), @@ -378,7 +377,6 @@ end # products.each do "google_project_iam_custom_role": resourceGoogleProjectIamCustomRole(), "google_project_organization_policy": resourceGoogleProjectOrganizationPolicy(), "google_project_usage_export_bucket": resourceProjectUsageBucket(), - "google_project_services": resourceGoogleProjectServices(), "google_pubsub_subscription_iam_binding": ResourceIamBinding(IamPubsubSubscriptionSchema, NewPubsubSubscriptionIamUpdater, PubsubSubscriptionIdParseFunc), "google_pubsub_subscription_iam_member": ResourceIamMember(IamPubsubSubscriptionSchema, NewPubsubSubscriptionIamUpdater, PubsubSubscriptionIdParseFunc), "google_pubsub_subscription_iam_policy": ResourceIamPolicy(IamPubsubSubscriptionSchema, NewPubsubSubscriptionIamUpdater, PubsubSubscriptionIdParseFunc), diff --git a/third_party/terraform/utils/serviceusage_operation.go b/third_party/terraform/utils/serviceusage_operation.go index 7eea8347d2ad..7e4794fc49ab 100644 --- a/third_party/terraform/utils/serviceusage_operation.go +++ b/third_party/terraform/utils/serviceusage_operation.go @@ -3,6 +3,7 @@ package google import ( "fmt" + "google.golang.org/api/googleapi" "google.golang.org/api/serviceusage/v1" ) @@ -37,3 +38,18 @@ func serviceUsageOperationWaitTime(config *Config, op *serviceusage.Operation, a } return OperationWait(w, activity, timeoutMinutes) } + +func handleServiceUsageRetryableError(err error) error { + if err == nil { + return nil + } + if gerr, ok := err.(*googleapi.Error); ok { + if (gerr.Code == 400 || gerr.Code == 412) && gerr.Message == "Precondition check failed." { + return &googleapi.Error{ + Code: 503, + Message: "api returned \"precondition failed\" while enabling service", + } + } + } + return err +} diff --git a/third_party/terraform/website-compiled/google.erb b/third_party/terraform/website-compiled/google.erb index 6b71dc6033da..9eeea5fa2e05 100644 --- a/third_party/terraform/website-compiled/google.erb +++ b/third_party/terraform/website-compiled/google.erb @@ -178,9 +178,6 @@ > google_project_organization_policy - > - google_project_services - > google_service_account @@ -431,9 +428,6 @@ > google_project_service - > - google_project_services - > google_project_usage_export_bucket diff --git a/third_party/terraform/website/docs/d/google_project_services.html.markdown b/third_party/terraform/website/docs/d/google_project_services.html.markdown deleted file mode 100644 index 6b0e1765c237..000000000000 --- a/third_party/terraform/website/docs/d/google_project_services.html.markdown +++ /dev/null @@ -1,40 +0,0 @@ ---- -subcategory: "Cloud Platform" -layout: "google" -page_title: "Google: google_project_services" -sidebar_current: "docs-google-datasource-project-services" -description: |- - Retrieve enabled of API services for a Google Cloud Platform project ---- - -# google\_project\_services - -Use this data source to get details on the enabled project services. - -For a list of services available, visit the -[API library page](https://console.cloud.google.com/apis/library) or run `gcloud services list`. - -## Example Usage - -```hcl -data "google_project_services" "project" { - project = "your-project-id" -} - -output "project_services" { - value = "${join(",", data.google_project_services.project.services)}" -} -``` - -## Argument Reference - -The following arguments are supported: - -* `project` - (Required) The project ID. - - -## Attributes Reference - -The following attributes are exported: - -See [google_project_services](https://www.terraform.io/docs/providers/google/r/google_project_services.html) resource for details of the available attributes. diff --git a/third_party/terraform/website/docs/guides/provider_reference.html.markdown b/third_party/terraform/website/docs/guides/provider_reference.html.markdown index 8344f7c1fde3..45336128e233 100644 --- a/third_party/terraform/website/docs/guides/provider_reference.html.markdown +++ b/third_party/terraform/website/docs/guides/provider_reference.html.markdown @@ -318,8 +318,7 @@ as their versioned counterpart but that won't necessarily always be the case. **So far, batching is implemented for**: -* enabling project services using `google_project_service` or - `google_project_services` +* enabling project services using `google_project_service`. The `batching` block supports the following fields. diff --git a/third_party/terraform/website/docs/r/google_project_service.html.markdown b/third_party/terraform/website/docs/r/google_project_service.html.markdown index c4f22cc4fa3f..6361c32fba93 100644 --- a/third_party/terraform/website/docs/r/google_project_service.html.markdown +++ b/third_party/terraform/website/docs/r/google_project_service.html.markdown @@ -14,9 +14,6 @@ Allows management of a single API service for an existing Google Cloud Platform For a list of services available, visit the [API library page](https://console.cloud.google.com/apis/library) or run `gcloud services list`. -~> **Note:** This resource _must not_ be used in conjunction with - `google_project_services` or they will fight over which services should be enabled. - ## Example Usage ```hcl diff --git a/third_party/terraform/website/docs/r/google_project_services.html.markdown b/third_party/terraform/website/docs/r/google_project_services.html.markdown deleted file mode 100644 index f685b10dfccc..000000000000 --- a/third_party/terraform/website/docs/r/google_project_services.html.markdown +++ /dev/null @@ -1,55 +0,0 @@ ---- -subcategory: "Cloud Platform" -layout: "google" -page_title: "Google: google_project_services" -sidebar_current: "docs-google-project-services" -description: |- - Allows management of API services for a Google Cloud Platform project. ---- - -# google\_project\_services - -Allows management of enabled API services for an existing Google Cloud -Platform project. Services in an existing project that are not defined -in the config will be removed. - -For a list of services available, visit the -[API library page](https://console.cloud.google.com/apis/library) or run `gcloud services list`. - -~> **Note:** This resource attempts to be the authoritative source on *all* enabled APIs, which often - leads to conflicts when certain actions enable other APIs. If you do not need to ensure that - *exclusively* a particular set of APIs are enabled, you should most likely use the - [google_project_service](google_project_service.html) resource, one resource per API. - -## Example Usage - -```hcl -resource "google_project_services" "project" { - project = "your-project-id" - services = ["iam.googleapis.com", "cloudresourcemanager.googleapis.com"] -} -``` - -## Argument Reference - -The following arguments are supported: - -* `project` - (Required) The project ID. - Changing this forces Terraform to attempt to disable all previously managed - API services in the previous project. - -* `services` - (Required) The list of services that are enabled. Supports - update. - -* `disable_on_destroy` - (Optional) Whether or not to disable APIs on project - when destroyed. Defaults to true. **Note**: When `disable_on_destroy` is - true and the project is changed, Terraform will force disable API services - managed by Terraform for the previous project. - -## Import - -Project services can be imported using the `project_id`, e.g. - -``` -$ terraform import google_project_services.my_project your-project-id -```