Beginnings of App Engine app support.

This should have all the code, but who really knows if it works or not,
tbh.
This commit is contained in:
Paddy Carver 2018-05-10 15:01:22 -07:00
parent 07ff71f287
commit 765d9af0a3
3 changed files with 437 additions and 1 deletions

View File

@ -0,0 +1,74 @@
package google
import (
"fmt"
"log"
"strconv"
"time"
"github.com/hashicorp/terraform/helper/resource"
"google.golang.org/api/appengine/v1"
)
type AppEngineOperationWaiter struct {
Service *appengine.APIService
Op *appengine.Operation
AppId string
}
func (w *AppEngineOperationWaiter) RefreshFunc() resource.StateRefreshFunc {
return func() (interface{}, string, error) {
op, err := w.Service.Apps.Operations.Get(w.AppId, w.Op.Name).Do()
if err != nil {
return nil, "", err
}
log.Printf("[DEBUG] Got %v when asking for operation %q", op.Done, w.Op.Name)
return op, strconv.FormatBool(op.Done), nil
}
}
func (w *AppEngineOperationWaiter) Conf() *resource.StateChangeConf {
return &resource.StateChangeConf{
Pending: []string{"false"},
Target: []string{"true"},
Refresh: w.RefreshFunc(),
}
}
// AppEngineOperationError wraps appengine.Status and implements the
// error interface so it can be returned.
type AppEngineOperationError appengine.Status
func (e AppEngineOperationError) Error() string {
return e.Message
}
func appEngineOperationWait(client *appengine.APIService, op *appengine.Operation, appId, activity string) error {
return appEngineOperationWaitTime(client, op, appId, activity, 4)
}
func appEngineOperationWaitTime(client *appengine.APIService, op *appengine.Operation, appId, activity string, timeoutMin int) error {
w := &AppEngineOperationWaiter{
Service: client,
Op: op,
AppId: appId,
}
state := w.Conf()
state.Delay = 10 * time.Second
state.Timeout = time.Duration(timeoutMin) * time.Minute
state.MinTimeout = 2 * time.Second
opRaw, err := state.WaitForState()
if err != nil {
return fmt.Errorf("Error waiting for %s: %s", activity, err)
}
resultOp := opRaw.(*appengine.Operation)
if resultOp.Error != nil {
return AppEngineOperationError(*resultOp.Error)
}
return nil
}

View File

@ -15,6 +15,7 @@ import (
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
"golang.org/x/oauth2/jwt"
appengine "google.golang.org/api/appengine/v1"
"google.golang.org/api/bigquery/v2"
"google.golang.org/api/cloudbilling/v1"
"google.golang.org/api/cloudfunctions/v1"
@ -76,6 +77,7 @@ type Config struct {
clientBigQuery *bigquery.Service
clientCloudFunctions *cloudfunctions.Service
clientCloudIoT *cloudiot.Service
clientAppEngine *appengine.APIService
bigtableClientFactory *BigtableClientFactory
}
@ -315,6 +317,13 @@ func (c *Config) loadAndValidate() error {
}
c.clientCloudIoT.UserAgent = userAgent
log.Printf("[INFO] Instantiating App Engine Client...")
c.clientAppEngine, err = appengine.New(client)
if err != nil {
return err
}
c.clientAppEngine.UserAgent = userAgent
return nil
}

View File

@ -1,6 +1,7 @@
package google
import (
"crypto/sha256"
"fmt"
"log"
"net/http"
@ -9,6 +10,8 @@ import (
"time"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/helper/validation"
appengine "google.golang.org/api/appengine/v1"
"google.golang.org/api/cloudbilling/v1"
"google.golang.org/api/cloudresourcemanager/v1"
"google.golang.org/api/googleapi"
@ -28,7 +31,8 @@ func resourceGoogleProject() *schema.Resource {
Importer: &schema.ResourceImporter{
State: resourceProjectImportState,
},
MigrateState: resourceGoogleProjectMigrateState,
MigrateState: resourceGoogleProjectMigrateState,
CustomizeDiff: resourceGoogleProjectCustomizeDiff,
Schema: map[string]*schema.Schema{
"project_id": &schema.Schema{
@ -86,10 +90,161 @@ func resourceGoogleProject() *schema.Resource {
Elem: &schema.Schema{Type: schema.TypeString},
Set: schema.HashString,
},
"app_engine": &schema.Schema{
Type: schema.TypeList,
Optional: true,
Elem: appEngineResource(),
MaxItems: 1,
},
},
}
}
func appEngineResource() *schema.Resource {
return &schema.Resource{
Schema: map[string]*schema.Schema{
"name": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
"url_dispatch_rule": &schema.Schema{
Type: schema.TypeList,
Computed: true,
Elem: appEngineURLDispatchRuleResource(),
},
"auth_domain": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"location_id": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
ValidateFunc: validation.StringInSlice([]string{
"northamerica-northeast1",
"us-central",
"us-east1",
"us-east4",
"southamerica-east1",
"europe-west",
"europe-west2",
"europe-west3",
"asia-northeast1",
"asia-south1",
"australia-southeast1",
}, false),
},
"code_bucket": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
"default_cookie_expiration_seconds": &schema.Schema{
Type: schema.TypeFloat,
Optional: true,
},
"serving_status": &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,
},
"iap": &schema.Schema{
Type: schema.TypeList,
Optional: true,
MaxItems: 1,
Elem: appEngineIAPResource(),
},
"gcr_domain": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"feature_settings": &schema.Schema{
Type: schema.TypeList,
Optional: true,
MaxItems: 1,
Elem: appEngineFeatureSettingsResource(),
},
},
}
}
func appEngineURLDispatchRuleResource() *schema.Resource {
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 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{
"split_health_checks": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
},
},
}
}
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
}
func resourceGoogleProjectCreate(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)
@ -153,6 +308,30 @@ 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
}
@ -207,6 +386,22 @@ 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())
}
appBlocks, err := flattenAppEngineApp(app)
if err != nil {
return fmt.Errorf("Error serializing App Engine app: %s", err.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 nil
}
@ -308,6 +503,25 @@ func resourceGoogleProjectUpdate(d *schema.ResourceData, meta interface{}) error
if err != nil {
return fmt.Errorf("Error updating project %q: %s", project_name, err)
}
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")
}
d.Partial(false)
@ -399,3 +613,142 @@ func updateProjectBillingAccount(d *schema.ResourceData, config *Config) error {
}
return nil
}
func expandAppEngineApp(d *schema.ResourceData) (*appengine.Application, error) {
blocks := d.Get("app_engine").([]interface{})
if len(blocks) < 1 {
return nil, nil
}
if len(blocks) > 1 {
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),
}
if v, ok := d.GetOkExists("app_engine.0.default_cookie_expiration_seconds"); ok {
result.DefaultCookieExpiration = strconv.FormatFloat(v.(float64), 'f', 9, 64) + "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
}
result.FeatureSettings = featureSettings
return result, nil
}
func flattenAppEngineApp(app *appengine.Application) ([]map[string]interface{}, error) {
result := map[string]interface{}{
"auth_domain": app.AuthDomain,
"code_bucket": app.CodeBucket,
"default_bucket": app.DefaultBucket,
"default_hostname": app.DefaultHostname,
"location_id": app.LocationId,
"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
}
result["dispatch_rule"] = dispatchRules
featureSettings, err := flattenAppEngineFeatureSettings(app.FeatureSettings)
if err != nil {
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
}
func expandAppEngineFeatureSettings(d *schema.ResourceData, prefix string) (*appengine.FeatureSettings, error) {
blocks := d.Get(prefix + "feature_settings").([]interface{})
if len(blocks) < 1 {
return nil, nil
}
if len(blocks) > 1 {
return nil, fmt.Errorf("only one feature_settings block may be defined per app")
}
return &appengine.FeatureSettings{
SplitHealthChecks: d.Get(prefix + "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 flattenAppEngineFeatureSettings(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 flattenAppEngineDispatchRules(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
}