diff --git a/mmv1/products/saasservicemgmt/Saas.yaml b/mmv1/products/saasservicemgmt/Saas.yaml index 41ec4ec19b9e..9dd67d9d94c7 100644 --- a/mmv1/products/saasservicemgmt/Saas.yaml +++ b/mmv1/products/saasservicemgmt/Saas.yaml @@ -27,13 +27,24 @@ examples: - name: saas_runtime_saas_basic primary_resource_id: "example" min_version: 'beta' + ignore_read_extra: + - blueprint_repo + - conditions + - state + - update_time + - etag + - application_template.0.sync_operation vars: saas_name: test-saas + test_env_vars: + project: 'PROJECT_NAME' bootstrap_iam: - member: "serviceAccount:service-{project_number}@gcp-sa-saasservicemgmt.iam.gserviceaccount.com" role: "roles/saasservicemgmt.serviceAgent" autogen_async: false autogen_status: U2Fhcw== +custom_code: + custom_delete: 'templates/terraform/custom_delete/saas_runtime_saas_delete.go.tmpl' parameters: - name: location type: String @@ -56,10 +67,108 @@ properties: They are not queryable and should be preserved when modifying objects. More info: https://kubernetes.io/docs/user-guide/annotations + - name: applicationTemplate + type: NestedObject + description: |- + CompositeRef represents a reference to a composite resource. + properties: + - name: applicationTemplate + type: String + required: true + description: Reference to the ApplicationTemplate resource. + - name: revision + type: String + description: |- + Revision of the ApplicationTemplate to use. + Changes to revision will trigger manual resynchronization. + If empty, ApplicationTemplate will be ignored. + - name: syncOperation + type: String + description: |- + Reference to on-going AppTemplate import and replication operation (i.e. + the operation_id for the long-running operation). + This field is opaque for external usage. + output: true + - name: blueprintRepo + type: String + description: |- + Name of repository in Artifact Registry for system-generated Blueprints, + eg. Blueprints of imported ApplicationTemplates. + output: true + - name: conditions + type: Array + description: |- + A set of conditions which indicate the various conditions this resource can + have. + output: true + item_type: + type: NestedObject + properties: + - name: lastTransitionTime + type: String + description: Last time the condition transited from one status to another. + output: true + - name: message + type: String + description: Human readable message indicating details about the last transition. + output: true + - name: reason + type: String + description: Brief reason for the condition's last transition. + output: true + - name: status + type: String + description: |- + Status of the condition. + Possible values: + STATUS_UNKNOWN + STATUS_TRUE + STATUS_FALSE + output: true + - name: type + type: String + description: |- + Type of the condition. + Possible values: + TYPE_READY + TYPE_SYNCHRONIZED + output: true - name: createTime type: String description: The timestamp when the resource was created. output: true + - name: error + type: NestedObject + output: true + description: |- + The `Status` type defines a logical error model that is suitable for + different programming environments, including REST APIs and RPC APIs. It is + used by [gRPC](https://github.com/grpc). Each `Status` message contains + three pieces of data: error code, error message, and error details. + + You can find out more about this error model and how to work with it in the + [API Design Guide](https://cloud.google.com/apis/design/errors). + properties: + - name: code + type: Integer + description: The status code, which should be an enum value of google.rpc.Code. + output: true + - name: details + type: Array + description: |- + A list of messages that carry the error details. There is a common set of + message types for APIs to use. + item_type: + type: NestedObject + properties: [] + output: true + - name: message + type: String + description: |- + A developer-facing error message, which should be in English. Any + user-facing error message should be localized and sent in the + google.rpc.Status.details field, or localized by the client. + output: true - name: etag type: String description: |- @@ -91,6 +200,16 @@ properties: "projects/{project}/locations/{location}/saas/{saas}" output: true + - name: state + type: String + description: |- + State of the Saas. + It is always in STATE_ACTIVE state if the application_template is empty. + Possible values: + STATE_ACTIVE + STATE_RUNNING + STATE_FAILED + output: true - name: uid type: String description: |- diff --git a/mmv1/templates/terraform/custom_delete/saas_runtime_saas_delete.go.tmpl b/mmv1/templates/terraform/custom_delete/saas_runtime_saas_delete.go.tmpl new file mode 100644 index 000000000000..31989481b331 --- /dev/null +++ b/mmv1/templates/terraform/custom_delete/saas_runtime_saas_delete.go.tmpl @@ -0,0 +1,91 @@ + billingProject := "" + + project, err := tpgresource.GetProject(d, config) + if err != nil { + return fmt.Errorf("Error fetching project for Saas: %s", err) + } + billingProject = project + url, err := tpgresource.ReplaceVars(d, config, fmt.Sprintf("%s%s", transport_tpg.BaseUrl(Product, config), "projects/{{"{{"}}project{{"}}"}}/locations/{{"{{"}}location{{"}}"}}/saas/{{"{{"}}saas_id{{"}}"}}")) + if err != nil { + return err + } + + // err == nil indicates that the billing_project value was found + if bp, err := tpgresource.GetBillingProject(d, config); err == nil { + billingProject = bp + } + + headers := make(http.Header) + + // 1. Wait for the resource to transition out of STATE_RUNNING before deleting + log.Printf("[DEBUG] Waiting for Saas %q to transition out of STATE_RUNNING", d.Id()) + err = retry.RetryContext(context.Background(), d.Timeout(schema.TimeoutDelete), func() *retry.RetryError { + res, readErr := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "GET", + Project: billingProject, + RawURL: url, + UserAgent: userAgent, + Headers: headers, + }) + if readErr != nil { + if transport_tpg.IsGoogleApiErrorWithCode(readErr, 404) { + return nil // already gone + } + return retry.NonRetryableError(readErr) + } + + state, ok := res["state"].(string) + if !ok { + return retry.RetryableError(fmt.Errorf("Saas %q state is not yet populated", d.Id())) + } + if state == "STATE_RUNNING" { + return retry.RetryableError(fmt.Errorf("Saas %q is still in STATE_RUNNING state", d.Id())) + } + + return nil + }) + if err != nil { + return fmt.Errorf("Error waiting for Saas %q to be ready for deletion: %s", d.Id(), err) + } + + // 2. Trigger the DELETE request + log.Printf("[DEBUG] Deleting Saas %q", d.Id()) + res, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "DELETE", + Project: billingProject, + RawURL: url, + UserAgent: userAgent, + Timeout: d.Timeout(schema.TimeoutDelete), + Headers: headers, + }) + if err != nil { + return transport_tpg.HandleNotFoundError(err, d, "Saas") + } + + log.Printf("[DEBUG] Finished trigger for deleting Saas %q: %#v", d.Id(), res) + + // 3. Wait for the Saas resource to be fully deleted from GCP (eventual consistency) + err = retry.RetryContext(context.Background(), d.Timeout(schema.TimeoutDelete), func() *retry.RetryError { + _, readErr := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "GET", + Project: billingProject, + RawURL: url, + UserAgent: userAgent, + Headers: headers, + }) + if readErr != nil { + if transport_tpg.IsGoogleApiErrorWithCode(readErr, 404) { + return nil + } + return retry.NonRetryableError(readErr) + } + return retry.RetryableError(fmt.Errorf("Saas %q still exists", d.Id())) + }) + if err != nil { + return fmt.Errorf("Error waiting for Saas %q to be fully deleted: %s", d.Id(), err) + } + + return nil diff --git a/mmv1/templates/terraform/examples/saas_runtime_saas_basic.tf.tmpl b/mmv1/templates/terraform/examples/saas_runtime_saas_basic.tf.tmpl index 8993684b993e..b12186c63a79 100644 --- a/mmv1/templates/terraform/examples/saas_runtime_saas_basic.tf.tmpl +++ b/mmv1/templates/terraform/examples/saas_runtime_saas_basic.tf.tmpl @@ -9,4 +9,9 @@ resource "google_saas_runtime_saas" "{{$.PrimaryResourceId}}" { locations { name = "europe-west1" } + + application_template { + application_template = "projects/%{project}/locations/global/applicationTemplates/my-template" + revision = "r1" + } } diff --git a/mmv1/third_party/terraform/services/saasruntime/resource_saas_runtime_saas_test.go.tmpl b/mmv1/third_party/terraform/services/saasruntime/resource_saas_runtime_saas_test.go.tmpl index d6736e9e3eec..a55473703b71 100644 --- a/mmv1/third_party/terraform/services/saasruntime/resource_saas_runtime_saas_test.go.tmpl +++ b/mmv1/third_party/terraform/services/saasruntime/resource_saas_runtime_saas_test.go.tmpl @@ -9,6 +9,7 @@ import ( "github.com/hashicorp/terraform-plugin-testing/plancheck" "github.com/hashicorp/terraform-provider-google/google/acctest" + "github.com/hashicorp/terraform-provider-google/google/envvar" _ "github.com/hashicorp/terraform-provider-google/google/services/saasruntime" "github.com/hashicorp/terraform-provider-google/google/services/resourcemanager" ) @@ -24,6 +25,7 @@ func TestAccSaasRuntimeSaas_update(t *testing.T) { context := map[string]interface{}{ "random_suffix": acctest.RandString(t, 10), + "project": envvar.GetTestProjectFromEnv(), } acctest.VcrTest(t, resource.TestCase{ @@ -37,7 +39,7 @@ func TestAccSaasRuntimeSaas_update(t *testing.T) { ResourceName: "google_saas_runtime_saas.example", ImportState: true, ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"annotations", "labels", "location", "saas_id", "terraform_labels"}, + ImportStateVerifyIgnore: []string{"annotations", "labels", "location", "saas_id", "terraform_labels", "blueprint_repo", "conditions", "state", "update_time", "etag"}, }, { Config: testAccSaasRuntimeSaas_update(context), @@ -51,7 +53,7 @@ func TestAccSaasRuntimeSaas_update(t *testing.T) { ResourceName: "google_saas_runtime_saas.example", ImportState: true, ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"annotations", "labels", "location", "saas_id", "terraform_labels"}, + ImportStateVerifyIgnore: []string{"annotations", "labels", "location", "saas_id", "terraform_labels", "blueprint_repo", "conditions", "state", "update_time", "etag"}, }, }, }) @@ -70,6 +72,10 @@ resource "google_saas_runtime_saas" "example" { locations { name = "europe-west1" } + + application_template { + application_template = "projects/%{project}/locations/global/applicationTemplates/my-template" + } } `, context) } @@ -95,6 +101,11 @@ resource "google_saas_runtime_saas" "example" { annotations = { "annotation-one": "bar" } + + application_template { + application_template = "projects/%{project}/locations/global/applicationTemplates/my-template" + revision = "r2" + } } `, context) }