diff --git a/.changelog/5215.txt b/.changelog/5215.txt new file mode 100644 index 00000000000..aacdd1b0ec8 --- /dev/null +++ b/.changelog/5215.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +project: support `billing_project` for `google_project_service` +``` diff --git a/google/resource_google_project.go b/google/resource_google_project.go index e2bf8d49486..7e22648651f 100644 --- a/google/resource_google_project.go +++ b/google/resource_google_project.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "log" + "net/http" "regexp" "strconv" "strings" @@ -17,6 +18,11 @@ import ( "google.golang.org/api/serviceusage/v1" ) +type ServicesCall interface { + Header() http.Header + Do(opts ...googleapi.CallOption) (*serviceusage.Operation, error) +} + // resourceGoogleProject returns a *schema.Resource that allows a customer // to declare a Google Cloud Project resource. func resourceGoogleProject() *schema.Resource { @@ -179,7 +185,14 @@ func resourceGoogleProjectCreate(d *schema.ResourceData, meta interface{}) error // a network and deleting it in the background. if !d.Get("auto_create_network").(bool) { // The compute API has to be enabled before we can delete a network. - if err = enableServiceUsageProjectServices([]string{"compute.googleapis.com"}, project.ProjectId, userAgent, config, d.Timeout(schema.TimeoutCreate)); err != nil { + + billingProject := project.ProjectId + // err == nil indicates that the billing_project value was found + if bp, err := getBillingProject(d, config); err == nil { + billingProject = bp + } + + if err = enableServiceUsageProjectServices([]string{"compute.googleapis.com"}, project.ProjectId, billingProject, userAgent, config, d.Timeout(schema.TimeoutCreate)); err != nil { return errwrap.Wrapf("Error enabling the Compute Engine API required to delete the default network: {{err}} ", err) } @@ -212,7 +225,13 @@ func resourceGoogleProjectCheckPreRequisites(config *Config, d *schema.ResourceD return fmt.Errorf("missing permission on %q: %v", ba, perm) } if !d.Get("auto_create_network").(bool) { - _, err := config.NewServiceUsageClient(userAgent).Services.Get("projects/00000000000/services/serviceusage.googleapis.com").Do() + call := config.NewServiceUsageClient(userAgent).Services.Get("projects/00000000000/services/serviceusage.googleapis.com") + if config.UserProjectOverride { + if billingProject, err := getBillingProject(d, config); err == nil { + call.Header().Add("X-Goog-User-Project", billingProject) + } + } + _, err := call.Do() switch { // We are querying a dummy project since the call is already coming from the quota project. // If the API is enabled we get a not found message or accessNotConfigured if API is not enabled. @@ -580,7 +599,7 @@ func readGoogleProject(d *schema.ResourceData, config *Config, userAgent string) } // Enables services. WARNING: Use BatchRequestEnableServices for better batching if possible. -func enableServiceUsageProjectServices(services []string, project, userAgent string, config *Config, timeout time.Duration) error { +func enableServiceUsageProjectServices(services []string, project, billingProject, userAgent string, config *Config, timeout time.Duration) error { // ServiceUsage does not allow more than 20 services to be enabled per // batchEnable API call. See // https://cloud.google.com/service-usage/docs/reference/rest/v1/services/batchEnable @@ -595,19 +614,19 @@ func enableServiceUsageProjectServices(services []string, project, userAgent str return nil } - if err := doEnableServicesRequest(nextBatch, project, userAgent, config, timeout); err != nil { + if err := doEnableServicesRequest(nextBatch, project, billingProject, userAgent, config, timeout); err != nil { return err } log.Printf("[DEBUG] Finished enabling next batch of %d project services: %+v", len(nextBatch), nextBatch) } log.Printf("[DEBUG] Verifying that all services are enabled") - return waitForServiceUsageEnabledServices(services, project, userAgent, config, timeout) + return waitForServiceUsageEnabledServices(services, project, billingProject, userAgent, config, timeout) } -func doEnableServicesRequest(services []string, project, userAgent string, config *Config, timeout time.Duration) error { +func doEnableServicesRequest(services []string, project, billingProject, userAgent string, config *Config, timeout time.Duration) error { var op *serviceusage.Operation - + var call ServicesCall err := retryTimeDuration(func() error { var rerr error if len(services) == 1 { @@ -615,20 +634,27 @@ func doEnableServicesRequest(services []string, project, userAgent string, confi // using service endpoint. name := fmt.Sprintf("projects/%s/services/%s", project, services[0]) req := &serviceusage.EnableServiceRequest{} - op, rerr = config.NewServiceUsageClient(userAgent).Services.Enable(name, req).Do() + call = config.NewServiceUsageClient(userAgent).Services.Enable(name, req) } else { // Batch enable for multiple services. name := fmt.Sprintf("projects/%s", project) req := &serviceusage.BatchEnableServicesRequest{ServiceIds: services} - op, rerr = config.NewServiceUsageClient(userAgent).Services.BatchEnable(name, req).Do() + call = config.NewServiceUsageClient(userAgent).Services.BatchEnable(name, req) } + if config.UserProjectOverride && billingProject != "" { + call.Header().Add("X-Goog-User-Project", billingProject) + } + op, rerr = call.Do() return handleServiceUsageRetryableError(rerr) - }, timeout, serviceUsageServiceBeingActivated) + }, + timeout, + serviceUsageServiceBeingActivated, + ) if err != nil { return errwrap.Wrapf("failed to send enable services request: {{err}}", err) } // Poll for the API to return - waitErr := serviceUsageOperationWait(config, op, project, fmt.Sprintf("Enable Project %q Services: %+v", project, services), userAgent, timeout) + waitErr := serviceUsageOperationWait(config, op, billingProject, fmt.Sprintf("Enable Project %q Services: %+v", project, services), userAgent, timeout) if waitErr != nil { return waitErr } @@ -639,15 +665,16 @@ func doEnableServicesRequest(services []string, project, userAgent string, confi // 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, userAgent string, config *Config, timeout time.Duration) (map[string]struct{}, error) { +func listCurrentlyEnabledServices(project, billingProject, userAgent string, config *Config, timeout time.Duration) (map[string]struct{}, error) { log.Printf("[DEBUG] Listing enabled services for project %s", project) apiServices := make(map[string]struct{}) err := retryTimeDuration(func() error { ctx := context.Background() - return config.NewServiceUsageClient(userAgent).Services. - List(fmt.Sprintf("projects/%s", project)). - Fields("services/name,nextPageToken"). - Filter("state:ENABLED"). + call := config.NewServiceUsageClient(userAgent).Services.List(fmt.Sprintf("projects/%s", project)) + if config.UserProjectOverride && billingProject != "" { + call.Header().Add("X-Goog-User-Project", billingProject) + } + return call.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}}" @@ -677,13 +704,13 @@ func listCurrentlyEnabledServices(project, userAgent string, config *Config, tim // 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. -func waitForServiceUsageEnabledServices(services []string, project, userAgent string, config *Config, timeout time.Duration) error { +func waitForServiceUsageEnabledServices(services []string, project, billingProject, userAgent string, config *Config, timeout time.Duration) error { missing := make([]string, 0, len(services)) delay := time.Duration(0) interval := time.Second err := retryTimeDuration(func() error { // Get the list of services that are enabled on the project - enabledServices, err := listCurrentlyEnabledServices(project, userAgent, config, timeout) + enabledServices, err := listCurrentlyEnabledServices(project, billingProject, userAgent, config, timeout) if err != nil { return err } diff --git a/google/resource_google_project_service.go b/google/resource_google_project_service.go index 0c910a78fc6..631c20d8e1d 100644 --- a/google/resource_google_project_service.go +++ b/google/resource_google_project_service.go @@ -185,7 +185,13 @@ func resourceGoogleProjectServiceRead(d *schema.ResourceData, meta interface{}) // Verify project for services still exists projectGetCall := config.NewResourceManagerClient(userAgent).Projects.Get(project) if config.UserProjectOverride { - projectGetCall.Header().Add("X-Goog-User-Project", project) + billingProject := project + + // err == nil indicates that the billing_project value was found + if bp, err := getBillingProject(d, config); err == nil { + billingProject = bp + } + projectGetCall.Header().Add("X-Goog-User-Project", billingProject) } p, err := projectGetCall.Do() @@ -256,24 +262,28 @@ func resourceGoogleProjectServiceUpdate(d *schema.ResourceData, meta interface{} // Disables a project service. func disableServiceUsageProjectService(service, project string, d *schema.ResourceData, config *Config, disableDependentServices bool) error { err := retryTimeDuration(func() error { + billingProject := project userAgent, err := generateUserAgentString(d, config.userAgent) if err != nil { return err } - name := fmt.Sprintf("projects/%s/services/%s", project, service) servicesDisableCall := config.NewServiceUsageClient(userAgent).Services.Disable(name, &serviceusage.DisableServiceRequest{ DisableDependentServices: disableDependentServices, }) if config.UserProjectOverride { - servicesDisableCall.Header().Add("X-Goog-User-Project", project) + // err == nil indicates that the billing_project value was found + if bp, err := getBillingProject(d, config); err == nil { + billingProject = bp + } + servicesDisableCall.Header().Add("X-Goog-User-Project", billingProject) } sop, err := servicesDisableCall.Do() if err != nil { return err } // Wait for the operation to complete - waitErr := serviceUsageOperationWait(config, sop, project, "api to disable", userAgent, d.Timeout(schema.TimeoutDelete)) + waitErr := serviceUsageOperationWait(config, sop, billingProject, "api to disable", userAgent, d.Timeout(schema.TimeoutDelete)) if waitErr != nil { return waitErr } diff --git a/google/resource_google_project_service_test.go b/google/resource_google_project_service_test.go index 7d7a462ad71..3789ae154e9 100644 --- a/google/resource_google_project_service_test.go +++ b/google/resource_google_project_service_test.go @@ -207,8 +207,7 @@ func TestAccProjectService_renamedService(t *testing.T) { func testAccCheckProjectService(t *testing.T, services []string, pid string, expectEnabled bool) resource.TestCheckFunc { return func(s *terraform.State) error { config := googleProviderConfig(t) - - currentlyEnabled, err := listCurrentlyEnabledServices(pid, config.userAgent, config, time.Minute*10) + currentlyEnabled, err := listCurrentlyEnabledServices(pid, "", config.userAgent, config, time.Minute*10) if err != nil { return fmt.Errorf("Error listing services for project %q: %v", pid, err) } diff --git a/google/serviceusage_batching.go b/google/serviceusage_batching.go index 9504e82153a..8bff91790ca 100644 --- a/google/serviceusage_batching.go +++ b/google/serviceusage_batching.go @@ -27,11 +27,17 @@ func BatchRequestEnableService(service string, project string, d *schema.Resourc return err } + billingProject := project + // err == nil indicates that the billing_project value was found + if bp, err := getBillingProject(d, config); err == nil { + billingProject = bp + } + req := &BatchRequest{ ResourceName: project, Body: []string{service}, CombineF: combineServiceUsageServicesBatches, - SendF: sendBatchFuncEnableServices(config, userAgent, d.Timeout(schema.TimeoutCreate)), + SendF: sendBatchFuncEnableServices(config, userAgent, billingProject, d.Timeout(schema.TimeoutCreate)), DebugId: fmt.Sprintf("Enable Project Service %q for project %q", service, project), } @@ -51,11 +57,17 @@ func tryEnableRenamedService(service, altName string, project string, d *schema. log.Printf("[DEBUG] found renamed service %s (with alternate name %s)", service, altName) // use a short timeout- failures are likely + billingProject := project + // err == nil indicates that the billing_project value was found + if bp, err := getBillingProject(d, config); err == nil { + billingProject = bp + } + log.Printf("[DEBUG] attempting enabling service with user-specified name %s", service) - err = enableServiceUsageProjectServices([]string{service}, project, userAgent, config, 1*time.Minute) + err = enableServiceUsageProjectServices([]string{service}, project, billingProject, userAgent, config, 1*time.Minute) if err != nil { log.Printf("[DEBUG] saw error %s. attempting alternate name %v", err, altName) - err2 := enableServiceUsageProjectServices([]string{altName}, project, userAgent, config, 1*time.Minute) + err2 := enableServiceUsageProjectServices([]string{altName}, project, billingProject, userAgent, config, 1*time.Minute) if err2 != nil { return fmt.Errorf("Saw 2 subsequent errors attempting to enable a renamed service: %s / %s", err, err2) } @@ -69,12 +81,18 @@ func BatchRequestReadServices(project string, d *schema.ResourceData, config *Co return nil, err } + billingProject := project + // err == nil indicates that the billing_project value was found + if bp, err := getBillingProject(d, config); err == nil { + billingProject = bp + } + req := &BatchRequest{ ResourceName: project, Body: nil, // Use empty CombineF since the request is exactly the same no matter how many services we read. CombineF: func(body interface{}, toAdd interface{}) (interface{}, error) { return nil, nil }, - SendF: sendListServices(config, userAgent, d.Timeout(schema.TimeoutRead)), + SendF: sendListServices(config, billingProject, userAgent, d.Timeout(schema.TimeoutRead)), DebugId: fmt.Sprintf("List Project Services %s", project), } @@ -97,18 +115,18 @@ func combineServiceUsageServicesBatches(srvsRaw interface{}, toAddRaw interface{ return append(srvs, toAdd...), nil } -func sendBatchFuncEnableServices(config *Config, userAgent string, timeout time.Duration) BatcherSendFunc { +func sendBatchFuncEnableServices(config *Config, userAgent, billingProject string, timeout time.Duration) BatcherSendFunc { return func(project string, toEnableRaw interface{}) (interface{}, error) { toEnable, ok := toEnableRaw.([]string) if !ok { return nil, fmt.Errorf("Expected batch body type to be []string, got %v. This is a provider error.", toEnableRaw) } - return nil, enableServiceUsageProjectServices(toEnable, project, userAgent, config, timeout) + return nil, enableServiceUsageProjectServices(toEnable, project, billingProject, userAgent, config, timeout) } } -func sendListServices(config *Config, userAgent string, timeout time.Duration) BatcherSendFunc { +func sendListServices(config *Config, billingProject, userAgent string, timeout time.Duration) BatcherSendFunc { return func(project string, _ interface{}) (interface{}, error) { - return listCurrentlyEnabledServices(project, userAgent, config, timeout) + return listCurrentlyEnabledServices(project, billingProject, userAgent, config, timeout) } }