mirror of
https://github.com/letic/terraform-provider-google.git
synced 2024-10-03 01:01:06 +00:00
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:
parent
07ff71f287
commit
765d9af0a3
74
google/appengine_operation.go
Normal file
74
google/appengine_operation.go
Normal 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
|
||||
}
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user