From f0622d834d0a8271e9bee61579d05c9402dfda92 Mon Sep 17 00:00:00 2001 From: The Magician Date: Thu, 12 Dec 2019 11:12:20 -0800 Subject: [PATCH] Add support for Deployment Manager (#5139) Signed-off-by: Modular Magician --- google/config.go | 3 + google/deployment_manager_operation.go | 84 +++ google/provider.go | 14 +- ...ine_standard_app_version_generated_test.go | 2 +- ...rce_bigtable_app_profile_generated_test.go | 4 +- ...cloud_run_domain_mapping_generated_test.go | 7 +- ...source_cloud_run_service_generated_test.go | 21 +- ...source_cloud_tasks_queue_generated_test.go | 7 +- google/resource_compute_subnetwork.go | 70 +- ...aproc_autoscaling_policy_generated_test.go | 7 +- .../resource_deployment_manager_deployment.go | 671 ++++++++++++++++++ ...urce_deployment_manager_deployment_test.go | 460 ++++++++++++ ...ource_filestore_instance_generated_test.go | 2 +- google/resource_sql_database_instance_test.go | 1 - .../service_account.yml.tmpl | 6 + ...eployment_manager_deployment.html.markdown | 236 ++++++ 16 files changed, 1530 insertions(+), 65 deletions(-) create mode 100644 google/deployment_manager_operation.go create mode 100644 google/resource_deployment_manager_deployment.go create mode 100644 google/resource_deployment_manager_deployment_test.go create mode 100644 google/test-fixtures/deploymentmanager/service_account.yml.tmpl create mode 100644 website/docs/r/deployment_manager_deployment.html.markdown diff --git a/google/config.go b/google/config.go index 4a2da57ada2..dcd689dee83 100644 --- a/google/config.go +++ b/google/config.go @@ -85,6 +85,7 @@ type Config struct { ComputeBasePath string ContainerAnalysisBasePath string DataprocBasePath string + DeploymentManagerBasePath string DNSBasePath string FilestoreBasePath string FirestoreBasePath string @@ -214,6 +215,7 @@ var CloudTasksDefaultBasePath = "https://cloudtasks.googleapis.com/v2/" var ComputeDefaultBasePath = "https://www.googleapis.com/compute/v1/" var ContainerAnalysisDefaultBasePath = "https://containeranalysis.googleapis.com/v1/" var DataprocDefaultBasePath = "https://dataproc.googleapis.com/v1/" +var DeploymentManagerDefaultBasePath = "https://www.googleapis.com/deploymentmanager/v2/" var DNSDefaultBasePath = "https://www.googleapis.com/dns/v1/" var FilestoreDefaultBasePath = "https://file.googleapis.com/v1/" var FirestoreDefaultBasePath = "https://firestore.googleapis.com/v1/" @@ -687,6 +689,7 @@ func ConfigureBasePaths(c *Config) { c.ComputeBasePath = ComputeDefaultBasePath c.ContainerAnalysisBasePath = ContainerAnalysisDefaultBasePath c.DataprocBasePath = DataprocDefaultBasePath + c.DeploymentManagerBasePath = DeploymentManagerDefaultBasePath c.DNSBasePath = DNSDefaultBasePath c.FilestoreBasePath = FilestoreDefaultBasePath c.FirestoreBasePath = FirestoreDefaultBasePath diff --git a/google/deployment_manager_operation.go b/google/deployment_manager_operation.go new file mode 100644 index 00000000000..8cf40fee844 --- /dev/null +++ b/google/deployment_manager_operation.go @@ -0,0 +1,84 @@ +package google + +import ( + "bytes" + "fmt" + "google.golang.org/api/compute/v1" +) + +type DeploymentManagerOperationWaiter struct { + Config *Config + Project string + OperationUrl string + ComputeOperationWaiter +} + +func (w *DeploymentManagerOperationWaiter) IsRetryable(error) bool { + return false +} + +func (w *DeploymentManagerOperationWaiter) QueryOp() (interface{}, error) { + if w == nil || w.Op == nil || w.Op.SelfLink == "" { + return nil, fmt.Errorf("cannot query unset/nil operation") + } + resp, err := sendRequest(w.Config, "GET", w.Project, w.Op.SelfLink, nil) + if err != nil { + return nil, err + } + op := &compute.Operation{} + if err := Convert(resp, op); err != nil { + return nil, fmt.Errorf("could not convert response to operation: %v", err) + } + return op, nil +} + +func deploymentManagerOperationWaitTime(config *Config, resp interface{}, project, activity string, timeoutMinutes int) error { + op := &compute.Operation{} + err := Convert(resp, op) + if err != nil { + return err + } + + w := &DeploymentManagerOperationWaiter{ + Config: config, + OperationUrl: op.SelfLink, + ComputeOperationWaiter: ComputeOperationWaiter{ + Project: project, + }, + } + if err := w.SetOp(op); err != nil { + return err + } + + return OperationWait(w, activity, timeoutMinutes) +} + +func (w *DeploymentManagerOperationWaiter) Error() error { + if w != nil && w.Op != nil && w.Op.Error != nil { + return DeploymentManagerOperationError{ + HTTPStatusCode: w.Op.HttpErrorStatusCode, + HTTPMessage: w.Op.HttpErrorMessage, + OperationError: *w.Op.Error, + } + } + return nil +} + +// DeploymentManagerOperationError wraps information from the compute.Operation +// in an implementation of Error. +type DeploymentManagerOperationError struct { + HTTPStatusCode int64 + HTTPMessage string + compute.OperationError +} + +func (e DeploymentManagerOperationError) Error() string { + var buf bytes.Buffer + buf.WriteString("Deployment Manager returned errors for this operation, likely due to invalid configuration.") + buf.WriteString(fmt.Sprintf("Operation failed with HTTP error %d: %s.", e.HTTPStatusCode, e.HTTPMessage)) + buf.WriteString("Errors returned: \n") + for _, err := range e.Errors { + buf.WriteString(err.Message + "\n") + } + return buf.String() +} diff --git a/google/provider.go b/google/provider.go index ec69f8fd545..c038bc7588b 100644 --- a/google/provider.go +++ b/google/provider.go @@ -221,6 +221,14 @@ func Provider() terraform.ResourceProvider { "GOOGLE_DATAPROC_CUSTOM_ENDPOINT", }, DataprocDefaultBasePath), }, + "deployment_manager_custom_endpoint": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validateCustomEndpoint, + DefaultFunc: schema.MultiEnvDefaultFunc([]string{ + "GOOGLE_DEPLOYMENT_MANAGER_CUSTOM_ENDPOINT", + }, DeploymentManagerDefaultBasePath), + }, "dns_custom_endpoint": { Type: schema.TypeString, Optional: true, @@ -460,9 +468,9 @@ func Provider() terraform.ResourceProvider { return provider } -// Generated resources: 87 +// Generated resources: 88 // Generated IAM resources: 42 -// Total generated resources: 129 +// Total generated resources: 130 func ResourceMap() map[string]*schema.Resource { resourceMap, _ := ResourceMapWithErrors() return resourceMap @@ -549,6 +557,7 @@ func ResourceMapWithErrors() (map[string]*schema.Resource, error) { "google_compute_vpn_tunnel": resourceComputeVpnTunnel(), "google_container_analysis_note": resourceContainerAnalysisNote(), "google_dataproc_autoscaling_policy": resourceDataprocAutoscalingPolicy(), + "google_deployment_manager_deployment": resourceDeploymentManagerDeployment(), "google_dns_managed_zone": resourceDNSManagedZone(), "google_filestore_instance": resourceFilestoreInstance(), "google_firestore_index": resourceFirestoreIndex(), @@ -767,6 +776,7 @@ func providerConfigure(d *schema.ResourceData, terraformVersion string) (interfa config.ComputeBasePath = d.Get("compute_custom_endpoint").(string) config.ContainerAnalysisBasePath = d.Get("container_analysis_custom_endpoint").(string) config.DataprocBasePath = d.Get("dataproc_custom_endpoint").(string) + config.DeploymentManagerBasePath = d.Get("deployment_manager_custom_endpoint").(string) config.DNSBasePath = d.Get("dns_custom_endpoint").(string) config.FilestoreBasePath = d.Get("filestore_custom_endpoint").(string) config.FirestoreBasePath = d.Get("firestore_custom_endpoint").(string) diff --git a/google/resource_app_engine_standard_app_version_generated_test.go b/google/resource_app_engine_standard_app_version_generated_test.go index fa6b9b773f6..c7af2a3debe 100644 --- a/google/resource_app_engine_standard_app_version_generated_test.go +++ b/google/resource_app_engine_standard_app_version_generated_test.go @@ -44,7 +44,7 @@ func TestAccAppEngineStandardAppVersion_appEngineStandardAppVersionExample(t *te ResourceName: "google_app_engine_standard_app_version.myapp_v1", ImportState: true, ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"threadsafe", "env_variables", "deployment", "entrypoint", "instance_class", "delete_service_on_destroy"}, + ImportStateVerifyIgnore: []string{"threadsafe", "env_variables", "deployment", "entrypoint", "instance_class", "service", "delete_service_on_destroy"}, }, }, }) diff --git a/google/resource_bigtable_app_profile_generated_test.go b/google/resource_bigtable_app_profile_generated_test.go index 28b1df74e9a..8a088351589 100644 --- a/google/resource_bigtable_app_profile_generated_test.go +++ b/google/resource_bigtable_app_profile_generated_test.go @@ -43,7 +43,7 @@ func TestAccBigtableAppProfile_bigtableAppProfileMulticlusterExample(t *testing. ResourceName: "google_bigtable_app_profile.ap", ImportState: true, ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"ignore_warnings"}, + ImportStateVerifyIgnore: []string{"app_profile_id", "instance", "ignore_warnings", "ignore_warnings"}, }, }, }) @@ -90,7 +90,7 @@ func TestAccBigtableAppProfile_bigtableAppProfileSingleclusterExample(t *testing ResourceName: "google_bigtable_app_profile.ap", ImportState: true, ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"ignore_warnings"}, + ImportStateVerifyIgnore: []string{"app_profile_id", "instance", "ignore_warnings", "ignore_warnings"}, }, }, }) diff --git a/google/resource_cloud_run_domain_mapping_generated_test.go b/google/resource_cloud_run_domain_mapping_generated_test.go index 1b2475e9751..deb4332ce1d 100644 --- a/google/resource_cloud_run_domain_mapping_generated_test.go +++ b/google/resource_cloud_run_domain_mapping_generated_test.go @@ -42,9 +42,10 @@ func TestAccCloudRunDomainMapping_cloudRunDomainMappingBasicExample(t *testing.T Config: testAccCloudRunDomainMapping_cloudRunDomainMappingBasicExample(context), }, { - ResourceName: "google_cloud_run_domain_mapping.default", - ImportState: true, - ImportStateVerify: true, + ResourceName: "google_cloud_run_domain_mapping.default", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"name", "location"}, }, }, }) diff --git a/google/resource_cloud_run_service_generated_test.go b/google/resource_cloud_run_service_generated_test.go index c4f243e98d6..b8d8947b460 100644 --- a/google/resource_cloud_run_service_generated_test.go +++ b/google/resource_cloud_run_service_generated_test.go @@ -41,9 +41,10 @@ func TestAccCloudRunService_cloudRunServiceBasicExample(t *testing.T) { Config: testAccCloudRunService_cloudRunServiceBasicExample(context), }, { - ResourceName: "google_cloud_run_service.default", - ImportState: true, - ImportStateVerify: true, + ResourceName: "google_cloud_run_service.default", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"name", "location"}, }, }, }) @@ -88,9 +89,10 @@ func TestAccCloudRunService_cloudRunServiceSqlExample(t *testing.T) { Config: testAccCloudRunService_cloudRunServiceSqlExample(context), }, { - ResourceName: "google_cloud_run_service.default", - ImportState: true, - ImportStateVerify: true, + ResourceName: "google_cloud_run_service.default", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"name", "location"}, }, }, }) @@ -146,9 +148,10 @@ func TestAccCloudRunService_cloudRunServiceNoauthExample(t *testing.T) { Config: testAccCloudRunService_cloudRunServiceNoauthExample(context), }, { - ResourceName: "google_cloud_run_service.default", - ImportState: true, - ImportStateVerify: true, + ResourceName: "google_cloud_run_service.default", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"name", "location"}, }, }, }) diff --git a/google/resource_cloud_tasks_queue_generated_test.go b/google/resource_cloud_tasks_queue_generated_test.go index a835fb39cec..9e84bb38a0a 100644 --- a/google/resource_cloud_tasks_queue_generated_test.go +++ b/google/resource_cloud_tasks_queue_generated_test.go @@ -40,9 +40,10 @@ func TestAccCloudTasksQueue_queueBasicExample(t *testing.T) { Config: testAccCloudTasksQueue_queueBasicExample(context), }, { - ResourceName: "google_cloud_tasks_queue.default", - ImportState: true, - ImportStateVerify: true, + ResourceName: "google_cloud_tasks_queue.default", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"location"}, }, }, }) diff --git a/google/resource_compute_subnetwork.go b/google/resource_compute_subnetwork.go index 5ac06de2137..88050fd9b8f 100644 --- a/google/resource_compute_subnetwork.go +++ b/google/resource_compute_subnetwork.go @@ -468,37 +468,21 @@ func resourceComputeSubnetworkUpdate(d *schema.ResourceData, meta interface{}) e d.SetPartial("ip_cidr_range") } - if d.HasChange("secondary_ip_range") { + if d.HasChange("private_ip_google_access") { obj := make(map[string]interface{}) - getUrl, err := replaceVars(d, config, "{{ComputeBasePath}}projects/{{project}}/regions/{{region}}/subnetworks/{{name}}") - if err != nil { - return err - } - - project, err := getProject(d, config) - if err != nil { - return err - } - getRes, err := sendRequest(config, "GET", project, getUrl, nil) - if err != nil { - return handleNotFoundError(err, d, fmt.Sprintf("ComputeSubnetwork %q", d.Id())) - } - - obj["fingerprint"] = getRes["fingerprint"] - - secondaryIpRangesProp, err := expandComputeSubnetworkSecondaryIpRange(d.Get("secondary_ip_range"), d, config) + privateIpGoogleAccessProp, err := expandComputeSubnetworkPrivateIpGoogleAccess(d.Get("private_ip_google_access"), d, config) if err != nil { return err - } else if v, ok := d.GetOkExists("secondary_ip_range"); ok || !reflect.DeepEqual(v, secondaryIpRangesProp) { - obj["secondaryIpRanges"] = secondaryIpRangesProp + } else if v, ok := d.GetOkExists("private_ip_google_access"); !isEmptyValue(reflect.ValueOf(v)) && (ok || !reflect.DeepEqual(v, privateIpGoogleAccessProp)) { + obj["privateIpGoogleAccess"] = privateIpGoogleAccessProp } - url, err := replaceVars(d, config, "{{ComputeBasePath}}projects/{{project}}/regions/{{region}}/subnetworks/{{name}}") + url, err := replaceVars(d, config, "{{ComputeBasePath}}projects/{{project}}/regions/{{region}}/subnetworks/{{name}}/setPrivateIpGoogleAccess") if err != nil { return err } - res, err := sendRequestWithTimeout(config, "PATCH", project, url, obj, d.Timeout(schema.TimeoutUpdate)) + res, err := sendRequestWithTimeout(config, "POST", project, url, obj, d.Timeout(schema.TimeoutUpdate)) if err != nil { return fmt.Errorf("Error updating Subnetwork %q: %s", d.Id(), err) } @@ -510,23 +494,34 @@ func resourceComputeSubnetworkUpdate(d *schema.ResourceData, meta interface{}) e return err } - d.SetPartial("secondary_ip_range") + d.SetPartial("private_ip_google_access") } - if d.HasChange("private_ip_google_access") { + if d.HasChange("log_config") { obj := make(map[string]interface{}) - privateIpGoogleAccessProp, err := expandComputeSubnetworkPrivateIpGoogleAccess(d.Get("private_ip_google_access"), d, config) + getUrl, err := replaceVars(d, config, "{{ComputeBasePath}}projects/{{project}}/regions/{{region}}/subnetworks/{{name}}") if err != nil { return err - } else if v, ok := d.GetOkExists("private_ip_google_access"); !isEmptyValue(reflect.ValueOf(v)) && (ok || !reflect.DeepEqual(v, privateIpGoogleAccessProp)) { - obj["privateIpGoogleAccess"] = privateIpGoogleAccessProp + } + getRes, err := sendRequest(config, "GET", project, getUrl, nil) + if err != nil { + return handleNotFoundError(err, d, fmt.Sprintf("ComputeSubnetwork %q", d.Id())) } - url, err := replaceVars(d, config, "{{ComputeBasePath}}projects/{{project}}/regions/{{region}}/subnetworks/{{name}}/setPrivateIpGoogleAccess") + obj["fingerprint"] = getRes["fingerprint"] + + logConfigProp, err := expandComputeSubnetworkLogConfig(d.Get("log_config"), d, config) if err != nil { return err + } else if v, ok := d.GetOkExists("log_config"); ok || !reflect.DeepEqual(v, logConfigProp) { + obj["logConfig"] = logConfigProp } - res, err := sendRequestWithTimeout(config, "POST", project, url, obj, d.Timeout(schema.TimeoutUpdate)) + + url, err := replaceVars(d, config, "{{ComputeBasePath}}projects/{{project}}/regions/{{region}}/subnetworks/{{name}}") + if err != nil { + return err + } + res, err := sendRequestWithTimeout(config, "PATCH", project, url, obj, d.Timeout(schema.TimeoutUpdate)) if err != nil { return fmt.Errorf("Error updating Subnetwork %q: %s", d.Id(), err) } @@ -538,20 +533,15 @@ func resourceComputeSubnetworkUpdate(d *schema.ResourceData, meta interface{}) e return err } - d.SetPartial("private_ip_google_access") + d.SetPartial("log_config") } - if d.HasChange("log_config") { + if d.HasChange("secondary_ip_range") { obj := make(map[string]interface{}) getUrl, err := replaceVars(d, config, "{{ComputeBasePath}}projects/{{project}}/regions/{{region}}/subnetworks/{{name}}") if err != nil { return err } - - project, err := getProject(d, config) - if err != nil { - return err - } getRes, err := sendRequest(config, "GET", project, getUrl, nil) if err != nil { return handleNotFoundError(err, d, fmt.Sprintf("ComputeSubnetwork %q", d.Id())) @@ -559,11 +549,11 @@ func resourceComputeSubnetworkUpdate(d *schema.ResourceData, meta interface{}) e obj["fingerprint"] = getRes["fingerprint"] - logConfigProp, err := expandComputeSubnetworkLogConfig(d.Get("log_config"), d, config) + secondaryIpRangesProp, err := expandComputeSubnetworkSecondaryIpRange(d.Get("secondary_ip_range"), d, config) if err != nil { return err - } else if v, ok := d.GetOkExists("log_config"); ok || !reflect.DeepEqual(v, logConfigProp) { - obj["logConfig"] = logConfigProp + } else if v, ok := d.GetOkExists("secondary_ip_range"); ok || !reflect.DeepEqual(v, secondaryIpRangesProp) { + obj["secondaryIpRanges"] = secondaryIpRangesProp } url, err := replaceVars(d, config, "{{ComputeBasePath}}projects/{{project}}/regions/{{region}}/subnetworks/{{name}}") @@ -582,7 +572,7 @@ func resourceComputeSubnetworkUpdate(d *schema.ResourceData, meta interface{}) e return err } - d.SetPartial("log_config") + d.SetPartial("secondary_ip_range") } d.Partial(false) diff --git a/google/resource_dataproc_autoscaling_policy_generated_test.go b/google/resource_dataproc_autoscaling_policy_generated_test.go index 2931e9b61eb..45d56252e17 100644 --- a/google/resource_dataproc_autoscaling_policy_generated_test.go +++ b/google/resource_dataproc_autoscaling_policy_generated_test.go @@ -40,9 +40,10 @@ func TestAccDataprocAutoscalingPolicy_dataprocAutoscalingPolicyExample(t *testin Config: testAccDataprocAutoscalingPolicy_dataprocAutoscalingPolicyExample(context), }, { - ResourceName: "google_dataproc_autoscaling_policy.asp", - ImportState: true, - ImportStateVerify: true, + ResourceName: "google_dataproc_autoscaling_policy.asp", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"location"}, }, }, }) diff --git a/google/resource_deployment_manager_deployment.go b/google/resource_deployment_manager_deployment.go new file mode 100644 index 00000000000..fd039975c03 --- /dev/null +++ b/google/resource_deployment_manager_deployment.go @@ -0,0 +1,671 @@ +// ---------------------------------------------------------------------------- +// +// *** AUTO GENERATED CODE *** AUTO GENERATED CODE *** +// +// ---------------------------------------------------------------------------- +// +// This file is automatically generated by Magic Modules and manual +// changes will be clobbered when the file is regenerated. +// +// Please read more about how to change this file in +// .github/CONTRIBUTING.md. +// +// ---------------------------------------------------------------------------- + +package google + +import ( + "fmt" + "log" + "reflect" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/helper/validation" +) + +func customDiffDeploymentManagerDeployment(d *schema.ResourceDiff, meta interface{}) error { + if preview := d.Get("preview").(bool); preview { + log.Printf("[WARN] Deployment preview set to true - Terraform will treat Deployment as recreate-only") + + if d.HasChange("preview") { + d.ForceNew("preview") + } + + if d.HasChange("target") { + d.ForceNew("target") + } + + if d.HasChange("labels") { + d.ForceNew("labels") + } + } + return nil +} + +func resourceDeploymentManagerDeployment() *schema.Resource { + return &schema.Resource{ + Create: resourceDeploymentManagerDeploymentCreate, + Read: resourceDeploymentManagerDeploymentRead, + Update: resourceDeploymentManagerDeploymentUpdate, + Delete: resourceDeploymentManagerDeploymentDelete, + + Importer: &schema.ResourceImporter{ + State: resourceDeploymentManagerDeploymentImport, + }, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(60 * time.Minute), + Update: schema.DefaultTimeout(60 * time.Minute), + Delete: schema.DefaultTimeout(60 * time.Minute), + }, + + CustomizeDiff: customDiffDeploymentManagerDeployment, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: `Unique name for the deployment`, + }, + "target": { + Type: schema.TypeList, + Required: true, + Description: `Parameters that define your deployment, including the deployment +configuration and relevant templates.`, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "config": { + Type: schema.TypeList, + Required: true, + Description: `The root configuration file to use for this deployment.`, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "content": { + Type: schema.TypeString, + Required: true, + Description: `The full YAML contents of your configuration file.`, + }, + }, + }, + }, + "imports": { + Type: schema.TypeList, + Optional: true, + Description: `Specifies import files for this configuration. This can be +used to import templates or other files. For example, you might +import a text file in order to use the file in a template.`, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "content": { + Type: schema.TypeString, + Optional: true, + Description: `The full contents of the template that you want to import.`, + }, + "name": { + Type: schema.TypeString, + Optional: true, + Description: `The name of the template to import, as declared in the YAML +configuration.`, + }, + }, + }, + }, + }, + }, + }, + "create_policy": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: validation.StringInSlice([]string{"ACQUIRE", "CREATE_OR_ACQUIRE", ""}, false), + Description: `Set the policy to use for creating new resources. Only used on +create and update. Valid values are 'CREATE_OR_ACQUIRE' (default) or +'ACQUIRE'. If set to 'ACQUIRE' and resources do not already exist, +the deployment will fail. Note that updating this field does not +actually affect the deployment, just how it is updated.`, + Default: "CREATE_OR_ACQUIRE", + }, + "delete_policy": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: validation.StringInSlice([]string{"ABANDON", "DELETE", ""}, false), + Description: `Set the policy to use for deleting new resources on update/delete. +Valid values are 'DELETE' (default) or 'ABANDON'. If 'DELETE', +resource is deleted after removal from Deployment Manager. If +'ABANDON', the resource is only removed from Deployment Manager +and is not actually deleted. Note that updating this field does not +actually change the deployment, just how it is updated.`, + Default: "DELETE", + }, + "description": { + Type: schema.TypeString, + Optional: true, + Description: `Optional user-provided description of deployment.`, + }, + "labels": { + Type: schema.TypeSet, + Optional: true, + Description: `Key-value pairs to apply to this labels.`, + Elem: deploymentmanagerDeploymentLabelsSchema(), + // Default schema.HashSchema is used. + }, + "preview": { + Type: schema.TypeBool, + Optional: true, + Description: `If set to true, a deployment is created with "shell" resources +that are not actually instantiated. This allows you to preview a +deployment. It can be updated to false to actually deploy +with real resources. + ~>**NOTE**: Deployment Manager does not allow update +of a deployment in preview (unless updating to preview=false). Thus, +Terraform will force-recreate deployments if either preview is updated +to true or if other fields are updated while preview is true.`, + Default: false, + }, + "deployment_id": { + Type: schema.TypeString, + Computed: true, + Description: `Unique identifier for deployment. Output only.`, + }, + "manifest": { + Type: schema.TypeString, + Computed: true, + Description: `Output only. URL of the manifest representing the last manifest that +was successfully deployed.`, + }, + "self_link": { + Type: schema.TypeString, + Computed: true, + Description: `Output only. Server defined URL for the resource.`, + }, + "project": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + }, + } +} + +func deploymentmanagerDeploymentLabelsSchema() *schema.Resource { + return &schema.Resource{ + Schema: map[string]*schema.Schema{ + "key": { + Type: schema.TypeString, + Optional: true, + Description: `Key for label.`, + }, + "value": { + Type: schema.TypeString, + Optional: true, + Description: `Value of label.`, + }, + }, + } +} + +func resourceDeploymentManagerDeploymentCreate(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + obj := make(map[string]interface{}) + nameProp, err := expandDeploymentManagerDeploymentName(d.Get("name"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("name"); !isEmptyValue(reflect.ValueOf(nameProp)) && (ok || !reflect.DeepEqual(v, nameProp)) { + obj["name"] = nameProp + } + descriptionProp, err := expandDeploymentManagerDeploymentDescription(d.Get("description"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("description"); !isEmptyValue(reflect.ValueOf(descriptionProp)) && (ok || !reflect.DeepEqual(v, descriptionProp)) { + obj["description"] = descriptionProp + } + labelsProp, err := expandDeploymentManagerDeploymentLabels(d.Get("labels"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("labels"); ok || !reflect.DeepEqual(v, labelsProp) { + obj["labels"] = labelsProp + } + targetProp, err := expandDeploymentManagerDeploymentTarget(d.Get("target"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("target"); !isEmptyValue(reflect.ValueOf(targetProp)) && (ok || !reflect.DeepEqual(v, targetProp)) { + obj["target"] = targetProp + } + + url, err := replaceVars(d, config, "{{DeploymentManagerBasePath}}projects/{{project}}/global/deployments?preview={{preview}}&createPolicy={{create_policy}}") + if err != nil { + return err + } + + log.Printf("[DEBUG] Creating new Deployment: %#v", obj) + project, err := getProject(d, config) + if err != nil { + return err + } + res, err := sendRequestWithTimeout(config, "POST", project, url, obj, d.Timeout(schema.TimeoutCreate)) + if err != nil { + return fmt.Errorf("Error creating Deployment: %s", err) + } + + // Store the ID now + id, err := replaceVars(d, config, "projects/{{project}}/deployments/{{name}}") + if err != nil { + return fmt.Errorf("Error constructing id: %s", err) + } + d.SetId(id) + + err = deploymentManagerOperationWaitTime( + config, res, project, "Creating Deployment", + int(d.Timeout(schema.TimeoutCreate).Minutes())) + + if err != nil { + resourceDeploymentManagerDeploymentPostCreateFailure(d, meta) + // The resource didn't actually create + d.SetId("") + return fmt.Errorf("Error waiting to create Deployment: %s", err) + } + + log.Printf("[DEBUG] Finished creating Deployment %q: %#v", d.Id(), res) + + return resourceDeploymentManagerDeploymentRead(d, meta) +} + +func resourceDeploymentManagerDeploymentRead(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + url, err := replaceVars(d, config, "{{DeploymentManagerBasePath}}projects/{{project}}/global/deployments/{{name}}") + if err != nil { + return err + } + + project, err := getProject(d, config) + if err != nil { + return err + } + res, err := sendRequest(config, "GET", project, url, nil) + if err != nil { + return handleNotFoundError(err, d, fmt.Sprintf("DeploymentManagerDeployment %q", d.Id())) + } + + if err := d.Set("project", project); err != nil { + return fmt.Errorf("Error reading Deployment: %s", err) + } + + if err := d.Set("name", flattenDeploymentManagerDeploymentName(res["name"], d)); err != nil { + return fmt.Errorf("Error reading Deployment: %s", err) + } + if err := d.Set("description", flattenDeploymentManagerDeploymentDescription(res["description"], d)); err != nil { + return fmt.Errorf("Error reading Deployment: %s", err) + } + if err := d.Set("labels", flattenDeploymentManagerDeploymentLabels(res["labels"], d)); err != nil { + return fmt.Errorf("Error reading Deployment: %s", err) + } + if err := d.Set("deployment_id", flattenDeploymentManagerDeploymentDeploymentId(res["id"], d)); err != nil { + return fmt.Errorf("Error reading Deployment: %s", err) + } + if err := d.Set("manifest", flattenDeploymentManagerDeploymentManifest(res["manifest"], d)); err != nil { + return fmt.Errorf("Error reading Deployment: %s", err) + } + if err := d.Set("self_link", flattenDeploymentManagerDeploymentSelfLink(res["selfLink"], d)); err != nil { + return fmt.Errorf("Error reading Deployment: %s", err) + } + + return nil +} + +func resourceDeploymentManagerDeploymentUpdate(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + project, err := getProject(d, config) + if err != nil { + return err + } + + d.Partial(true) + + if d.HasChange("preview") { + obj := make(map[string]interface{}) + + getUrl, err := replaceVars(d, config, "{{DeploymentManagerBasePath}}projects/{{project}}/global/deployments/{{name}}") + if err != nil { + return err + } + getRes, err := sendRequest(config, "GET", project, getUrl, nil) + if err != nil { + return handleNotFoundError(err, d, fmt.Sprintf("DeploymentManagerDeployment %q", d.Id())) + } + + obj["fingerprint"] = getRes["fingerprint"] + + url, err := replaceVars(d, config, "{{DeploymentManagerBasePath}}projects/{{project}}/global/deployments/{{name}}?preview={{preview}}&createPolicy={{create_policy}}&deletePolicy={{delete_policy}}") + if err != nil { + return err + } + res, err := sendRequestWithTimeout(config, "PATCH", project, url, obj, d.Timeout(schema.TimeoutUpdate)) + if err != nil { + return fmt.Errorf("Error updating Deployment %q: %s", d.Id(), err) + } + + err = deploymentManagerOperationWaitTime( + config, res, project, "Updating Deployment", + int(d.Timeout(schema.TimeoutUpdate).Minutes())) + if err != nil { + return err + } + + d.SetPartial("preview") + } + if d.HasChange("description") || d.HasChange("labels") || d.HasChange("target") { + obj := make(map[string]interface{}) + + getUrl, err := replaceVars(d, config, "{{DeploymentManagerBasePath}}projects/{{project}}/global/deployments/{{name}}") + if err != nil { + return err + } + getRes, err := sendRequest(config, "GET", project, getUrl, nil) + if err != nil { + return handleNotFoundError(err, d, fmt.Sprintf("DeploymentManagerDeployment %q", d.Id())) + } + + obj["fingerprint"] = getRes["fingerprint"] + + descriptionProp, err := expandDeploymentManagerDeploymentDescription(d.Get("description"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("description"); !isEmptyValue(reflect.ValueOf(v)) && (ok || !reflect.DeepEqual(v, descriptionProp)) { + obj["description"] = descriptionProp + } + labelsProp, err := expandDeploymentManagerDeploymentLabels(d.Get("labels"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("labels"); ok || !reflect.DeepEqual(v, labelsProp) { + obj["labels"] = labelsProp + } + targetProp, err := expandDeploymentManagerDeploymentTarget(d.Get("target"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("target"); !isEmptyValue(reflect.ValueOf(v)) && (ok || !reflect.DeepEqual(v, targetProp)) { + obj["target"] = targetProp + } + + url, err := replaceVars(d, config, "{{DeploymentManagerBasePath}}projects/{{project}}/global/deployments/{{name}}?preview={{preview}}&createPolicy={{create_policy}}&deletePolicy={{delete_policy}}") + if err != nil { + return err + } + res, err := sendRequestWithTimeout(config, "PATCH", project, url, obj, d.Timeout(schema.TimeoutUpdate)) + if err != nil { + return fmt.Errorf("Error updating Deployment %q: %s", d.Id(), err) + } + + err = deploymentManagerOperationWaitTime( + config, res, project, "Updating Deployment", + int(d.Timeout(schema.TimeoutUpdate).Minutes())) + if err != nil { + return err + } + + d.SetPartial("description") + d.SetPartial("labels") + d.SetPartial("target") + } + + d.Partial(false) + + return resourceDeploymentManagerDeploymentRead(d, meta) +} + +func resourceDeploymentManagerDeploymentDelete(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + project, err := getProject(d, config) + if err != nil { + return err + } + + url, err := replaceVars(d, config, "{{DeploymentManagerBasePath}}projects/{{project}}/global/deployments/{{name}}?deletePolicy={{delete_policy}}") + if err != nil { + return err + } + + var obj map[string]interface{} + log.Printf("[DEBUG] Deleting Deployment %q", d.Id()) + + res, err := sendRequestWithTimeout(config, "DELETE", project, url, obj, d.Timeout(schema.TimeoutDelete)) + if err != nil { + return handleNotFoundError(err, d, "Deployment") + } + + err = deploymentManagerOperationWaitTime( + config, res, project, "Deleting Deployment", + int(d.Timeout(schema.TimeoutDelete).Minutes())) + + if err != nil { + return err + } + + log.Printf("[DEBUG] Finished deleting Deployment %q: %#v", d.Id(), res) + return nil +} + +func resourceDeploymentManagerDeploymentImport(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + config := meta.(*Config) + if err := parseImportId([]string{ + "projects/(?P[^/]+)/deployments/(?P[^/]+)", + "(?P[^/]+)/(?P[^/]+)", + "(?P[^/]+)", + }, d, config); err != nil { + return nil, err + } + + // Replace import id for the resource id + id, err := replaceVars(d, config, "projects/{{project}}/deployments/{{name}}") + if err != nil { + return nil, fmt.Errorf("Error constructing id: %s", err) + } + d.SetId(id) + + return []*schema.ResourceData{d}, nil +} + +func flattenDeploymentManagerDeploymentName(v interface{}, d *schema.ResourceData) interface{} { + return v +} + +func flattenDeploymentManagerDeploymentDescription(v interface{}, d *schema.ResourceData) interface{} { + return v +} + +func flattenDeploymentManagerDeploymentLabels(v interface{}, d *schema.ResourceData) interface{} { + if v == nil { + return v + } + l := v.([]interface{}) + transformed := schema.NewSet(schema.HashResource(deploymentmanagerDeploymentLabelsSchema()), []interface{}{}) + for _, raw := range l { + original := raw.(map[string]interface{}) + if len(original) < 1 { + // Do not include empty json objects coming back from the api + continue + } + transformed.Add(map[string]interface{}{ + "key": flattenDeploymentManagerDeploymentLabelsKey(original["key"], d), + "value": flattenDeploymentManagerDeploymentLabelsValue(original["value"], d), + }) + } + return transformed +} +func flattenDeploymentManagerDeploymentLabelsKey(v interface{}, d *schema.ResourceData) interface{} { + return v +} + +func flattenDeploymentManagerDeploymentLabelsValue(v interface{}, d *schema.ResourceData) interface{} { + return v +} + +func flattenDeploymentManagerDeploymentDeploymentId(v interface{}, d *schema.ResourceData) interface{} { + return v +} + +func flattenDeploymentManagerDeploymentManifest(v interface{}, d *schema.ResourceData) interface{} { + return v +} + +func flattenDeploymentManagerDeploymentSelfLink(v interface{}, d *schema.ResourceData) interface{} { + return v +} + +func expandDeploymentManagerDeploymentName(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandDeploymentManagerDeploymentDescription(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandDeploymentManagerDeploymentLabels(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + v = v.(*schema.Set).List() + l := v.([]interface{}) + req := make([]interface{}, 0, len(l)) + for _, raw := range l { + if raw == nil { + continue + } + original := raw.(map[string]interface{}) + transformed := make(map[string]interface{}) + + transformedKey, err := expandDeploymentManagerDeploymentLabelsKey(original["key"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedKey); val.IsValid() && !isEmptyValue(val) { + transformed["key"] = transformedKey + } + + transformedValue, err := expandDeploymentManagerDeploymentLabelsValue(original["value"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedValue); val.IsValid() && !isEmptyValue(val) { + transformed["value"] = transformedValue + } + + req = append(req, transformed) + } + return req, nil +} + +func expandDeploymentManagerDeploymentLabelsKey(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandDeploymentManagerDeploymentLabelsValue(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandDeploymentManagerDeploymentTarget(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + l := v.([]interface{}) + if len(l) == 0 || l[0] == nil { + return nil, nil + } + raw := l[0] + original := raw.(map[string]interface{}) + transformed := make(map[string]interface{}) + + transformedConfig, err := expandDeploymentManagerDeploymentTargetConfig(original["config"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedConfig); val.IsValid() && !isEmptyValue(val) { + transformed["config"] = transformedConfig + } + + transformedImports, err := expandDeploymentManagerDeploymentTargetImports(original["imports"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedImports); val.IsValid() && !isEmptyValue(val) { + transformed["imports"] = transformedImports + } + + return transformed, nil +} + +func expandDeploymentManagerDeploymentTargetConfig(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + l := v.([]interface{}) + if len(l) == 0 || l[0] == nil { + return nil, nil + } + raw := l[0] + original := raw.(map[string]interface{}) + transformed := make(map[string]interface{}) + + transformedContent, err := expandDeploymentManagerDeploymentTargetConfigContent(original["content"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedContent); val.IsValid() && !isEmptyValue(val) { + transformed["content"] = transformedContent + } + + return transformed, nil +} + +func expandDeploymentManagerDeploymentTargetConfigContent(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandDeploymentManagerDeploymentTargetImports(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + l := v.([]interface{}) + req := make([]interface{}, 0, len(l)) + for _, raw := range l { + if raw == nil { + continue + } + original := raw.(map[string]interface{}) + transformed := make(map[string]interface{}) + + transformedContent, err := expandDeploymentManagerDeploymentTargetImportsContent(original["content"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedContent); val.IsValid() && !isEmptyValue(val) { + transformed["content"] = transformedContent + } + + transformedName, err := expandDeploymentManagerDeploymentTargetImportsName(original["name"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedName); val.IsValid() && !isEmptyValue(val) { + transformed["name"] = transformedName + } + + req = append(req, transformed) + } + return req, nil +} + +func expandDeploymentManagerDeploymentTargetImportsContent(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandDeploymentManagerDeploymentTargetImportsName(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func resourceDeploymentManagerDeploymentPostCreateFailure(d *schema.ResourceData, meta interface{}) { + log.Printf("[WARN] Attempt to clean up Deployment if it still exists") + var cleanErr error + if cleanErr = resourceDeploymentManagerDeploymentRead(d, meta); cleanErr == nil { + if d.Id() != "" { + log.Printf("[WARN] Deployment %q still exists, attempting to delete...", d.Id()) + if cleanErr = resourceDeploymentManagerDeploymentDelete(d, meta); cleanErr == nil { + log.Printf("[WARN] Invalid Deployment was successfully deleted") + d.SetId("") + } + } + } + if cleanErr != nil { + log.Printf("[WARN] Could not confirm cleanup of Deployment if created in error state: %v", cleanErr) + } +} diff --git a/google/resource_deployment_manager_deployment_test.go b/google/resource_deployment_manager_deployment_test.go new file mode 100644 index 00000000000..a2a84cc558d --- /dev/null +++ b/google/resource_deployment_manager_deployment_test.go @@ -0,0 +1,460 @@ +package google + +import ( + "bytes" + "fmt" + "github.com/hashicorp/terraform-plugin-sdk/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/terraform" + "io/ioutil" + "regexp" + "strings" + "testing" +) + +func TestAccDeploymentManagerDeployment_basicFile(t *testing.T) { + t.Parallel() + + randSuffix := acctest.RandString(10) + deploymentId := "tf-dm-" + randSuffix + accountId := "tf-dm-account-" + randSuffix + yamlPath := createYamlConfigFileForTest(t, "test-fixtures/deploymentmanager/service_account.yml.tmpl", map[string]interface{}{ + "account_id": accountId, + }) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: resource.ComposeTestCheckFunc( + testAccCheckDeploymentManagerDeploymentDestroy, + testDeploymentManagerDeploymentVerifyServiceAccountMissing(accountId)), + Steps: []resource.TestStep{ + { + Config: testAccDeploymentManagerDeployment_basicFile(deploymentId, yamlPath), + }, + { + ResourceName: "google_deployment_manager_deployment.deployment", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"target", "create_policy", "delete_policy", "preview"}, + }, + }, + }) +} + +func TestAccDeploymentManagerDeployment_deleteInvalidOnCreate(t *testing.T) { + t.Parallel() + + randStr := acctest.RandString(10) + deploymentName := "tf-dm-" + randStr + accountId := "tf-dm-" + randStr + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckDeploymentManagerDestroyInvalidDeployment(deploymentName), + Steps: []resource.TestStep{ + { + Config: testAccDeploymentManagerDeployment_invalidCreatePolicy(deploymentName, accountId), + ExpectError: regexp.MustCompile("BAD REQUEST"), + }, + }, + }) +} + +func TestAccDeploymentManagerDeployment_createDeletePolicy(t *testing.T) { + t.Parallel() + + randStr := acctest.RandString(10) + deploymentName := "tf-dm-" + randStr + accountId := "tf-dm-" + randStr + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckDeploymentManagerDeploymentDestroy, + Steps: []resource.TestStep{ + { + Config: testAccDeploymentManagerDeployment_createDeletePolicy(deploymentName, accountId), + }, + { + ResourceName: "google_deployment_manager_deployment.deployment", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"target", "create_policy", "delete_policy", "preview"}, + }, + }, + }) +} + +func TestAccDeploymentManagerDeployment_imports(t *testing.T) { + t.Parallel() + + randStr := acctest.RandString(10) + deploymentName := "tf-dm-" + randStr + accountId := "tf-dm-" + randStr + importFilepath := createYamlConfigFileForTest(t, "test-fixtures/deploymentmanager/service_account.yml.tmpl", map[string]interface{}{ + "account_id": "{{ env['name'] }}", + }) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: resource.ComposeTestCheckFunc( + testAccCheckDeploymentManagerDeploymentDestroy, + testDeploymentManagerDeploymentVerifyServiceAccountMissing(accountId)), + Steps: []resource.TestStep{ + { + Config: testAccDeploymentManagerDeployment_imports(deploymentName, accountId, importFilepath), + Check: testDeploymentManagerDeploymentVerifyServiceAccountExists(accountId), + }, + { + ResourceName: "google_deployment_manager_deployment.deployment", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"target", "create_policy", "delete_policy", "preview"}, + }, + }, + }) +} + +func TestAccDeploymentManagerDeployment_update(t *testing.T) { + t.Parallel() + + randStr := acctest.RandString(10) + deploymentName := "tf-dm-" + randStr + accountId := "tf-dm-first" + randStr + accountId2 := "tf-dm-second" + randStr + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: resource.ComposeTestCheckFunc( + testAccCheckDeploymentManagerDeploymentDestroy, + testDeploymentManagerDeploymentVerifyServiceAccountMissing(accountId)), + Steps: []resource.TestStep{ + { + Config: testAccDeploymentManagerDeployment_preview(deploymentName, accountId), + Check: testDeploymentManagerDeploymentVerifyServiceAccountMissing(accountId), + }, + { + ResourceName: "google_deployment_manager_deployment.deployment", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"target", "create_policy", "delete_policy", "preview"}, + }, + { + Config: testAccDeploymentManagerDeployment_previewUpdated(deploymentName, accountId2), + Check: testDeploymentManagerDeploymentVerifyServiceAccountMissing(accountId2), + }, + { + ResourceName: "google_deployment_manager_deployment.deployment", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"target", "create_policy", "delete_policy", "preview"}, + }, + { + // Turn preview to false + Config: testAccDeploymentManagerDeployment_deployed(deploymentName, accountId), + Check: testDeploymentManagerDeploymentVerifyServiceAccountExists(accountId), + }, + { + ResourceName: "google_deployment_manager_deployment.deployment", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"target", "create_policy", "delete_policy", "preview"}, + }, + }, + }) +} + +func testAccDeploymentManagerDeployment_basicFile(deploymentName, yamlPath string) string { + return fmt.Sprintf(` +resource "google_deployment_manager_deployment" "deployment" { + name = "%s" + + target { + config { + content = file("%s") + } + } + + labels { + key = "foo" + value = "bar" + } +} +`, deploymentName, yamlPath) +} + +func testAccDeploymentManagerDeployment_invalidCreatePolicy(deployment, accountId string) string { + // The service account doesn't exist, so create policy acquire fails + return fmt.Sprintf(` +resource "google_deployment_manager_deployment" "deployment" { + name = "%s" + create_policy = "ACQUIRE" + + target { + config { + content = < **Warning:** Deployment Manager shares similar behavior with Terraform as both +products manage GCP resource lifecycle and state. This Terraform +resource is intended only to manage a Deployment resource, +and attempts to manage the Deployment's resources in Terraform as well +will likely result in errors or unexpected behavior as the two tools +fight over ownership. We strongly discourage doing so unless you are an +experienced user of both tools. + +In addition, due to limitations of the API, Terraform will treat +deployments in preview as recreate-only for any update operation other +than actually deploying an in-preview deployment (i.e. `preview=true` to +`preview=false`). + +## Example Usage - Deployment Manager Deployment Basic + + +```hcl +resource "google_deployment_manager_deployment" "deployment" { + name = "my-deployment" + + target { + config { + content = file("path/to/config.yml") + } + } + + labels { + key = "foo" + value = "bar" + } +} +``` +## Example Usage - Deployment Manager Deployment Imports + + +```hcl +resource "google_deployment_manager_deployment" "deployment" { + name = "my-deployment" + target { + config { + content = <**NOTE**: Deployment Manager does not allow update + of a deployment in preview (unless updating to preview=false). Thus, + Terraform will force-recreate deployments if either preview is updated + to true or if other fields are updated while preview is true. + +* `project` - (Optional) The ID of the project in which the resource belongs. + If it is not provided, the provider project is used. + + +The `labels` block supports: + +* `key` - + (Optional) + Key for label. + +* `value` - + (Optional) + Value of label. + +## Attributes Reference + +In addition to the arguments listed above, the following computed attributes are exported: + + +* `deployment_id` - + Unique identifier for deployment. Output only. + +* `manifest` - + Output only. URL of the manifest representing the last manifest that + was successfully deployed. + +* `self_link` - + Output only. Server defined URL for the resource. + + +## Timeouts + +This resource provides the following +[Timeouts](/docs/configuration/resources.html#timeouts) configuration options: + +- `create` - Default is 60 minutes. +- `update` - Default is 60 minutes. +- `delete` - Default is 60 minutes. + +## Import + +Deployment can be imported using any of these accepted formats: + +``` +$ terraform import google_deployment_manager_deployment.default projects/{{project}}/deployments/{{name}} +$ terraform import google_deployment_manager_deployment.default {{project}}/{{name}} +$ terraform import google_deployment_manager_deployment.default {{name}} +``` + +-> If you're importing a resource with beta features, make sure to include `-provider=google-beta` +as an argument so that Terraform uses the correct provider to import your resource. + +## User Project Overrides + +This resource supports [User Project Overrides](https://www.terraform.io/docs/providers/google/guides/provider_reference.html#user_project_override).