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 #5215

Merged
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
ScottSuarez marked this conversation as resolved.
Show resolved Hide resolved
// 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")
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unsure if this call needs billing project passed. I am unsure of the exact purpose here.
@rileykarson

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe it does!

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 != "" {
ScottSuarez marked this conversation as resolved.
Show resolved Hide resolved
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)
}
ScottSuarez marked this conversation as resolved.
Show resolved Hide resolved
// 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 @@ -187,7 +187,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 @@ -258,24 +264,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 @@ -198,8 +198,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)
}
}