Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

support billing_project for google_project_service #10395

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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .changelog/5215.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:enhancement
project: support `billing_project` for `google_project_service`
```
63 changes: 45 additions & 18 deletions google/resource_google_project.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"fmt"
"log"
"net/http"
"regexp"
"strconv"
"strings"
Expand All @@ -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 {
Expand Down Expand Up @@ -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)
}

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -595,40 +614,47 @@ 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 {
// 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.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
}
Expand All @@ -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}}"
Expand Down Expand Up @@ -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
}
Expand Down
18 changes: 14 additions & 4 deletions google/resource_google_project_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -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
}
Expand Down
3 changes: 1 addition & 2 deletions google/resource_google_project_service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
34 changes: 26 additions & 8 deletions google/serviceusage_batching.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}

Expand All @@ -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)
}
Expand All @@ -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),
}

Expand All @@ -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)
}
}