diff --git a/google/appengine_operation.go b/google/appengine_operation.go index 08614782..dbd5590a 100644 --- a/google/appengine_operation.go +++ b/google/appengine_operation.go @@ -3,6 +3,7 @@ package google import ( "fmt" "log" + "regexp" "strconv" "time" @@ -11,6 +12,10 @@ import ( "google.golang.org/api/appengine/v1" ) +var ( + appEngineOperationIdRE = regexp.MustCompile(fmt.Sprintf("apps/%s/operations/(.*)", ProjectRegex)) +) + type AppEngineOperationWaiter struct { Service *appengine.APIService Op *appengine.Operation @@ -19,7 +24,11 @@ type AppEngineOperationWaiter struct { func (w *AppEngineOperationWaiter) RefreshFunc() resource.StateRefreshFunc { return func() (interface{}, string, error) { - op, err := w.Service.Apps.Operations.Get(w.AppId, w.Op.Name).Do() + matches := appEngineOperationIdRE.FindStringSubmatch(w.Op.Name) + if len(matches) != 2 { + return nil, "", fmt.Errorf("Expected %d results of parsing operation name, got %d from %s", 2, len(matches), w.Op.Name) + } + op, err := w.Service.Apps.Operations.Get(w.AppId, matches[1]).Do() if err != nil { return nil, "", err } diff --git a/google/resource_google_project.go b/google/resource_google_project.go index c4fc3179..4ff7d3ef 100644 --- a/google/resource_google_project.go +++ b/google/resource_google_project.go @@ -140,11 +140,35 @@ func appEngineResource() *schema.Resource { Computed: true, }, "default_cookie_expiration_seconds": &schema.Schema{ - Type: schema.TypeFloat, + Type: schema.TypeInt, Optional: true, + // There's an undocumented requirement that this field be set to 1 day, 1 week, or 2 weeks in seconds + // that's 86400, 604800, and 1209600, respectively. This doesn't appear anywhere in the docs, as far + // as I can tell, but if you try to create with another value, the API is clear in its response that + // these are the only supported values: + // + // "This field must be either 1 day, 1 week, or 2 weeks in seconds. Valid values are 86400s, 604800s, + // or 1209600s." + ValidateFunc: func(i interface{}, k string) ([]string, []error) { + v, ok := i.(int) + if !ok { + return nil, []error{fmt.Errorf("expected type of %q to be int, was %T", k, i)} + } + if v != 86400 && v != 604800 && v != 1209600 { + return nil, []error{fmt.Errorf("only possible values for %q are %d, %d, and %d", k, 86400, 604800, 1209600)} + } + return nil, nil + }, }, "serving_status": &schema.Schema{ Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{ + "UNSPECIFIED", + "SERVING", + "USER_DISABLED", + "SYSTEM_DISABLED", + }, false), Computed: true, }, "default_hostname": &schema.Schema{ @@ -163,7 +187,7 @@ func appEngineResource() *schema.Resource { }, "gcr_domain": &schema.Schema{ Type: schema.TypeString, - Optional: true, + Computed: true, }, "feature_settings": &schema.Schema{ Type: schema.TypeList, @@ -233,7 +257,7 @@ func appEngineFeatureSettingsResource() *schema.Resource { func resourceGoogleProjectCustomizeDiff(diff *schema.ResourceDiff, meta interface{}) error { // don't need to check if changed, the call is a no-op/error if there's no change - diff.ForceNew("app_engine.#") + diff.ForceNew("app_engine") // force a change to client secret if it doesn't match its sha if !diff.HasChange("app_engine.0.iap.0.oauth2_client_secret") { @@ -289,6 +313,34 @@ func resourceGoogleProjectCreate(d *schema.ResourceData, meta interface{}) error } } + // set up App Engine, too + app, err := expandAppEngineApp(d) + if err != nil { + return err + } + log.Printf("[DEBUG] Creating App Engine if %v is true", app != nil) + 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") + 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") + } + err = resourceGoogleProjectRead(d, meta) if err != nil { return err @@ -308,30 +360,6 @@ func resourceGoogleProjectCreate(d *schema.ResourceData, meta interface{}) error return fmt.Errorf("Error deleting default network in project %s: %s", project.ProjectId, err) } } - - // set up App Engine, too - if len(d.Get("app_engine").([]interface{})) > 0 { - // 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) - } - app, err := expandAppEngineApp(d) - if err != nil { - return err - } - if app != nil { - 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, app.Id, "App Engine app to create") - if waitErr != nil { - return waitErr - } - } - } return nil } @@ -399,7 +427,7 @@ func resourceGoogleProjectRead(d *schema.ResourceData, meta interface{}) 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") + 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 @@ -623,13 +651,14 @@ func expandAppEngineApp(d *schema.ResourceData) (*appengine.Application, error) 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("gcr_domain").(string), + 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), } if v, ok := d.GetOkExists("app_engine.0.default_cookie_expiration_seconds"); ok { - result.DefaultCookieExpiration = strconv.FormatFloat(v.(float64), 'f', 9, 64) + "s" + result.DefaultCookieExpiration = strconv.FormatInt(int64(v.(int)), 10) + "s" } iap, err := expandAppEngineIAP(d, "app_engine.0.") if err != nil { @@ -666,7 +695,7 @@ func flattenAppEngineApp(app *appengine.Application) ([]map[string]interface{}, if err != nil { return nil, err } - result["dispatch_rule"] = dispatchRules + result["url_dispatch_rule"] = dispatchRules featureSettings, err := flattenAppEngineFeatureSettings(app.FeatureSettings) if err != nil { return nil, err diff --git a/google/resource_google_project_test.go b/google/resource_google_project_test.go index a599c62b..14d5fbee 100644 --- a/google/resource_google_project_test.go +++ b/google/resource_google_project_test.go @@ -187,6 +187,25 @@ func TestAccProject_parentFolder(t *testing.T) { }) } +func TestAccProject_appEngineBasic(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: testAccProject_appEngineBasic(pid, org), + Check: resource.ComposeTestCheckFunc( + testAccCheckGoogleProjectExists("google_project.acceptance", pid), + ), + }, + }, + }) +} + func testAccCheckGoogleProjectExists(r, pid string) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[r] @@ -349,6 +368,22 @@ resource "google_folder" "folder1" { `, pid, projectName, folderName, org) } +func testAccProject_appEngineBasic(pid, org string) string { + return fmt.Sprintf(` +resource "google_project" "acceptance" { + project_id = "%s" + name = "%s" + org_id = "%s" + + app_engine { + auth_domain = "hashicorptest.com" + location_id = "us-central" + default_cookie_expiration_seconds = 86400 + serving_status = "SERVING" + } +}`, pid, pid, org) +} + func skipIfEnvNotSet(t *testing.T, envs ...string) { for _, k := range envs { if os.Getenv(k) == "" {