-
Notifications
You must be signed in to change notification settings - Fork 1.8k
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
Add google_app_engine_application resource. #2147
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,285 @@ | ||
package google | ||
|
||
import ( | ||
"fmt" | ||
"log" | ||
|
||
"github.com/hashicorp/terraform/helper/customdiff" | ||
"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, | ||
}, | ||
|
||
CustomizeDiff: customdiff.All( | ||
appEngineApplicationLocationIDCustomizeDiff, | ||
), | ||
|
||
Schema: map[string]*schema.Schema{ | ||
"project": &schema.Schema{ | ||
Type: schema.TypeString, | ||
Optional: true, | ||
Computed: 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(), | ||
}, | ||
"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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: any reason these are funcs and not vars? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it's conventional/how I learned how to do it? If I had to guess, it's to keep important mutable state out of the global scope (the Schema is a pointer, someone mutating it will have a big effect on everyone else, probably unintentionally) but I could be very wrong about that. |
||
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 appEngineApplicationLocationIDCustomizeDiff(d *schema.ResourceDiff, meta interface{}) error { | ||
old, new := d.GetChange("location_id") | ||
if old != "" && old != new { | ||
return fmt.Errorf("Cannot change location_id once the resource is created.") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh clever! Let's do this for project too. |
||
} | ||
return nil | ||
} | ||
|
||
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 | ||
} | ||
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 { | ||
d.SetId("") | ||
return waitErr | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Either we need to |
||
} | ||
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 { | ||
return handleNotFoundError(err, d, fmt.Sprintf("App Engine Application %q", pid)) | ||
} | ||
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 { | ||
log.Println("[WARN] App Engine applications cannot be destroyed once created. The project must be deleted to delete the application.") | ||
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 | ||
} | ||
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 | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
package google | ||
|
||
import ( | ||
"fmt" | ||
"testing" | ||
|
||
"github.com/hashicorp/terraform/helper/acctest" | ||
"github.com/hashicorp/terraform/helper/resource" | ||
) | ||
|
||
func TestAccAppEngineApplication_basic(t *testing.T) { | ||
t.Parallel() | ||
|
||
org := getTestOrgFromEnv(t) | ||
pid := acctest.RandomWithPrefix("tf-test") | ||
resource.Test(t, resource.TestCase{ | ||
PreCheck: func() { testAccPreCheck(t) }, | ||
Providers: testAccProviders, | ||
Steps: []resource.TestStep{ | ||
{ | ||
Config: testAccAppEngineApplication_basic(pid, org), | ||
Check: resource.ComposeTestCheckFunc( | ||
resource.TestCheckResourceAttrSet("google_app_engine_application.acceptance", "url_dispatch_rule.#"), | ||
resource.TestCheckResourceAttrSet("google_app_engine_application.acceptance", "name"), | ||
resource.TestCheckResourceAttrSet("google_app_engine_application.acceptance", "code_bucket"), | ||
resource.TestCheckResourceAttrSet("google_app_engine_application.acceptance", "default_hostname"), | ||
resource.TestCheckResourceAttrSet("google_app_engine_application.acceptance", "default_bucket"), | ||
), | ||
}, | ||
{ | ||
ResourceName: "google_app_engine_application.acceptance", | ||
ImportState: true, | ||
ImportStateVerify: true, | ||
}, | ||
{ | ||
Config: testAccAppEngineApplication_update(pid, org), | ||
}, | ||
{ | ||
ResourceName: "google_app_engine_application.acceptance", | ||
ImportState: true, | ||
ImportStateVerify: true, | ||
}, | ||
}, | ||
}) | ||
} | ||
|
||
func testAccAppEngineApplication_basic(pid, org string) string { | ||
return fmt.Sprintf(` | ||
resource "google_project" "acceptance" { | ||
project_id = "%s" | ||
name = "%s" | ||
org_id = "%s" | ||
} | ||
|
||
resource "google_app_engine_application" "acceptance" { | ||
project = "${google_project.acceptance.project_id}" | ||
auth_domain = "hashicorptest.com" | ||
location_id = "us-central" | ||
serving_status = "SERVING" | ||
}`, pid, pid, org) | ||
} | ||
|
||
func testAccAppEngineApplication_update(pid, org string) string { | ||
return fmt.Sprintf(` | ||
resource "google_project" "acceptance" { | ||
project_id = "%s" | ||
name = "%s" | ||
org_id = "%s" | ||
} | ||
|
||
resource "google_app_engine_application" "acceptance" { | ||
project = "${google_project.acceptance.project_id}" | ||
auth_domain = "tf-test.club" | ||
location_id = "us-central" | ||
serving_status = "USER_DISABLED" | ||
}`, pid, pid, org) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this updatable? It's not in the updateMask if so
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nope. I guess I'll make it ForceNew, even though that will cause a problem if people do update it? I'm not 100% sure what a good user experience is in this case. I guess I could customize diff to reject this, at least.