mirror of
https://github.com/letic/terraform-provider-google.git
synced 2024-07-09 11:38:29 +00:00
![Paddy Carver](/assets/img/avatar_default.png)
Deprecate the app_engine sub-block of google_project, and create a google_app_engine_application resource instead. Also, add some tests for its behaviour, as well as some documentation for it. Note that this is largely an implementation of the ideas discussed in #2118, except we're not using CustomizeDiff to reject deletions without our special flag set, because CustomizeDiff apparently doesn't run on Delete. Who knew? This leaves us rejecting the deletion at apply time, which is less than ideal, but the only other option I see is to silently not delete the resource, and that's... not ideal, either. This also stops the app_engine sub-block on google_project from forcing new when it's removed, and sets it to computed, so users can safely move from using the sub-block to using the resource without state surgery or deleting their entire project. This does mean it's impossible to delete an App Engine application from a sub-block now, but seeing as that was the same situation before, and we just papered over it by making the project recreate itself in that situation, and people Were Not Fans of that, I'm considering that an acceptable casualty.
704 lines
21 KiB
Go
704 lines
21 KiB
Go
package google
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/hashicorp/terraform/helper/schema"
|
|
"github.com/hashicorp/terraform/helper/validation"
|
|
appengine "google.golang.org/api/appengine/v1"
|
|
"google.golang.org/api/cloudbilling/v1"
|
|
"google.golang.org/api/cloudresourcemanager/v1"
|
|
"google.golang.org/api/googleapi"
|
|
)
|
|
|
|
// 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,
|
|
|
|
Importer: &schema.ResourceImporter{
|
|
State: resourceProjectImportState,
|
|
},
|
|
MigrateState: resourceGoogleProjectMigrateState,
|
|
CustomizeDiff: resourceGoogleProjectCustomizeDiff,
|
|
|
|
Schema: map[string]*schema.Schema{
|
|
"project_id": &schema.Schema{
|
|
Type: schema.TypeString,
|
|
Required: true,
|
|
ForceNew: true,
|
|
ValidateFunc: validateProjectID(),
|
|
},
|
|
"skip_delete": &schema.Schema{
|
|
Type: schema.TypeBool,
|
|
Optional: true,
|
|
Computed: true,
|
|
},
|
|
"auto_create_network": &schema.Schema{
|
|
Type: schema.TypeBool,
|
|
Optional: true,
|
|
Default: true,
|
|
},
|
|
"name": &schema.Schema{
|
|
Type: schema.TypeString,
|
|
Required: true,
|
|
ValidateFunc: validateProjectName(),
|
|
},
|
|
"org_id": &schema.Schema{
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
Computed: true,
|
|
},
|
|
"folder_id": &schema.Schema{
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
Computed: true,
|
|
StateFunc: parseFolderId,
|
|
},
|
|
"policy_data": &schema.Schema{
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
Computed: true,
|
|
Removed: "Use the 'google_project_iam_policy' resource to define policies for a Google Project",
|
|
},
|
|
"policy_etag": &schema.Schema{
|
|
Type: schema.TypeString,
|
|
Computed: true,
|
|
Removed: "Use the the 'google_project_iam_policy' resource to define policies for a Google Project",
|
|
},
|
|
"number": &schema.Schema{
|
|
Type: schema.TypeString,
|
|
Computed: true,
|
|
},
|
|
"billing_account": &schema.Schema{
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
},
|
|
"labels": &schema.Schema{
|
|
Type: schema.TypeMap,
|
|
Optional: true,
|
|
Elem: &schema.Schema{Type: schema.TypeString},
|
|
Set: schema.HashString,
|
|
},
|
|
"app_engine": &schema.Schema{
|
|
Type: schema.TypeList,
|
|
Optional: true,
|
|
Computed: true,
|
|
Elem: appEngineResource(),
|
|
MaxItems: 1,
|
|
Deprecated: "Use the google_app_engine_application resource instead.",
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func appEngineResource() *schema.Resource {
|
|
return &schema.Resource{
|
|
Schema: map[string]*schema.Schema{
|
|
"auth_domain": &schema.Schema{
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
Computed: true,
|
|
},
|
|
"location_id": &schema.Schema{
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
Computed: true,
|
|
ValidateFunc: validation.StringInSlice([]string{
|
|
"northamerica-northeast1",
|
|
"us-central",
|
|
"us-west2",
|
|
"us-east1",
|
|
"us-east4",
|
|
"southamerica-east1",
|
|
"europe-west",
|
|
"europe-west2",
|
|
"europe-west3",
|
|
"asia-northeast1",
|
|
"asia-south1",
|
|
"australia-southeast1",
|
|
}, false),
|
|
},
|
|
"serving_status": &schema.Schema{
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
ValidateFunc: validation.StringInSlice([]string{
|
|
"UNSPECIFIED",
|
|
"SERVING",
|
|
"USER_DISABLED",
|
|
"SYSTEM_DISABLED",
|
|
}, false),
|
|
Computed: true,
|
|
},
|
|
"feature_settings": &schema.Schema{
|
|
Type: schema.TypeList,
|
|
Optional: true,
|
|
Computed: true,
|
|
MaxItems: 1,
|
|
Elem: appEngineFeatureSettingsResource(),
|
|
},
|
|
"name": &schema.Schema{
|
|
Type: schema.TypeString,
|
|
Computed: true,
|
|
},
|
|
"url_dispatch_rule": &schema.Schema{
|
|
Type: schema.TypeList,
|
|
Computed: true,
|
|
Elem: appEngineURLDispatchRuleResource(),
|
|
},
|
|
"code_bucket": &schema.Schema{
|
|
Type: schema.TypeString,
|
|
Computed: true,
|
|
},
|
|
"default_hostname": &schema.Schema{
|
|
Type: schema.TypeString,
|
|
Computed: true,
|
|
},
|
|
"default_bucket": &schema.Schema{
|
|
Type: schema.TypeString,
|
|
Computed: true,
|
|
},
|
|
"gcr_domain": &schema.Schema{
|
|
Type: schema.TypeString,
|
|
Computed: true,
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func appEngineURLDispatchRuleResource() *schema.Resource {
|
|
return &schema.Resource{
|
|
Schema: map[string]*schema.Schema{
|
|
"domain": &schema.Schema{
|
|
Type: schema.TypeString,
|
|
Computed: true,
|
|
},
|
|
"path": &schema.Schema{
|
|
Type: schema.TypeString,
|
|
Computed: true,
|
|
},
|
|
"service": &schema.Schema{
|
|
Type: schema.TypeString,
|
|
Computed: true,
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func appEngineFeatureSettingsResource() *schema.Resource {
|
|
return &schema.Resource{
|
|
Schema: map[string]*schema.Schema{
|
|
"split_health_checks": &schema.Schema{
|
|
Type: schema.TypeBool,
|
|
Optional: true,
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func resourceGoogleProjectCustomizeDiff(diff *schema.ResourceDiff, meta interface{}) error {
|
|
if old, _ := diff.GetChange("app_engine.0.location_id"); diff.HasChange("app_engine.0.location_id") && old != nil && old.(string) != "" {
|
|
// if location_id was already set, and has a new value, that forces a new app
|
|
// if location_id wasn't set, don't force a new value, as we're just enabling app engine
|
|
return diff.ForceNew("app_engine.0.location_id")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func resourceGoogleProjectCreate(d *schema.ResourceData, meta interface{}) error {
|
|
config := meta.(*Config)
|
|
|
|
var pid string
|
|
var err error
|
|
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("labels"); ok {
|
|
project.Labels = expandLabels(d)
|
|
}
|
|
|
|
op, err := config.clientResourceManager.Projects.Create(project).Do()
|
|
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(pid)
|
|
|
|
// Wait for the operation to complete
|
|
waitErr := resourceManagerOperationWait(config.clientResourceManager, op, "project to create")
|
|
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)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// set up App Engine, too
|
|
app, err := expandAppEngineApp(d)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if app != nil {
|
|
log.Printf("[DEBUG] Enabling App Engine")
|
|
// enable the app engine APIs so we can create stuff
|
|
if err = enableService("appengine.googleapis.com", project.ProjectId, config); err != nil {
|
|
return fmt.Errorf("Error enabling the App Engine Admin API required to configure App Engine applications: %s", err)
|
|
}
|
|
log.Printf("[DEBUG] Enabled App Engine")
|
|
err = createAppEngineApp(config, pid, app)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
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.
|
|
if err = enableService("compute.googleapis.com", project.ProjectId, config); err != nil {
|
|
return fmt.Errorf("Error enabling the Compute Engine API required to delete the default network: %s", err)
|
|
}
|
|
|
|
if err = forceDeleteComputeNetwork(project.ProjectId, "default", config); err != nil {
|
|
return fmt.Errorf("Error deleting default network in project %s: %s", project.ProjectId, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func createAppEngineApp(config *Config, pid string, app *appengine.Application) error {
|
|
app.Id = pid
|
|
log.Printf("[DEBUG] Creating App Engine App")
|
|
op, err := config.clientAppEngine.Apps.Create(app).Do()
|
|
if err != nil {
|
|
return fmt.Errorf("Error creating App Engine application: %s", err.Error())
|
|
}
|
|
|
|
// Wait for the operation to complete
|
|
waitErr := appEngineOperationWait(config.clientAppEngine, op, pid, "App Engine app to create")
|
|
if waitErr != nil {
|
|
return waitErr
|
|
}
|
|
log.Printf("[DEBUG] Created App Engine App")
|
|
return nil
|
|
}
|
|
|
|
func resourceGoogleProjectRead(d *schema.ResourceData, meta interface{}) error {
|
|
config := meta.(*Config)
|
|
pid := d.Id()
|
|
|
|
// Read the project
|
|
p, err := config.clientResourceManager.Projects.Get(pid).Do()
|
|
if err != nil {
|
|
return 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
|
|
}
|
|
|
|
d.Set("project_id", pid)
|
|
d.Set("number", strconv.FormatInt(int64(p.ProjectNumber), 10))
|
|
d.Set("name", p.Name)
|
|
d.Set("labels", p.Labels)
|
|
|
|
if p.Parent != nil {
|
|
switch p.Parent.Type {
|
|
case "organization":
|
|
d.Set("org_id", p.Parent.Id)
|
|
d.Set("folder_id", "")
|
|
case "folder":
|
|
d.Set("folder_id", p.Parent.Id)
|
|
d.Set("org_id", "")
|
|
}
|
|
}
|
|
|
|
// Read the billing account
|
|
ba, err := config.clientBilling.Projects.GetBillingInfo(prefixedProject(pid)).Do()
|
|
if err != nil && !isApiNotEnabledError(err) {
|
|
return fmt.Errorf("Error reading billing account for project %q: %v", prefixedProject(pid), err)
|
|
} else if 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)
|
|
}
|
|
d.Set("billing_account", _ba)
|
|
}
|
|
|
|
// read the App Engine app, if one exists
|
|
// we don't have the config available for import, so we can't rely on
|
|
// that to read it. And honestly, we want to know if an App exists that
|
|
// shouldn't. So this tries to read it, sets it to empty if none exists,
|
|
// or sets it in state if one does exist.
|
|
app, err := config.clientAppEngine.Apps.Get(pid).Do()
|
|
if err != nil && !isGoogleApiErrorWithCode(err, 404) && !isApiNotEnabledError(err) {
|
|
return fmt.Errorf("Error retrieving App Engine application %q: %s", pid, err.Error())
|
|
} else if isGoogleApiErrorWithCode(err, 404) {
|
|
d.Set("app_engine", []map[string]interface{}{})
|
|
} else if isApiNotEnabledError(err) {
|
|
log.Printf("[WARN] App Engine Admin API not enabled, please enable it to read App Engine info about project %q: %s", pid, err.Error())
|
|
} else {
|
|
appBlocks, err := flattenAppEngineApp(app)
|
|
if err != nil {
|
|
return fmt.Errorf("Error serializing App Engine app: %s", err.Error())
|
|
}
|
|
err = d.Set("app_engine", appBlocks)
|
|
if err != nil {
|
|
return fmt.Errorf("Error setting App Engine application in state. This is a bug, please report it at https://github.com/terraform-providers/terraform-provider-google/issues. Error is:\n%s", err.Error())
|
|
}
|
|
}
|
|
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.(*Config)
|
|
pid := d.Id()
|
|
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 := config.clientResourceManager.Projects.Get(pid).Do()
|
|
if err != nil {
|
|
if v, ok := err.(*googleapi.Error); ok && v.Code == http.StatusNotFound {
|
|
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
|
|
p, err = config.clientResourceManager.Projects.Update(p.ProjectId, p).Do()
|
|
if err != nil {
|
|
return fmt.Errorf("Error updating project %q: %s", project_name, err)
|
|
}
|
|
d.SetPartial("name")
|
|
}
|
|
|
|
// 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
|
|
p, err = config.clientResourceManager.Projects.Update(p.ProjectId, p).Do()
|
|
if err != nil {
|
|
return fmt.Errorf("Error updating project %q: %s", project_name, err)
|
|
}
|
|
d.SetPartial("org_id")
|
|
d.SetPartial("folder_id")
|
|
}
|
|
|
|
// Billing account has changed
|
|
if ok := d.HasChange("billing_account"); ok {
|
|
err = updateProjectBillingAccount(d, config)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Project Labels have changed
|
|
if ok := d.HasChange("labels"); ok {
|
|
p.Labels = expandLabels(d)
|
|
|
|
// Do Update on project
|
|
p, err = config.clientResourceManager.Projects.Update(p.ProjectId, p).Do()
|
|
if err != nil {
|
|
return fmt.Errorf("Error updating project %q: %s", project_name, err)
|
|
}
|
|
d.SetPartial("labels")
|
|
}
|
|
|
|
// App Engine App has changed
|
|
if ok := d.HasChange("app_engine"); ok {
|
|
app, err := expandAppEngineApp(d)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// ignore if app is now not set; that should force new resource using customizediff
|
|
if app != nil {
|
|
if old, new := d.GetChange("app_engine.#"); (old == nil || old.(int) < 1) && new != nil && new.(int) > 0 {
|
|
err = createAppEngineApp(config, pid, app)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
log.Printf("[DEBUG] Updating App Engine App")
|
|
op, err := config.clientAppEngine.Apps.Patch(pid, app).UpdateMask("authDomain,servingStatus,featureSettings.splitHealthChecks").Do()
|
|
if err != nil {
|
|
return fmt.Errorf("Error creating App Engine application: %s", err.Error())
|
|
}
|
|
|
|
// Wait for the operation to complete
|
|
waitErr := appEngineOperationWait(config.clientAppEngine, op, pid, "App Engine app to update")
|
|
if waitErr != nil {
|
|
return waitErr
|
|
}
|
|
log.Printf("[DEBUG] Updated App Engine App")
|
|
}
|
|
d.SetPartial("app_engine")
|
|
}
|
|
}
|
|
|
|
d.Partial(false)
|
|
|
|
return resourceGoogleProjectRead(d, meta)
|
|
}
|
|
|
|
func resourceGoogleProjectDelete(d *schema.ResourceData, meta interface{}) error {
|
|
config := meta.(*Config)
|
|
// Only delete projects if skip_delete isn't set
|
|
if !d.Get("skip_delete").(bool) {
|
|
pid := d.Id()
|
|
_, err := config.clientResourceManager.Projects.Delete(pid).Do()
|
|
if err != nil {
|
|
return fmt.Errorf("Error deleting project %q: %s", pid, err)
|
|
}
|
|
}
|
|
d.SetId("")
|
|
return nil
|
|
}
|
|
|
|
func resourceProjectImportState(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
|
|
// Explicitly set to default as a workaround for `ImportStateVerify` tests, and so that users
|
|
// don't see a diff immediately after import.
|
|
d.Set("auto_create_network", true)
|
|
return []*schema.ResourceData{d}, nil
|
|
}
|
|
|
|
// Delete a compute network along with the firewall rules inside it.
|
|
func forceDeleteComputeNetwork(projectId, networkName string, config *Config) error {
|
|
networkLink := fmt.Sprintf("https://www.googleapis.com/compute/v1/projects/%s/global/networks/%s", projectId, networkName)
|
|
|
|
token := ""
|
|
for paginate := true; paginate; {
|
|
filter := fmt.Sprintf("network eq %s", networkLink)
|
|
resp, err := config.clientCompute.Firewalls.List(projectId).Filter(filter).Do()
|
|
if err != nil {
|
|
return fmt.Errorf("Error listing firewall rules in proj: %s", err)
|
|
}
|
|
|
|
log.Printf("[DEBUG] Found %d firewall rules in %q network", len(resp.Items), networkName)
|
|
|
|
for _, firewall := range resp.Items {
|
|
op, err := config.clientCompute.Firewalls.Delete(projectId, firewall.Name).Do()
|
|
if err != nil {
|
|
return fmt.Errorf("Error deleting firewall: %s", err)
|
|
}
|
|
err = computeSharedOperationWait(config.clientCompute, op, projectId, "Deleting Firewall")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
token = resp.NextPageToken
|
|
paginate = token != ""
|
|
}
|
|
|
|
return deleteComputeNetwork(projectId, networkName, config)
|
|
}
|
|
|
|
func updateProjectBillingAccount(d *schema.ResourceData, config *Config) error {
|
|
pid := d.Id()
|
|
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
|
|
}
|
|
_, err := config.clientBilling.Projects.UpdateBillingInfo(prefixedProject(pid), ba).Do()
|
|
if err != nil {
|
|
d.Set("billing_account", "")
|
|
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++ {
|
|
ba, err = config.clientBilling.Projects.GetBillingInfo(prefixedProject(pid)).Do()
|
|
if err != nil {
|
|
return 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 expandAppEngineApp(d *schema.ResourceData) (*appengine.Application, error) {
|
|
blocks := d.Get("app_engine").([]interface{})
|
|
if len(blocks) < 1 {
|
|
return nil, nil
|
|
}
|
|
if len(blocks) > 1 {
|
|
return nil, fmt.Errorf("only one app_engine block may be defined per project")
|
|
}
|
|
result := &appengine.Application{
|
|
AuthDomain: d.Get("app_engine.0.auth_domain").(string),
|
|
LocationId: d.Get("app_engine.0.location_id").(string),
|
|
Id: d.Get("project_id").(string),
|
|
GcrDomain: d.Get("app_engine.0.gcr_domain").(string),
|
|
ServingStatus: d.Get("app_engine.0.serving_status").(string),
|
|
}
|
|
featureSettings, err := expandAppEngineFeatureSettings(d, "app_engine.0.")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result.FeatureSettings = featureSettings
|
|
return result, nil
|
|
}
|
|
|
|
func flattenAppEngineApp(app *appengine.Application) ([]map[string]interface{}, error) {
|
|
result := map[string]interface{}{
|
|
"auth_domain": app.AuthDomain,
|
|
"code_bucket": app.CodeBucket,
|
|
"default_bucket": app.DefaultBucket,
|
|
"default_hostname": app.DefaultHostname,
|
|
"location_id": app.LocationId,
|
|
"name": app.Name,
|
|
"serving_status": app.ServingStatus,
|
|
}
|
|
dispatchRules, err := flattenAppEngineDispatchRules(app.DispatchRules)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result["url_dispatch_rule"] = dispatchRules
|
|
featureSettings, err := flattenAppEngineFeatureSettings(app.FeatureSettings)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result["feature_settings"] = featureSettings
|
|
return []map[string]interface{}{result}, nil
|
|
}
|
|
|
|
func expandAppEngineFeatureSettings(d *schema.ResourceData, prefix string) (*appengine.FeatureSettings, error) {
|
|
blocks := d.Get(prefix + "feature_settings").([]interface{})
|
|
if len(blocks) < 1 {
|
|
return nil, nil
|
|
}
|
|
if len(blocks) > 1 {
|
|
return nil, fmt.Errorf("only one feature_settings block may be defined per app")
|
|
}
|
|
return &appengine.FeatureSettings{
|
|
SplitHealthChecks: d.Get(prefix + "feature_settings.0.split_health_checks").(bool),
|
|
// force send SplitHealthChecks, so if it's set to false it still gets disabled
|
|
ForceSendFields: []string{"SplitHealthChecks"},
|
|
}, nil
|
|
}
|
|
|
|
func flattenAppEngineFeatureSettings(settings *appengine.FeatureSettings) ([]map[string]interface{}, error) {
|
|
if settings == nil {
|
|
return []map[string]interface{}{}, nil
|
|
}
|
|
result := map[string]interface{}{
|
|
"split_health_checks": settings.SplitHealthChecks,
|
|
}
|
|
return []map[string]interface{}{result}, nil
|
|
}
|
|
|
|
func flattenAppEngineDispatchRules(rules []*appengine.UrlDispatchRule) ([]map[string]interface{}, error) {
|
|
results := make([]map[string]interface{}, 0, len(rules))
|
|
for _, rule := range rules {
|
|
results = append(results, map[string]interface{}{
|
|
"domain": rule.Domain,
|
|
"path": rule.Path,
|
|
"service": rule.Service,
|
|
})
|
|
}
|
|
return results, nil
|
|
}
|