Skip to content

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

Merged
merged 4 commits into from
Oct 3, 2018
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions google/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ func Provider() terraform.ResourceProvider {
GeneratedRedisResourcesMap,
GeneratedResourceManagerResourcesMap,
map[string]*schema.Resource{
"google_app_engine_application": resourceAppEngineApplication(),
"google_bigquery_dataset": resourceBigQueryDataset(),
"google_bigquery_table": resourceBigQueryTable(),
"google_bigtable_instance": resourceBigtableInstance(),
Expand Down
278 changes: 278 additions & 0 deletions google/resource_app_engine_application.go
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{
Copy link
Contributor

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

Copy link
Contributor Author

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.

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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: any reason these are funcs and not vars?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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
Copy link
Contributor

Choose a reason for hiding this comment

The 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Either we need to d.SetId("") here or move the d.SetId(project) call to after this. I'd recommend the first just for consistency with the rest of the resources.

}
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) {
Copy link
Contributor

Choose a reason for hiding this comment

The 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 handleNotFoundError call

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.")
Copy link
Contributor

Choose a reason for hiding this comment

The 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 {
Copy link
Contributor

Choose a reason for hiding this comment

The 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
}
77 changes: 77 additions & 0 deletions google/resource_app_engine_application_test.go
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)
}
15 changes: 7 additions & 8 deletions google/resource_google_project.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,10 +92,12 @@ func resourceGoogleProject() *schema.Resource {
Set: schema.HashString,
},
"app_engine": &schema.Schema{
Type: schema.TypeList,
Optional: true,
Elem: appEngineResource(),
MaxItems: 1,
Type: schema.TypeList,
Optional: true,
Computed: true,
Elem: appEngineResource(),
MaxItems: 1,
Deprecated: "Use the google_app_engine_application resource instead.",
},
},
}
Expand Down Expand Up @@ -206,10 +208,7 @@ func appEngineFeatureSettingsResource() *schema.Resource {
}

func resourceGoogleProjectCustomizeDiff(diff *schema.ResourceDiff, meta interface{}) error {
if old, new := diff.GetChange("app_engine.#"); old != nil && new != nil && old.(int) > 0 && new.(int) < 1 {
// if we're going from app_engine set to unset, we need to delete the project, app_engine has no delete
return diff.ForceNew("app_engine")
} else if old, _ := diff.GetChange("app_engine.0.location_id"); diff.HasChange("app_engine.0.location_id") && old != nil && old.(string) != "" {
if old, _ := diff.GetChange("app_engine.0.location_id"); diff.HasChange("app_engine.0.location_id") && old != nil && old.(string) != "" {
// if location_id was already set, and has a new value, that forces a new app
// if location_id wasn't set, don't force a new value, as we're just enabling app engine
return diff.ForceNew("app_engine.0.location_id")
Expand Down
Loading