Skip to content

Commit

Permalink
support billing_project for google_project_service (#5215)
Browse files Browse the repository at this point in the history
  • Loading branch information
ScottSuarez authored Oct 25, 2021
1 parent cf3cb92 commit 06837b4
Show file tree
Hide file tree
Showing 5 changed files with 87 additions and 33 deletions.
63 changes: 45 additions & 18 deletions mmv1/third_party/terraform/resources/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
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
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ func resourceProjectServiceIdentityCreate(d *schema.ResourceData, meta interface

var opRes map[string]interface{}
err = serviceUsageOperationWaitTimeWithResponse(
config, res, &opRes, project, "Creating Service Identity", userAgent,
config, res, &opRes, billingProject, "Creating Service Identity", userAgent,
d.Timeout(schema.TimeoutCreate))
if err != nil {
return err
Expand Down
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 mmv1/third_party/terraform/utils/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)
}
}

0 comments on commit 06837b4

Please sign in to comment.