blob: f6b037556b412ae09311a385af182fc569705eea [file] [log] [blame]
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package appengine
import (
"context"
"fmt"
"log"
"time"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
"github.com/hashicorp/terraform-provider-google-beta/google-beta/tpgresource"
transport_tpg "github.com/hashicorp/terraform-provider-google-beta/google-beta/transport"
"github.com/hashicorp/terraform-provider-google-beta/google-beta/verify"
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,
},
Timeouts: &schema.ResourceTimeout{
Create: schema.DefaultTimeout(4 * time.Minute),
Update: schema.DefaultTimeout(4 * time.Minute),
},
CustomizeDiff: customdiff.All(
tpgresource.DefaultProviderProject,
appEngineApplicationLocationIDCustomizeDiff,
),
Schema: map[string]*schema.Schema{
"project": {
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
ValidateFunc: verify.ValidateProjectID(),
Description: `The project ID to create the application under.`,
},
"auth_domain": {
Type: schema.TypeString,
Optional: true,
Computed: true,
Description: `The domain to authenticate users with when using App Engine's User API.`,
},
"location_id": {
Type: schema.TypeString,
Required: true,
Description: `The location to serve the app from.`,
},
"serving_status": {
Type: schema.TypeString,
Optional: true,
ValidateFunc: validation.StringInSlice([]string{
"UNSPECIFIED",
"SERVING",
"USER_DISABLED",
"SYSTEM_DISABLED",
}, false),
Computed: true,
Description: `The serving status of the app.`,
},
"database_type": {
Type: schema.TypeString,
Optional: true,
ValidateFunc: validation.StringInSlice([]string{
"CLOUD_FIRESTORE",
"CLOUD_DATASTORE_COMPATIBILITY",
// NOTE: this is provided for compatibility with instances from
// before CLOUD_DATASTORE_COMPATIBILITY - it cannot be set
// for new instances.
"CLOUD_DATASTORE",
}, false),
Computed: true,
},
"feature_settings": {
Type: schema.TypeList,
Optional: true,
Computed: true,
MaxItems: 1,
Description: `A block of optional settings to configure specific App Engine features:`,
Elem: appEngineApplicationFeatureSettingsResource(),
},
"name": {
Type: schema.TypeString,
Computed: true,
Description: `Unique name of the app.`,
},
"app_id": {
Type: schema.TypeString,
Computed: true,
Description: `Identifier of the app.`,
},
"url_dispatch_rule": {
Type: schema.TypeList,
Computed: true,
Description: `A list of dispatch rule blocks. Each block has a domain, path, and service field.`,
Elem: appEngineApplicationURLDispatchRuleResource(),
},
"code_bucket": {
Type: schema.TypeString,
Computed: true,
Description: `The GCS bucket code is being stored in for this app.`,
},
"default_hostname": {
Type: schema.TypeString,
Computed: true,
Description: `The default hostname for this app.`,
},
"default_bucket": {
Type: schema.TypeString,
Computed: true,
Description: `The GCS bucket content is being stored in for this app.`,
},
"gcr_domain": {
Type: schema.TypeString,
Computed: true,
Description: `The GCR domain used for storing managed Docker images for this app.`,
},
"iap": {
Type: schema.TypeList,
Optional: true,
Computed: true,
MaxItems: 1,
Description: `Settings for enabling Cloud Identity Aware Proxy`,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"enabled": {
Type: schema.TypeBool,
Optional: true,
Default: false,
Description: `Adapted for use with the app`,
},
"oauth2_client_id": {
Type: schema.TypeString,
Required: true,
Description: `OAuth2 client ID to use for the authentication flow.`,
},
"oauth2_client_secret": {
Type: schema.TypeString,
Required: true,
Sensitive: true,
Description: `OAuth2 client secret to use for the authentication flow. The SHA-256 hash of the value is returned in the oauth2ClientSecretSha256 field.`,
},
"oauth2_client_secret_sha256": {
Type: schema.TypeString,
Computed: true,
Sensitive: true,
Description: `Hex-encoded SHA-256 hash of the client secret.`,
},
},
},
},
},
UseJSONNumber: true,
}
}
func appEngineApplicationURLDispatchRuleResource() *schema.Resource {
return &schema.Resource{
Schema: map[string]*schema.Schema{
"domain": {
Type: schema.TypeString,
Computed: true,
},
"path": {
Type: schema.TypeString,
Computed: true,
},
"service": {
Type: schema.TypeString,
Computed: true,
},
},
}
}
func appEngineApplicationFeatureSettingsResource() *schema.Resource {
return &schema.Resource{
Schema: map[string]*schema.Schema{
"split_health_checks": {
Type: schema.TypeBool,
Required: true,
},
},
}
}
func appEngineApplicationLocationIDCustomizeDiff(_ context.Context, 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.")
}
return nil
}
func resourceAppEngineApplicationCreate(d *schema.ResourceData, meta interface{}) error {
config := meta.(*transport_tpg.Config)
userAgent, err := tpgresource.GenerateUserAgentString(d, config.UserAgent)
if err != nil {
return err
}
project, err := tpgresource.GetProject(d, config)
if err != nil {
return err
}
app, err := expandAppEngineApplication(d, project)
if err != nil {
return err
}
lockName, err := tpgresource.ReplaceVars(d, config, "apps/{{project}}")
if err != nil {
return err
}
transport_tpg.MutexStore.Lock(lockName)
defer transport_tpg.MutexStore.Unlock(lockName)
log.Printf("[DEBUG] Creating App Engine App")
op, err := config.NewAppEngineClient(userAgent).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 := AppEngineOperationWaitTime(config, op, project, "App Engine app to create", userAgent, d.Timeout(schema.TimeoutCreate))
if waitErr != nil {
d.SetId("")
return waitErr
}
log.Printf("[DEBUG] Created App Engine App")
return resourceAppEngineApplicationRead(d, meta)
}
func resourceAppEngineApplicationRead(d *schema.ResourceData, meta interface{}) error {
config := meta.(*transport_tpg.Config)
userAgent, err := tpgresource.GenerateUserAgentString(d, config.UserAgent)
if err != nil {
return err
}
pid := d.Id()
app, err := config.NewAppEngineClient(userAgent).Apps.Get(pid).Do()
if err != nil {
return transport_tpg.HandleNotFoundError(err, d, fmt.Sprintf("App Engine Application %q", pid))
}
if err := d.Set("auth_domain", app.AuthDomain); err != nil {
return fmt.Errorf("Error setting auth_domain: %s", err)
}
if err := d.Set("code_bucket", app.CodeBucket); err != nil {
return fmt.Errorf("Error setting code_bucket: %s", err)
}
if err := d.Set("default_bucket", app.DefaultBucket); err != nil {
return fmt.Errorf("Error setting default_bucket: %s", err)
}
if err := d.Set("default_hostname", app.DefaultHostname); err != nil {
return fmt.Errorf("Error setting default_hostname: %s", err)
}
if err := d.Set("location_id", app.LocationId); err != nil {
return fmt.Errorf("Error setting location_id: %s", err)
}
if err := d.Set("name", app.Name); err != nil {
return fmt.Errorf("Error setting name: %s", err)
}
if err := d.Set("app_id", app.Id); err != nil {
return fmt.Errorf("Error setting app_id: %s", err)
}
if err := d.Set("serving_status", app.ServingStatus); err != nil {
return fmt.Errorf("Error setting serving_status: %s", err)
}
if err := d.Set("gcr_domain", app.GcrDomain); err != nil {
return fmt.Errorf("Error setting gcr_domain: %s", err)
}
if err := d.Set("database_type", app.DatabaseType); err != nil {
return fmt.Errorf("Error setting database_type: %s", err)
}
if err := d.Set("project", pid); err != nil {
return fmt.Errorf("Error setting project: %s", err)
}
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/hashicorp/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/hashicorp/terraform-provider-google/issues. Error is:\n%s", err.Error())
}
iap, err := flattenAppEngineApplicationIap(d, app.Iap)
if err != nil {
return err
}
err = d.Set("iap", iap)
if err != nil {
return fmt.Errorf("Error setting iap in state. This is a bug, please report it at https://github.com/hashicorp/terraform-provider-google/issues. Error is:\n%s", err.Error())
}
return nil
}
func resourceAppEngineApplicationUpdate(d *schema.ResourceData, meta interface{}) error {
config := meta.(*transport_tpg.Config)
userAgent, err := tpgresource.GenerateUserAgentString(d, config.UserAgent)
if err != nil {
return err
}
pid := d.Id()
app, err := expandAppEngineApplication(d, pid)
if err != nil {
return err
}
lockName, err := tpgresource.ReplaceVars(d, config, "apps/{{project}}")
if err != nil {
return err
}
transport_tpg.MutexStore.Lock(lockName)
defer transport_tpg.MutexStore.Unlock(lockName)
log.Printf("[DEBUG] Updating App Engine App")
op, err := config.NewAppEngineClient(userAgent).Apps.Patch(pid, app).UpdateMask("authDomain,databaseType,servingStatus,featureSettings.splitHealthChecks,iap").Do()
if err != nil {
return fmt.Errorf("Error updating App Engine application: %s", err.Error())
}
// Wait for the operation to complete
waitErr := AppEngineOperationWaitTime(config, op, pid, "App Engine app to update", userAgent, d.Timeout(schema.TimeoutUpdate))
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),
DatabaseType: d.Get("database_type").(string),
ServingStatus: d.Get("serving_status").(string),
}
featureSettings, err := expandAppEngineApplicationFeatureSettings(d)
if err != nil {
return nil, err
}
result.FeatureSettings = featureSettings
iap, err := expandAppEngineApplicationIap(d)
if err != nil {
return nil, err
}
result.Iap = iap
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 expandAppEngineApplicationIap(d *schema.ResourceData) (*appengine.IdentityAwareProxy, error) {
blocks := d.Get("iap").([]interface{})
if len(blocks) < 1 {
return nil, nil
}
return &appengine.IdentityAwareProxy{
Enabled: d.Get("iap.0.enabled").(bool),
Oauth2ClientId: d.Get("iap.0.oauth2_client_id").(string),
Oauth2ClientSecret: d.Get("iap.0.oauth2_client_secret").(string),
Oauth2ClientSecretSha256: d.Get("iap.0.oauth2_client_secret_sha256").(string),
}, 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 flattenAppEngineApplicationIap(d *schema.ResourceData, 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": d.Get("iap.0.oauth2_client_secret"),
"oauth2_client_secret_sha256": iap.Oauth2ClientSecretSha256,
}
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
}