-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Add google_app_engine_application resource. #2147
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
Changes from 2 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,278 @@ | ||
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(), | ||
}, | ||
"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 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 | ||
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. this line can be removed, it's handled in the expander |
||
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 | ||
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 && !isGoogleApiErrorWithCode(err, 404) { | ||
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 since it's its own resource now, you can do a regular |
||
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 { | ||
log.Println("[DEBUG] App Engine applications cannot be destroyed once created. The project must be deleted to delete the application.") | ||
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. it doesn't really matter since from my memory log levels only apply to tf core and not the providers, but if they did make a difference, i would say this should be a WARNING like we do for KmsKeyRing |
||
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 { | ||
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. this is already a plan-time check, so no need to check it again |
||
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 | ||
} |
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.