terraform-provider-google/google/resource_app_engine_application.go
Paddy Carver 9ceb644460 Add google_app_engine_application resource.
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.
2018-10-02 02:59:17 -07:00

286 lines
8.4 KiB
Go

package google
import (
"fmt"
"log"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/helper/validation"
appengine "google.golang.org/api/appengine/v1"
)
func resourceAppEngineApplication() *schema.Resource {
return &schema.Resource{
Create: resourceAppEngineApplicationCreate,
Read: resourceAppEngineApplicationRead,
Update: resourceAppEngineApplicationUpdate,
Delete: resourceAppEngineApplicationDelete,
Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
},
Schema: map[string]*schema.Schema{
"project": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
ValidateFunc: validateProjectID(),
},
"auth_domain": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
},
"location_id": &schema.Schema{
Type: schema.TypeString,
Required: 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: appEngineApplicationFeatureSettingsResource(),
},
"ack_delete_noop": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
},
"name": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
"url_dispatch_rule": &schema.Schema{
Type: schema.TypeList,
Computed: true,
Elem: appEngineApplicationURLDispatchRuleResource(),
},
"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 appEngineApplicationURLDispatchRuleResource() *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 appEngineApplicationFeatureSettingsResource() *schema.Resource {
return &schema.Resource{
Schema: map[string]*schema.Schema{
"split_health_checks": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
},
},
}
}
func resourceAppEngineApplicationCreate(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)
project, err := getProject(d, config)
if err != nil {
return err
}
app, err := expandAppEngineApplication(d, project)
if err != nil {
return err
}
app.Id = project
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())
}
d.SetId(project)
// Wait for the operation to complete
waitErr := appEngineOperationWait(config.clientAppEngine, op, project, "App Engine app to create")
if waitErr != nil {
return waitErr
}
log.Printf("[DEBUG] Created App Engine App")
return resourceAppEngineApplicationRead(d, meta)
}
func resourceAppEngineApplicationRead(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)
pid := d.Id()
app, err := config.clientAppEngine.Apps.Get(pid).Do()
if err != nil && !isGoogleApiErrorWithCode(err, 404) {
return fmt.Errorf("Error retrieving App Engine application %q: %s", pid, err.Error())
} else if isGoogleApiErrorWithCode(err, 404) {
log.Printf("[WARN] App Engine application %q not found, removing from state", pid)
d.SetId("")
return nil
}
d.Set("auth_domain", app.AuthDomain)
d.Set("code_bucket", app.CodeBucket)
d.Set("default_bucket", app.DefaultBucket)
d.Set("default_hostname", app.DefaultHostname)
d.Set("location_id", app.LocationId)
d.Set("name", app.Name)
d.Set("serving_status", app.ServingStatus)
d.Set("project", pid)
dispatchRules, err := flattenAppEngineApplicationDispatchRules(app.DispatchRules)
if err != nil {
return err
}
err = d.Set("url_dispatch_rule", dispatchRules)
if err != nil {
return fmt.Errorf("Error setting dispatch rules 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())
}
featureSettings, err := flattenAppEngineApplicationFeatureSettings(app.FeatureSettings)
if err != nil {
return err
}
err = d.Set("feature_settings", featureSettings)
if err != nil {
return fmt.Errorf("Error setting feature settings 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 resourceAppEngineApplicationUpdate(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)
pid := d.Id()
app, err := expandAppEngineApplication(d, pid)
if err != nil {
return err
}
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 updating 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")
return resourceAppEngineApplicationRead(d, meta)
}
func resourceAppEngineApplicationDelete(d *schema.ResourceData, meta interface{}) error {
// only delete app engine applications if the user has acknowledged it does nothing
if !d.Get("ack_delete_noop").(bool) {
return fmt.Errorf("App Engine applications cannot be destroyed once created. The project must be deleted to delete the application. To acknowledge this limitation and let Terraform think it deleted your application--even though it won't--set the `ack_delete_noop` field to true, and this error will go away.")
}
return nil
}
func expandAppEngineApplication(d *schema.ResourceData, project string) (*appengine.Application, error) {
result := &appengine.Application{
AuthDomain: d.Get("auth_domain").(string),
LocationId: d.Get("location_id").(string),
Id: project,
GcrDomain: d.Get("gcr_domain").(string),
ServingStatus: d.Get("serving_status").(string),
}
featureSettings, err := expandAppEngineApplicationFeatureSettings(d)
if err != nil {
return nil, err
}
result.FeatureSettings = featureSettings
return result, nil
}
func expandAppEngineApplicationFeatureSettings(d *schema.ResourceData) (*appengine.FeatureSettings, error) {
blocks := d.Get("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("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 flattenAppEngineApplicationFeatureSettings(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 flattenAppEngineApplicationDispatchRules(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
}