From f78de6b76e5f6aadb2a91b8a5ba24ef4cc83da54 Mon Sep 17 00:00:00 2001 From: Paddy Date: Thu, 17 May 2018 14:47:34 -0700 Subject: [PATCH] Drop the resources we can't support. IAP has no reasonable support policy, because PATCH is broken, and IAP must be configured with an OAuth2 client ID and secret that belongs to the project the app is associated with. There's no programmatic way to create Clients. But we create the project and the app at the same time, and we can't update because PATCH is broken. So this just drops IAP. It also forces all our updates to ForceNew, because we can't update. Also, adds more test coverage and docs, and fixes import by not relying on the config for setting app engine info in state. --- google/resource_google_project.go | 159 +++----------------- google/resource_google_project_test.go | 51 ++++++- website/docs/r/google_project.html.markdown | 34 +++++ 3 files changed, 101 insertions(+), 143 deletions(-) diff --git a/google/resource_google_project.go b/google/resource_google_project.go index 4ff7d3ef..4eca4fc8 100644 --- a/google/resource_google_project.go +++ b/google/resource_google_project.go @@ -1,7 +1,6 @@ package google import ( - "crypto/sha256" "fmt" "log" "net/http" @@ -115,6 +114,8 @@ func appEngineResource() *schema.Resource { "auth_domain": &schema.Schema{ Type: schema.TypeString, Optional: true, + ForceNew: true, + Computed: true, }, "location_id": &schema.Schema{ Type: schema.TypeString, @@ -139,30 +140,10 @@ func appEngineResource() *schema.Resource { Type: schema.TypeString, Computed: true, }, - "default_cookie_expiration_seconds": &schema.Schema{ - 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, + ForceNew: true, ValidateFunc: validation.StringInSlice([]string{ "UNSPECIFIED", "SERVING", @@ -179,12 +160,6 @@ func appEngineResource() *schema.Resource { Type: schema.TypeString, Computed: true, }, - "iap": &schema.Schema{ - Type: schema.TypeList, - Optional: true, - MaxItems: 1, - Elem: appEngineIAPResource(), - }, "gcr_domain": &schema.Schema{ Type: schema.TypeString, Computed: true, @@ -192,6 +167,8 @@ func appEngineResource() *schema.Resource { "feature_settings": &schema.Schema{ Type: schema.TypeList, Optional: true, + Computed: true, + ForceNew: true, MaxItems: 1, Elem: appEngineFeatureSettingsResource(), }, @@ -218,32 +195,6 @@ func appEngineURLDispatchRuleResource() *schema.Resource { } } -func appEngineIAPResource() *schema.Resource { - return &schema.Resource{ - Schema: map[string]*schema.Schema{ - "enabled": &schema.Schema{ - Type: schema.TypeBool, - Required: true, - }, - "oauth2_client_id": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "oauth2_client_secret": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - Computed: true, - Sensitive: true, - }, - "oauth2_client_secret_sha256": &schema.Schema{ - Type: schema.TypeString, - Computed: true, - Sensitive: true, - }, - }, - } -} - func appEngineFeatureSettingsResource() *schema.Resource { return &schema.Resource{ Schema: map[string]*schema.Schema{ @@ -258,14 +209,6 @@ 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") - - // force a change to client secret if it doesn't match its sha - if !diff.HasChange("app_engine.0.iap.0.oauth2_client_secret") { - sha := sha256.Sum256([]byte(diff.Get("app_engine.0.iap.0.oauth2_client_secret").(string))) - if string(sha[:]) != diff.Get("app_engine.0.iap.0.oauth2_client_secret_sha256").(string) { - diff.SetNew("app_engine.0.iap.0.oauth2_client_secret", diff.Get("app_engine.0.iap.0.oauth2_client_secret")) - } - } return nil } @@ -415,12 +358,17 @@ func resourceGoogleProjectRead(d *schema.ResourceData, meta interface{}) error { d.Set("billing_account", _ba) } - // Read the App Engine app - if d.Get("app_engine.#").(int) > 0 { - app, err := config.clientAppEngine.Apps.Get(pid).Do() - if err != nil { - return fmt.Errorf("Error retrieving App Engine application %q: %s", pid, err.Error()) - } + // 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) { + 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 { appBlocks, err := flattenAppEngineApp(app) if err != nil { return fmt.Errorf("Error serializing App Engine app: %s", err.Error()) @@ -534,23 +482,7 @@ func resourceGoogleProjectUpdate(d *schema.ResourceData, meta interface{}) error d.SetPartial("labels") } - if d.HasChange("app_engine") { - // don't need to worry about case where block is removed; custom diff does that - // for us - app, err := expandAppEngineApp(d) - if err != nil { - return err - } - op, err := config.clientAppEngine.Apps.Patch(p.ProjectId, app).Do() - if err != nil { - return fmt.Errorf("Error updating App Engine application %q: %s", p.ProjectId, err.Error()) - } - waitErr := appEngineOperationWait(config.clientAppEngine, op, p.ProjectId, "App Engine app to update") - if waitErr != nil { - return waitErr - } - d.SetPartial("app_engine") - } + // ignore app_engine changes, they don't work anyways. d.Partial(false) return nil @@ -657,14 +589,6 @@ func expandAppEngineApp(d *schema.ResourceData) (*appengine.Application, error) 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.FormatInt(int64(v.(int)), 10) + "s" - } - iap, err := expandAppEngineIAP(d, "app_engine.0.") - if err != nil { - return nil, err - } - result.Iap = iap featureSettings, err := expandAppEngineFeatureSettings(d, "app_engine.0.") if err != nil { return nil, err @@ -683,14 +607,6 @@ func flattenAppEngineApp(app *appengine.Application) ([]map[string]interface{}, "name": app.Name, "serving_status": app.ServingStatus, } - if app.DefaultCookieExpiration != "" { - seconds := strings.TrimSuffix(app.DefaultCookieExpiration, "s") - exp, err := strconv.ParseFloat(seconds, 64) - if err != nil { - return nil, fmt.Errorf("invalid default cookie expiration: %s", err.Error()) - } - result["default_cookie_expiration_seconds"] = exp - } dispatchRules, err := flattenAppEngineDispatchRules(app.DispatchRules) if err != nil { return nil, err @@ -701,47 +617,6 @@ func flattenAppEngineApp(app *appengine.Application) ([]map[string]interface{}, return nil, err } result["feature_settings"] = featureSettings - iap, err := flattenAppEngineIAP(app.Iap) - if err != nil { - return nil, err - } - result["iap"] = iap - return []map[string]interface{}{result}, nil -} - -func expandAppEngineIAP(d *schema.ResourceData, prefix string) (*appengine.IdentityAwareProxy, error) { - blocks := d.Get(prefix + "iap").([]interface{}) - if len(blocks) < 1 { - return nil, nil - } - if len(blocks) > 1 { - return nil, fmt.Errorf("only one iap block may be defined per app") - } - if d.Get(prefix+"iap.0.oauth2_client_id").(string) == "" && d.Get(prefix+"iap.0.enabled").(bool) { - return nil, fmt.Errorf("oauth2_client_id must be set if the IAP is enabled") - } - if d.Get(prefix+"iap.0.oauth2_client_secret") == "" && d.Get(prefix+"iap.0.enabled").(bool) { - return nil, fmt.Errorf("oauth2_client_secret must be set if the IAP is enabled") - } - return &appengine.IdentityAwareProxy{ - Enabled: d.Get(prefix + "iap.0.enabled").(bool), - Oauth2ClientId: d.Get(prefix + "iap.0.oauth2_client_id").(string), - Oauth2ClientSecret: d.Get(prefix + "iap.0.oauth2_client_secret").(string), - // force send enabled, so if it's set to false, IAP still gets turned off - ForceSendFields: []string{"Enabled"}, - }, nil -} - -func flattenAppEngineIAP(iap *appengine.IdentityAwareProxy) ([]map[string]interface{}, error) { - if iap == nil { - return []map[string]interface{}{}, nil - } - result := map[string]interface{}{ - "enabled": iap.Enabled, - "oauth2_client_id": iap.Oauth2ClientId, - "oauth2_client_secret_sha256": iap.Oauth2ClientSecretSha256, - // don't set client secret, it's not returned by the API - } return []map[string]interface{}{result}, nil } diff --git a/google/resource_google_project_test.go b/google/resource_google_project_test.go index 14d5fbee..597a54ac 100644 --- a/google/resource_google_project_test.go +++ b/google/resource_google_project_test.go @@ -200,8 +200,42 @@ func TestAccProject_appEngineBasic(t *testing.T) { Config: testAccProject_appEngineBasic(pid, org), Check: resource.ComposeTestCheckFunc( testAccCheckGoogleProjectExists("google_project.acceptance", pid), + resource.TestCheckResourceAttrSet("google_project.acceptance", "app_engine.0.name"), + resource.TestCheckResourceAttrSet("google_project.acceptance", "app_engine.0.url_dispatch_rule.#"), + resource.TestCheckResourceAttrSet("google_project.acceptance", "app_engine.0.code_bucket"), + resource.TestCheckResourceAttrSet("google_project.acceptance", "app_engine.0.default_hostname"), + resource.TestCheckResourceAttrSet("google_project.acceptance", "app_engine.0.default_bucket"), ), }, + resource.TestStep{ + ResourceName: "google_project.acceptance", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccProject_appEngineFeatureSettings(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_appEngineFeatureSettings(pid, org), + Check: resource.ComposeTestCheckFunc( + testAccCheckGoogleProjectExists("google_project.acceptance", pid), + ), + }, + resource.TestStep{ + ResourceName: "google_project.acceptance", + ImportState: true, + ImportStateVerify: true, + }, }, }) } @@ -378,12 +412,27 @@ resource "google_project" "acceptance" { app_engine { auth_domain = "hashicorptest.com" location_id = "us-central" - default_cookie_expiration_seconds = 86400 serving_status = "SERVING" } }`, pid, pid, org) } +func testAccProject_appEngineFeatureSettings(pid, org string) string { + return fmt.Sprintf(` +resource "google_project" "acceptance" { + project_id = "%s" + name = "%s" + org_id = "%s" + + app_engine { + location_id = "us-central" + feature_settings { + "split_health_checks" = true + } + } +}`, pid, pid, org) +} + func skipIfEnvNotSet(t *testing.T, envs ...string) { for _, k := range envs { if os.Getenv(k) == "" { diff --git a/website/docs/r/google_project.html.markdown b/website/docs/r/google_project.html.markdown index a0193472..3fb72d7c 100755 --- a/website/docs/r/google_project.html.markdown +++ b/website/docs/r/google_project.html.markdown @@ -61,6 +61,20 @@ resource "google_folder" "department1" { } ``` +To create a project with an App Engine app attached + +```hcl +resource "google_project" "my-app-engine-app" { + name = "App Engine Project" + project_id = "app-engine-project" + org_id = "1234567" + + app_engine { + location_id = "us-central" + } +} +``` + ## Argument Reference The following arguments are supported: @@ -103,6 +117,19 @@ The following arguments are supported: name to match the GCP Console UI. Setting this field to false will enable the Compute Engine API which is required to delete the network. +* `app_engine` - (Optional) A block of configuration to enable an App Engine app. Setting this + field will enabled the App Engine Admin API, which is required to manage the app. + +The `app_engine` block has the following configuration options: + +* `location_id` - (Required) The [location](https://cloud.google.com/appengine/docs/locations) + to serve the app from. +* `auth_domain` - (Optional) The domain to authenticate users with when using App Engine's User API. +* `serving_status` - (Optional) The serving status of the app. Note that this can't be updated at the moment. +* `feature_settings` - (Optional) A block of optional settings to configure specific App Engine features: + * `split_health_checks` - (Optional) Set to false to use the legacy health check instead of the readiness + and liveness checks. + ## Attributes Reference In addition to the arguments listed above, the following computed attributes are @@ -115,6 +142,13 @@ exported: `etag` property instead; future versions of Terraform will remove the `policy_etag` attribute +* `app_engine.0.name` - Unique name of the app, usually `apps/{PROJECT_ID}` +* `app_engine.0.url_dispatch_rule` - A list of dispatch rule blocks. Each block has a `domain`, `path`, and `service` field. +* `app_engine.0.code_bucket` - The GCS bucket code is being stored in for this app. +* `app_engine.0.default_hostname` - The default hostname for this app. +* `app_engine.0.default_bucket` - The GCS bucket content is being stored in for this app. +* `app_engine.0.gcr_domain` - The GCR domain used for storing managed Docker images for this app. + ## Import Projects can be imported using the `project_id`, e.g.