blob: 21001b87f8e7af4dd988d282f3f63fb12b759351 [file] [log] [blame]
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package resourcemanager
import (
"context"
"fmt"
"log"
"net/http"
"regexp"
"strconv"
"strings"
"time"
"github.com/hashicorp/errwrap"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
tpgcompute "github.com/hashicorp/terraform-provider-google-beta/google-beta/services/compute"
tpgserviceusage "github.com/hashicorp/terraform-provider-google-beta/google-beta/services/serviceusage"
"github.com/hashicorp/terraform-provider-google-beta/google-beta/tpgresource"
transport_tpg "github.com/hashicorp/terraform-provider-google-beta/google-beta/transport"
"github.com/hashicorp/terraform-provider-google-beta/google-beta/verify"
"google.golang.org/api/cloudbilling/v1"
"google.golang.org/api/cloudresourcemanager/v1"
"google.golang.org/api/googleapi"
"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 {
return &schema.Resource{
SchemaVersion: 1,
Create: resourceGoogleProjectCreate,
Read: resourceGoogleProjectRead,
Update: resourceGoogleProjectUpdate,
Delete: resourceGoogleProjectDelete,
CustomizeDiff: customdiff.All(
tpgresource.SetLabelsDiff,
),
Importer: &schema.ResourceImporter{
State: resourceProjectImportState,
},
Timeouts: &schema.ResourceTimeout{
Create: schema.DefaultTimeout(10 * time.Minute),
Update: schema.DefaultTimeout(10 * time.Minute),
Read: schema.DefaultTimeout(10 * time.Minute),
Delete: schema.DefaultTimeout(10 * time.Minute),
},
MigrateState: resourceGoogleProjectMigrateState,
Schema: map[string]*schema.Schema{
"project_id": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
ValidateFunc: verify.ValidateProjectID(),
Description: `The project ID. Changing this forces a new project to be created.`,
},
"skip_delete": {
Type: schema.TypeBool,
Optional: true,
Computed: true,
Description: `If true, the Terraform resource can be deleted without deleting the Project via the Google API.`,
},
"auto_create_network": {
Type: schema.TypeBool,
Optional: true,
Default: true,
Description: `Create the 'default' network automatically. Default true. If set to false, the default network will be deleted. Note that, for quota purposes, you will still need to have 1 network slot available to create the project successfully, even if you set auto_create_network to false, since the network will exist momentarily.`,
},
"name": {
Type: schema.TypeString,
Required: true,
ValidateFunc: verify.ValidateProjectName(),
Description: `The display name of the project.`,
},
"org_id": {
Type: schema.TypeString,
Optional: true,
ConflictsWith: []string{"folder_id"},
Description: `The numeric ID of the organization this project belongs to. Changing this forces a new project to be created. Only one of org_id or folder_id may be specified. If the org_id is specified then the project is created at the top level. Changing this forces the project to be migrated to the newly specified organization.`,
},
"folder_id": {
Type: schema.TypeString,
Optional: true,
StateFunc: ParseFolderId,
ConflictsWith: []string{"org_id"},
Description: `The numeric ID of the folder this project should be created under. Only one of org_id or folder_id may be specified. If the folder_id is specified, then the project is created under the specified folder. Changing this forces the project to be migrated to the newly specified folder.`,
},
"number": {
Type: schema.TypeString,
Computed: true,
Description: `The numeric identifier of the project.`,
},
"billing_account": {
Type: schema.TypeString,
Optional: true,
Description: `The alphanumeric ID of the billing account this project belongs to. The user or service account performing this operation with Terraform must have Billing Account Administrator privileges (roles/billing.admin) in the organization. See Google Cloud Billing API Access Control for more details.`,
},
"labels": {
Type: schema.TypeMap,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
Description: `A set of key/value label pairs to assign to the project.
**Note**: This field is non-authoritative, and will only manage the labels present in your configuration.
Please refer to the field 'effective_labels' for all of the labels present on the resource.`,
},
"terraform_labels": {
Type: schema.TypeMap,
Computed: true,
Description: `The combination of labels configured directly on the resource and default labels configured on the provider.`,
Elem: &schema.Schema{Type: schema.TypeString},
},
"effective_labels": {
Type: schema.TypeMap,
Computed: true,
Description: `All of labels (key/value pairs) present on the resource in GCP, including the labels configured through Terraform, other clients and services.`,
Elem: &schema.Schema{Type: schema.TypeString},
},
},
UseJSONNumber: true,
}
}
func resourceGoogleProjectCreate(d *schema.ResourceData, meta interface{}) error {
config := meta.(*transport_tpg.Config)
userAgent, err := tpgresource.GenerateUserAgentString(d, config.UserAgent)
if err != nil {
return err
}
if err = resourceGoogleProjectCheckPreRequisites(config, d, userAgent); err != nil {
return fmt.Errorf("failed pre-requisites: %v", err)
}
var pid string
pid = d.Get("project_id").(string)
log.Printf("[DEBUG]: Creating new project %q", pid)
project := &cloudresourcemanager.Project{
ProjectId: pid,
Name: d.Get("name").(string),
}
if err = getParentResourceId(d, project); err != nil {
return err
}
if _, ok := d.GetOk("effective_labels"); ok {
project.Labels = tpgresource.ExpandEffectiveLabels(d)
}
var op *cloudresourcemanager.Operation
err = transport_tpg.Retry(transport_tpg.RetryOptions{
RetryFunc: func() (reqErr error) {
op, reqErr = config.NewResourceManagerClient(userAgent).Projects.Create(project).Do()
return reqErr
},
Timeout: d.Timeout(schema.TimeoutCreate),
})
if err != nil {
return fmt.Errorf("error creating project %s (%s): %s. "+
"If you received a 403 error, make sure you have the"+
" `roles/resourcemanager.projectCreator` permission",
project.ProjectId, project.Name, err)
}
d.SetId(fmt.Sprintf("projects/%s", pid))
// Wait for the operation to complete
opAsMap, err := tpgresource.ConvertToMap(op)
if err != nil {
return err
}
waitErr := ResourceManagerOperationWaitTime(config, opAsMap, "creating folder", userAgent, d.Timeout(schema.TimeoutCreate))
if waitErr != nil {
// The resource wasn't actually created
d.SetId("")
return waitErr
}
// Set the billing account
if _, ok := d.GetOk("billing_account"); ok {
err = updateProjectBillingAccount(d, config, userAgent)
if err != nil {
return err
}
}
// Sleep for 10s, letting the billing account settle before other resources
// try to use this project.
time.Sleep(10 * time.Second)
err = resourceGoogleProjectRead(d, meta)
if err != nil {
return err
}
// There's no such thing as "don't auto-create network", only "delete the network
// post-creation" - but that's what it's called in the UI and let's not confuse
// people if we don't have to. The GCP Console is doing the same thing - creating
// 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.
billingProject := project.ProjectId
// err == nil indicates that the billing_project value was found
if bp, err := tpgresource.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)
}
if err = forceDeleteComputeNetwork(d, config, project.ProjectId, "default"); err != nil {
if transport_tpg.IsGoogleApiErrorWithCode(err, 404) {
log.Printf("[DEBUG] Default network not found for project %q, no need to delete it", project.ProjectId)
} else {
return errwrap.Wrapf(fmt.Sprintf("Error deleting default network in project %s: {{err}}", project.ProjectId), err)
}
}
}
return nil
}
func resourceGoogleProjectCheckPreRequisites(config *transport_tpg.Config, d *schema.ResourceData, userAgent string) error {
ib, ok := d.GetOk("billing_account")
if !ok {
return nil
}
ba := "billingAccounts/" + ib.(string)
const perm = "billing.resourceAssociations.create"
req := &cloudbilling.TestIamPermissionsRequest{
Permissions: []string{perm},
}
resp, err := config.NewBillingClient(userAgent).BillingAccounts.TestIamPermissions(ba, req).Do()
if err != nil {
return fmt.Errorf("failed to check permissions on billing account %q: %v", ba, err)
}
if !tpgresource.StringInSlice(resp.Permissions, perm) {
return fmt.Errorf("missing permission on %q: %v", ba, perm)
}
if !d.Get("auto_create_network").(bool) {
call := config.NewServiceUsageClient(userAgent).Services.Get("projects/00000000000/services/serviceusage.googleapis.com")
if config.UserProjectOverride {
if billingProject, err := tpgresource.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.
case err.Error() == "googleapi: Error 403: Project '00000000000' not found or permission denied., forbidden":
return nil
case strings.Contains(err.Error(), "accessNotConfigured"):
return fmt.Errorf("API serviceusage not enabled.\nFound error: %v", err)
}
}
return nil
}
func resourceGoogleProjectRead(d *schema.ResourceData, meta interface{}) error {
config := meta.(*transport_tpg.Config)
userAgent, err := tpgresource.GenerateUserAgentString(d, config.UserAgent)
if err != nil {
return err
}
parts := strings.Split(d.Id(), "/")
pid := parts[len(parts)-1]
p, err := readGoogleProject(d, config, userAgent)
if err != nil {
if gerr, ok := err.(*googleapi.Error); ok && gerr.Code == 403 && strings.Contains(gerr.Message, "caller does not have permission") {
return fmt.Errorf("the user does not have permission to access Project %q or it may not exist", pid)
}
return transport_tpg.HandleNotFoundError(err, d, fmt.Sprintf("Project %q", pid))
}
// If the project has been deleted from outside Terraform, remove it from state file.
if p.LifecycleState != "ACTIVE" {
log.Printf("[WARN] Removing project '%s' because its state is '%s' (requires 'ACTIVE').", pid, p.LifecycleState)
d.SetId("")
return nil
}
if err := d.Set("project_id", pid); err != nil {
return fmt.Errorf("Error setting project_id: %s", err)
}
if err := d.Set("number", strconv.FormatInt(p.ProjectNumber, 10)); err != nil {
return fmt.Errorf("Error setting number: %s", err)
}
if err := d.Set("name", p.Name); err != nil {
return fmt.Errorf("Error setting name: %s", err)
}
if err := tpgresource.SetLabels(p.Labels, d, "labels"); err != nil {
return fmt.Errorf("Error setting labels: %s", err)
}
if err := tpgresource.SetLabels(p.Labels, d, "terraform_labels"); err != nil {
return fmt.Errorf("Error setting terraform_labels: %s", err)
}
if err := d.Set("effective_labels", p.Labels); err != nil {
return fmt.Errorf("Error setting effective_labels: %s", err)
}
if p.Parent != nil {
switch p.Parent.Type {
case "organization":
if err := d.Set("org_id", p.Parent.Id); err != nil {
return fmt.Errorf("Error setting org_id: %s", err)
}
if err := d.Set("folder_id", ""); err != nil {
return fmt.Errorf("Error setting folder_id: %s", err)
}
case "folder":
if err := d.Set("folder_id", p.Parent.Id); err != nil {
return fmt.Errorf("Error setting folder_id: %s", err)
}
if err := d.Set("org_id", ""); err != nil {
return fmt.Errorf("Error setting org_id: %s", err)
}
}
}
var ba *cloudbilling.ProjectBillingInfo
err = transport_tpg.Retry(transport_tpg.RetryOptions{
RetryFunc: func() (reqErr error) {
ba, reqErr = config.NewBillingClient(userAgent).Projects.GetBillingInfo(PrefixedProject(pid)).Do()
return reqErr
},
Timeout: d.Timeout(schema.TimeoutRead),
})
// Read the billing account
if err != nil && !transport_tpg.IsApiNotEnabledError(err) {
return fmt.Errorf("Error reading billing account for project %q: %v", PrefixedProject(pid), err)
} else if transport_tpg.IsApiNotEnabledError(err) {
log.Printf("[WARN] Billing info API not enabled, please enable it to read billing info about project %q: %s", pid, err.Error())
} else if ba.BillingAccountName != "" {
// BillingAccountName is contains the resource name of the billing account
// associated with the project, if any. For example,
// `billingAccounts/012345-567890-ABCDEF`. We care about the ID and not
// the `billingAccounts/` prefix, so we need to remove that. If the
// prefix ever changes, we'll validate to make sure it's something we
// recognize.
_ba := strings.TrimPrefix(ba.BillingAccountName, "billingAccounts/")
if ba.BillingAccountName == _ba {
return fmt.Errorf("Error parsing billing account for project %q. Expected value to begin with 'billingAccounts/' but got %s", PrefixedProject(pid), ba.BillingAccountName)
}
if err := d.Set("billing_account", _ba); err != nil {
return fmt.Errorf("Error setting billing_account: %s", err)
}
}
return nil
}
func PrefixedProject(pid string) string {
return "projects/" + pid
}
func getParentResourceId(d *schema.ResourceData, p *cloudresourcemanager.Project) error {
orgId := d.Get("org_id").(string)
folderId := d.Get("folder_id").(string)
if orgId != "" && folderId != "" {
return fmt.Errorf("'org_id' and 'folder_id' cannot be both set.")
}
if orgId != "" {
p.Parent = &cloudresourcemanager.ResourceId{
Id: orgId,
Type: "organization",
}
}
if folderId != "" {
p.Parent = &cloudresourcemanager.ResourceId{
Id: ParseFolderId(folderId),
Type: "folder",
}
}
return nil
}
func ParseFolderId(v interface{}) string {
folderId := v.(string)
if strings.HasPrefix(folderId, "folders/") {
return folderId[8:]
}
return folderId
}
func resourceGoogleProjectUpdate(d *schema.ResourceData, meta interface{}) error {
config := meta.(*transport_tpg.Config)
userAgent, err := tpgresource.GenerateUserAgentString(d, config.UserAgent)
if err != nil {
return err
}
parts := strings.Split(d.Id(), "/")
pid := parts[len(parts)-1]
project_name := d.Get("name").(string)
// Read the project
// we need the project even though refresh has already been called
// because the API doesn't support patch, so we need the actual object
p, err := readGoogleProject(d, config, userAgent)
if err != nil {
if transport_tpg.IsGoogleApiErrorWithCode(err, 404) {
return fmt.Errorf("Project %q does not exist.", pid)
}
return fmt.Errorf("Error checking project %q: %s", pid, err)
}
d.Partial(true)
// Project display name has changed
if ok := d.HasChange("name"); ok {
p.Name = project_name
// Do update on project
if p, err = updateProject(config, d, project_name, userAgent, p); err != nil {
return err
}
}
// Project parent has changed
if d.HasChange("org_id") || d.HasChange("folder_id") {
if err := getParentResourceId(d, p); err != nil {
return err
}
// Do update on project
if p, err = updateProject(config, d, project_name, userAgent, p); err != nil {
return err
}
}
// Billing account has changed
if ok := d.HasChange("billing_account"); ok {
err = updateProjectBillingAccount(d, config, userAgent)
if err != nil {
return err
}
}
// Project Labels have changed
if ok := d.HasChange("effective_labels"); ok {
p.Labels = tpgresource.ExpandEffectiveLabels(d)
// Do Update on project
if p, err = updateProject(config, d, project_name, userAgent, p); err != nil {
return err
}
}
d.Partial(false)
return resourceGoogleProjectRead(d, meta)
}
func updateProject(config *transport_tpg.Config, d *schema.ResourceData, projectName, userAgent string, desiredProject *cloudresourcemanager.Project) (*cloudresourcemanager.Project, error) {
var newProj *cloudresourcemanager.Project
if err := transport_tpg.Retry(transport_tpg.RetryOptions{
RetryFunc: func() (updateErr error) {
newProj, updateErr = config.NewResourceManagerClient(userAgent).Projects.Update(desiredProject.ProjectId, desiredProject).Do()
return updateErr
},
Timeout: d.Timeout(schema.TimeoutUpdate),
}); err != nil {
return nil, fmt.Errorf("Error updating project %q: %s", projectName, err)
}
return newProj, nil
}
func resourceGoogleProjectDelete(d *schema.ResourceData, meta interface{}) error {
config := meta.(*transport_tpg.Config)
userAgent, err := tpgresource.GenerateUserAgentString(d, config.UserAgent)
if err != nil {
return err
}
// Only delete projects if skip_delete isn't set
if !d.Get("skip_delete").(bool) {
parts := strings.Split(d.Id(), "/")
pid := parts[len(parts)-1]
if err := transport_tpg.Retry(transport_tpg.RetryOptions{
RetryFunc: func() error {
_, delErr := config.NewResourceManagerClient(userAgent).Projects.Delete(pid).Do()
return delErr
},
Timeout: d.Timeout(schema.TimeoutDelete),
}); err != nil {
return transport_tpg.HandleNotFoundError(err, d, fmt.Sprintf("Project %s", pid))
}
}
d.SetId("")
return nil
}
func resourceProjectImportState(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
parts := strings.Split(d.Id(), "/")
pid := parts[len(parts)-1]
// Prevent importing via project number, this will cause issues later
matched, err := regexp.MatchString("^\\d+$", pid)
if err != nil {
return nil, fmt.Errorf("Error matching project %q: %s", pid, err)
}
if matched {
return nil, fmt.Errorf("Error importing project %q, please use project_id", pid)
}
// Ensure the id format includes projects/
d.SetId(fmt.Sprintf("projects/%s", pid))
// Explicitly set to default as a workaround for `ImportStateVerify` tests, and so that users
// don't see a diff immediately after import.
if err := d.Set("auto_create_network", true); err != nil {
return nil, fmt.Errorf("Error setting auto_create_network: %s", err)
}
return []*schema.ResourceData{d}, nil
}
// Delete a compute network along with the firewall rules inside it.
func forceDeleteComputeNetwork(d *schema.ResourceData, config *transport_tpg.Config, projectId, networkName string) error {
userAgent, err := tpgresource.GenerateUserAgentString(d, config.UserAgent)
if err != nil {
return err
}
// Read the network from the API so we can get the correct self link format. We can't construct it from the
// base path because it might not line up exactly (compute.googleapis.com vs www.googleapis.com)
net, err := config.NewComputeClient(userAgent).Networks.Get(projectId, networkName).Do()
if err != nil {
return err
}
token := ""
for paginate := true; paginate; {
filter := fmt.Sprintf("network eq %s", net.SelfLink)
resp, err := config.NewComputeClient(userAgent).Firewalls.List(projectId).Filter(filter).Do()
if err != nil {
return errwrap.Wrapf("Error listing firewall rules in proj: {{err}}", err)
}
log.Printf("[DEBUG] Found %d firewall rules in %q network", len(resp.Items), networkName)
for _, firewall := range resp.Items {
op, err := config.NewComputeClient(userAgent).Firewalls.Delete(projectId, firewall.Name).Do()
if err != nil {
return errwrap.Wrapf("Error deleting firewall: {{err}}", err)
}
err = tpgcompute.ComputeOperationWaitTime(config, op, projectId, "Deleting Firewall", userAgent, d.Timeout(schema.TimeoutCreate))
if err != nil {
return err
}
}
token = resp.NextPageToken
paginate = token != ""
}
return deleteComputeNetwork(projectId, networkName, userAgent, config)
}
func updateProjectBillingAccount(d *schema.ResourceData, config *transport_tpg.Config, userAgent string) error {
parts := strings.Split(d.Id(), "/")
pid := parts[len(parts)-1]
name := d.Get("billing_account").(string)
ba := &cloudbilling.ProjectBillingInfo{}
// If we're unlinking an existing billing account, an empty request does that, not an empty-string billing account.
if name != "" {
ba.BillingAccountName = "billingAccounts/" + name
}
updateBillingInfoFunc := func() error {
_, err := config.NewBillingClient(userAgent).Projects.UpdateBillingInfo(PrefixedProject(pid), ba).Do()
return err
}
err := transport_tpg.Retry(transport_tpg.RetryOptions{
RetryFunc: updateBillingInfoFunc,
Timeout: d.Timeout(schema.TimeoutUpdate),
})
if err != nil {
if err := d.Set("billing_account", ""); err != nil {
return fmt.Errorf("Error setting billing_account: %s", err)
}
if _err, ok := err.(*googleapi.Error); ok {
return fmt.Errorf("Error setting billing account %q for project %q: %v", name, PrefixedProject(pid), _err)
}
return fmt.Errorf("Error setting billing account %q for project %q: %v", name, PrefixedProject(pid), err)
}
for retries := 0; retries < 3; retries++ {
var ba *cloudbilling.ProjectBillingInfo
err = transport_tpg.Retry(transport_tpg.RetryOptions{
RetryFunc: func() (reqErr error) {
ba, reqErr = config.NewBillingClient(userAgent).Projects.GetBillingInfo(PrefixedProject(pid)).Do()
return reqErr
},
Timeout: d.Timeout(schema.TimeoutRead),
})
if err != nil {
return fmt.Errorf("Error getting billing info for project %q: %v", PrefixedProject(pid), err)
}
baName := strings.TrimPrefix(ba.BillingAccountName, "billingAccounts/")
if baName == name {
return nil
}
time.Sleep(3 * time.Second)
}
return fmt.Errorf("Timed out waiting for billing account to return correct value. Waiting for %s, got %s.",
name, strings.TrimPrefix(ba.BillingAccountName, "billingAccounts/"))
}
func deleteComputeNetwork(project, network, userAgent string, config *transport_tpg.Config) error {
op, err := config.NewComputeClient(userAgent).Networks.Delete(
project, network).Do()
if err != nil {
return errwrap.Wrapf("Error deleting network: {{err}}", err)
}
err = tpgcompute.ComputeOperationWaitTime(config, op, project, "Deleting Network", userAgent, 10*time.Minute)
if err != nil {
return err
}
return nil
}
func readGoogleProject(d *schema.ResourceData, config *transport_tpg.Config, userAgent string) (*cloudresourcemanager.Project, error) {
var p *cloudresourcemanager.Project
// Read the project
parts := strings.Split(d.Id(), "/")
pid := parts[len(parts)-1]
err := transport_tpg.Retry(transport_tpg.RetryOptions{
RetryFunc: func() (reqErr error) {
p, reqErr = config.NewResourceManagerClient(userAgent).Projects.Get(pid).Do()
return reqErr
},
Timeout: d.Timeout(schema.TimeoutRead),
})
return p, err
}
// Enables services. WARNING: Use BatchRequestEnableServices for better batching if possible.
func EnableServiceUsageProjectServices(services []string, project, billingProject, userAgent string, config *transport_tpg.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
for i := 0; i < len(services); i += maxServiceUsageBatchSize {
j := i + maxServiceUsageBatchSize
if j > len(services) {
j = len(services)
}
nextBatch := services[i:j]
if len(nextBatch) == 0 {
// All batches finished, return.
return 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, billingProject, userAgent, config, timeout)
}
func doEnableServicesRequest(services []string, project, billingProject, userAgent string, config *transport_tpg.Config, timeout time.Duration) error {
var op *serviceusage.Operation
// errors can come up at multiple points, so there are a few levels of
// retrying here.
// logicalErr / waitErr: overall error on the logical operation (enabling services)
// but possibly also errors when retrieving the LRO (these are rare)
// err / reqErr: precondition errors when sending the request received instead of an LRO
logicalErr := transport_tpg.Retry(transport_tpg.RetryOptions{
RetryFunc: func() error {
err := transport_tpg.Retry(transport_tpg.RetryOptions{
RetryFunc: func() error {
var reqErr error
var call ServicesCall
if len(services) == 1 {
// BatchEnable returns an error for a single item, so enable with single endpoint
name := fmt.Sprintf("projects/%s/services/%s", project, services[0])
req := &serviceusage.EnableServiceRequest{}
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}
call = config.NewServiceUsageClient(userAgent).Services.BatchEnable(name, req)
}
if config.UserProjectOverride && billingProject != "" {
call.Header().Add("X-Goog-User-Project", billingProject)
}
op, reqErr = call.Do()
return handleServiceUsageRetryablePreconditionError(reqErr)
},
Timeout: timeout,
ErrorRetryPredicates: []transport_tpg.RetryErrorPredicateFunc{transport_tpg.ServiceUsageServiceBeingActivated},
})
if err != nil {
return errwrap.Wrapf("failed on request preconditions: {{err}}", err)
}
waitErr := tpgserviceusage.ServiceUsageOperationWait(config, op, billingProject, fmt.Sprintf("Enable Project %q Services: %+v", project, services), userAgent, timeout)
if waitErr != nil {
return waitErr
}
return nil
},
Timeout: timeout,
ErrorRetryPredicates: []transport_tpg.RetryErrorPredicateFunc{transport_tpg.ServiceUsageInternalError160009},
})
if logicalErr != nil {
return errwrap.Wrapf("failed to enable services: {{err}}", logicalErr)
}
return nil
}
// Handle errors that are retryable at call time for serviceusage
// Specifically, errors in https://cloud.google.com/service-usage/docs/reference/rest/v1/services/batchEnable#response-body
// Errors in operations are handled separately.
// NOTE(rileykarson): This should probably be turned into a retry predicate
func handleServiceUsageRetryablePreconditionError(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
}
// 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, billingProject, userAgent string, config *transport_tpg.Config, timeout time.Duration) (map[string]struct{}, error) {
log.Printf("[DEBUG] Listing enabled services for project %s", project)
apiServices := make(map[string]struct{})
err := transport_tpg.Retry(transport_tpg.RetryOptions{
RetryFunc: func() error {
ctx := context.Background()
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}}"
name := tpgresource.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: 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.
func waitForServiceUsageEnabledServices(services []string, project, billingProject, userAgent string, config *transport_tpg.Config, timeout time.Duration) error {
missing := make([]string, 0, len(services))
delay := time.Duration(0)
interval := time.Second
err := transport_tpg.Retry(transport_tpg.RetryOptions{
RetryFunc: func() error {
// Get the list of services that are enabled on the project
enabledServices, err := ListCurrentlyEnabledServices(project, billingProject, userAgent, config, timeout)
if err != nil {
return err
}
missing := make([]string, 0, len(services))
for _, s := range services {
if _, ok := enabledServices[s]; !ok {
missing = append(missing, s)
}
}
if len(missing) > 0 {
log.Printf("[DEBUG] waiting %v before reading project %s services...", delay, project)
time.Sleep(delay)
delay += interval
interval += delay
// Spoof a googleapi Error so retryTime will try again
return &googleapi.Error{
Code: 503,
Message: fmt.Sprintf("The service(s) %q are still being enabled for project %s. This isn't a real API error, this is just eventual consistency.", missing, project),
}
}
return nil
},
Timeout: timeout,
})
if err != nil {
return errwrap.Wrap(err, fmt.Errorf("failed to enable some service(s) %q for project %s", missing, project))
}
return nil
}