From bb777877f206fed2413847df866c4a04018298da Mon Sep 17 00:00:00 2001 From: Katie McLaughlin Date: Thu, 27 Apr 2023 14:07:01 +1000 Subject: [PATCH 01/32] feat: support multiple firebase sites --- infra/apis.tf | 2 +- infra/firebase.tf | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/infra/apis.tf b/infra/apis.tf index 997ed12e..347cf8d0 100644 --- a/infra/apis.tf +++ b/infra/apis.tf @@ -27,7 +27,7 @@ locals { "cloudbuild.googleapis.com", "secretmanager.googleapis.com", "cloudresourcemanager.googleapis.com", - "firebase.googleapis.com", + "firebasehosting.googleapis.com", ] } diff --git a/infra/firebase.tf b/infra/firebase.tf index 38f7ae6b..6414af98 100644 --- a/infra/firebase.tf +++ b/infra/firebase.tf @@ -20,3 +20,9 @@ resource "google_firebase_project" "default" { depends_on = [google_project_service.enabled] } + +resource "google_firebase_hosting_site" "client" { + provider = google-beta + project = var.project_id + site_id = var.random_suffix ? "${var.project_id}-${random_id.suffix.hex}" : var.project_id +} From 8ccc7a2234df8dcf16963c6dc6cfce16d8668ffe Mon Sep 17 00:00:00 2001 From: Katie McLaughlin Date: Thu, 27 Apr 2023 14:41:15 +1000 Subject: [PATCH 02/32] fix: add suffix directly on the job, rather than parsing later --- infra/jobs.tf | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/infra/jobs.tf b/infra/jobs.tf index ae72982e..ad6d70d1 100644 --- a/infra/jobs.tf +++ b/infra/jobs.tf @@ -115,6 +115,10 @@ resource "google_cloud_run_v2_job" "client" { service_account = google_service_account.client.email containers { image = local.client_image + env { + name = "SUFFIX" + value = var.random_suffix ? random_id.suffix.hex : "" + } env { name = "SERVICE_NAME" value = google_cloud_run_v2_service.server.name From b260df6321e9e2f5e1ac32fdeef00abb6f2ce1e2 Mon Sep 17 00:00:00 2001 From: Katie McLaughlin Date: Thu, 27 Apr 2023 15:53:07 +1000 Subject: [PATCH 03/32] fix: ensure api dependence --- infra/firebase.tf | 2 ++ 1 file changed, 2 insertions(+) diff --git a/infra/firebase.tf b/infra/firebase.tf index 6414af98..886f3ab5 100644 --- a/infra/firebase.tf +++ b/infra/firebase.tf @@ -25,4 +25,6 @@ resource "google_firebase_hosting_site" "client" { provider = google-beta project = var.project_id site_id = var.random_suffix ? "${var.project_id}-${random_id.suffix.hex}" : var.project_id + + depends_on = [google_project_service.enabled] } From 20d9ad98764b636eb7cef306e8f738dd9e3abd62 Mon Sep 17 00:00:00 2001 From: Katie McLaughlin Date: Fri, 28 Apr 2023 13:25:38 +1000 Subject: [PATCH 04/32] bump avocano --- infra/variables.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infra/variables.tf b/infra/variables.tf index 10a83a3b..3a334424 100644 --- a/infra/variables.tf +++ b/infra/variables.tf @@ -55,7 +55,7 @@ variable "init" { variable "image_version" { type = string - default = "v1.6.0" + default = "v1.7.0" description = "Version of the Container Registry image to use" } From c9cacd6986d858526cb8a29db42cf5c15c2aa4e6 Mon Sep 17 00:00:00 2001 From: Katie McLaughlin Date: Fri, 28 Apr 2023 13:25:47 +1000 Subject: [PATCH 05/32] fix: update firebase_url based on resource attribute --- infra/outputs.tf | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/infra/outputs.tf b/infra/outputs.tf index 3788a69a..0cbebf80 100644 --- a/infra/outputs.tf +++ b/infra/outputs.tf @@ -16,11 +16,12 @@ locals { server_url = google_cloud_run_v2_service.server.uri + firebase_url = google_firebase_hosting_site.client.default_url } output "firebase_url" { description = "Firebase URL" - value = "https://${var.project_id}.web.app" + value = firebase_url } locals { @@ -55,7 +56,7 @@ output "usage" { sensitive = true value = <<-EOF This deployment is now ready for use! - https://${var.project_id}.web.app + ${firebase_url} API Login: ${google_cloud_run_v2_service.server.uri}/admin Username: admin From 53e17718506ba35c765e603bcd898345a2ed7ad2 Mon Sep 17 00:00:00 2001 From: Katie McLaughlin Date: Fri, 28 Apr 2023 13:49:07 +1000 Subject: [PATCH 06/32] fix: syntax --- infra/outputs.tf | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/infra/outputs.tf b/infra/outputs.tf index 0cbebf80..8fb60d8e 100644 --- a/infra/outputs.tf +++ b/infra/outputs.tf @@ -15,13 +15,13 @@ */ locals { - server_url = google_cloud_run_v2_service.server.uri + server_url = google_cloud_run_v2_service.server.uri firebase_url = google_firebase_hosting_site.client.default_url } output "firebase_url" { description = "Firebase URL" - value = firebase_url + value = local.firebase_url } locals { @@ -56,7 +56,7 @@ output "usage" { sensitive = true value = <<-EOF This deployment is now ready for use! - ${firebase_url} + ${local.firebase_url} API Login: ${google_cloud_run_v2_service.server.uri}/admin Username: admin From a85b68594a32d44df14b417637ffcf877e57fa33 Mon Sep 17 00:00:00 2001 From: Katie McLaughlin Date: Fri, 28 Apr 2023 15:28:41 +1000 Subject: [PATCH 07/32] docs: update --- infra/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infra/README.md b/infra/README.md index 9251eae2..d90293a7 100644 --- a/infra/README.md +++ b/infra/README.md @@ -47,7 +47,7 @@ Functional examples are included in the | client\_image\_host | Container Registry that hosts the client image (PROJECT\_ID[/folder]) | `string` | `"hsa-public/terraform-python-dynamic-webapp"` | no | | database\_name | Cloud SQL database name | `string` | `"django"` | no | | database\_username | Cloud SQL database name | `string` | `"server"` | no | -| image\_version | Version of the Container Registry image to use | `string` | `"v1.6.0"` | no | +| image\_version | Version of the Container Registry image to use | `string` | `"v1.7.0"` | no | | init | Initialize database? | `bool` | `true` | no | | instance\_name | Cloud SQL Instance name | `string` | `"psql"` | no | | labels | A set of key/value label pairs to assign to the resources deployed by this blueprint. | `map(string)` | `{}` | no | From cd5807754ce8a8d74e845d5356eca797e2d29f8d Mon Sep 17 00:00:00 2001 From: Katie McLaughlin Date: Mon, 1 May 2023 10:50:31 +1000 Subject: [PATCH 08/32] (noop for re-run tests) --- infra/postdeployment.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infra/postdeployment.tf b/infra/postdeployment.tf index c81f2675..75b0450d 100644 --- a/infra/postdeployment.tf +++ b/infra/postdeployment.tf @@ -66,7 +66,7 @@ resource "google_compute_instance" "initialize" { service_account { email = google_service_account.compute[0].email - scopes = ["cloud-platform"] # TODO: Restrict? + scopes = ["cloud-platform"] # TODO: Restrict?? } metadata_startup_script = < Date: Tue, 2 May 2023 16:00:31 +1000 Subject: [PATCH 09/32] reintro firebasehosting api --- infra/apis.tf | 1 + 1 file changed, 1 insertion(+) diff --git a/infra/apis.tf b/infra/apis.tf index 347cf8d0..a2acf6ad 100644 --- a/infra/apis.tf +++ b/infra/apis.tf @@ -27,6 +27,7 @@ locals { "cloudbuild.googleapis.com", "secretmanager.googleapis.com", "cloudresourcemanager.googleapis.com", + "firebase.googleapis.com", "firebasehosting.googleapis.com", ] } From bfc59415a6aa86146788d1d48e63494031bf0c96 Mon Sep 17 00:00:00 2001 From: Katie McLaughlin Date: Thu, 11 May 2023 12:51:37 +1000 Subject: [PATCH 10/32] fix: explicitly encode project/region for google-beta provider --- infra/main.tf | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/infra/main.tf b/infra/main.tf index c51d7ad6..df067440 100644 --- a/infra/main.tf +++ b/infra/main.tf @@ -20,3 +20,8 @@ provider "google" { project = var.project_id region = var.region } + +provider "google-beta" { + project = var.project_id + region = var.region +} From 0c5fc0b6f02a60eb04c2e3761e66a7adf00dacb2 Mon Sep 17 00:00:00 2001 From: Katie McLaughlin Date: Tue, 16 May 2023 07:34:34 +1000 Subject: [PATCH 11/32] make firebase site depend on project as suggested in https://github.com/hashicorp/terraform-provider-google/issues/14585#issuecomment-1547966035 --- infra/firebase.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infra/firebase.tf b/infra/firebase.tf index 886f3ab5..d46d0492 100644 --- a/infra/firebase.tf +++ b/infra/firebase.tf @@ -23,7 +23,7 @@ resource "google_firebase_project" "default" { resource "google_firebase_hosting_site" "client" { provider = google-beta - project = var.project_id + project = google_firebase_project.default.project site_id = var.random_suffix ? "${var.project_id}-${random_id.suffix.hex}" : var.project_id depends_on = [google_project_service.enabled] From 6885933abe9450cbded72588a716476421ba1bf2 Mon Sep 17 00:00:00 2001 From: Katie McLaughlin Date: Tue, 16 May 2023 17:19:51 +1000 Subject: [PATCH 12/32] add suffix logic to placeholder --- app/placeholder/placeholder-deploy.sh | 18 ++++++++++++++++++ infra/jobs.tf | 4 ++++ 2 files changed, 22 insertions(+) diff --git a/app/placeholder/placeholder-deploy.sh b/app/placeholder/placeholder-deploy.sh index 81fc7172..0b4cd0a0 100755 --- a/app/placeholder/placeholder-deploy.sh +++ b/app/placeholder/placeholder-deploy.sh @@ -17,6 +17,24 @@ # any errors? exit immediately. set -e +# if deploying with a suffix, adjust the config to suit the custom site +# https://firebase.google.com/docs/hosting/multisites#set_up_deploy_targets +if [[ -n $SUFFIX ]]; then + json -I -f firebase.json -e "this.hosting.target='$SUFFIX'" + UPDATED=true + + echo "{\"projects\": {}, \"targets\": {\"${PROJECT_ID}\": {\"hosting\": {\"${SUFFIX}\": [\"${PROJECT_ID}-${SUFFIX}\"]}}},\"etags\": {}}" | json > .firebaserc + echo "Customised .firebaserc created to support site." + cat .firebaserc + fi +fi + +# If anything was updated, then export the output. +if [[ -n $UPDATED ]]; then + echo "Deploying with the following updated config: " + cat firebase.json +fi + echo "Deploying placeholder to Firebase..." firebase deploy --project "$PROJECT_ID" --only hosting diff --git a/infra/jobs.tf b/infra/jobs.tf index 9576c36b..ccc66586 100644 --- a/infra/jobs.tf +++ b/infra/jobs.tf @@ -158,6 +158,10 @@ resource "google_cloud_run_v2_job" "placeholder" { value = var.project_id } + env { + name = "SUFFIX" + value = var.random_suffix ? random_id.suffix.hex : "" + } } } } From 9bcf68e8c563ead6b92dc298496a27382e8fe5aa Mon Sep 17 00:00:00 2001 From: Katie McLaughlin Date: Tue, 16 May 2023 17:36:08 +1000 Subject: [PATCH 13/32] debugging --- infra/test/integration/simple_example/simple_example_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/infra/test/integration/simple_example/simple_example_test.go b/infra/test/integration/simple_example/simple_example_test.go index 5f3b6f8b..2f5717aa 100644 --- a/infra/test/integration/simple_example/simple_example_test.go +++ b/infra/test/integration/simple_example/simple_example_test.go @@ -47,6 +47,7 @@ func TestSimpleExample(t *testing.T) { // // https://github.com/GoogleCloudPlatform/terraform-dynamic-python-webapp/issues/64 firebase_url := terraform.OutputRequired(t, example.GetTFOptions(), "firebase_url") + t.Log("Firebase Hosting should be running at ", firebase_url) assertResponseContains(assert, firebase_url, "Your application is still deploying") }) From 0430c1f14358519a55093fc4b87bdfd770bdcbf1 Mon Sep 17 00:00:00 2001 From: Katie McLaughlin Date: Tue, 16 May 2023 17:40:46 +1000 Subject: [PATCH 14/32] fix trailing fi --- app/placeholder/placeholder-deploy.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/app/placeholder/placeholder-deploy.sh b/app/placeholder/placeholder-deploy.sh index 0b4cd0a0..3fe801af 100755 --- a/app/placeholder/placeholder-deploy.sh +++ b/app/placeholder/placeholder-deploy.sh @@ -26,7 +26,6 @@ if [[ -n $SUFFIX ]]; then echo "{\"projects\": {}, \"targets\": {\"${PROJECT_ID}\": {\"hosting\": {\"${SUFFIX}\": [\"${PROJECT_ID}-${SUFFIX}\"]}}},\"etags\": {}}" | json > .firebaserc echo "Customised .firebaserc created to support site." cat .firebaserc - fi fi # If anything was updated, then export the output. From 870797cf0e21c59d1438328e4c88ce32e11436ca Mon Sep 17 00:00:00 2001 From: Katie McLaughlin Date: Wed, 17 May 2023 08:10:37 +1000 Subject: [PATCH 15/32] Include json package for config alteration --- app/placeholder/Dockerfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/placeholder/Dockerfile b/app/placeholder/Dockerfile index cfce24d4..e35a2656 100644 --- a/app/placeholder/Dockerfile +++ b/app/placeholder/Dockerfile @@ -18,4 +18,6 @@ ARG PROJECT_ID=YOURPROJECTID FROM gcr.io/$PROJECT_ID/firebase COPY . ./ + +RUN npm install -g json ENTRYPOINT ./placeholder-deploy.sh From ae80693b04b03197200cf32e6ba47e1ddd9d0c25 Mon Sep 17 00:00:00 2001 From: Katie McLaughlin Date: Wed, 17 May 2023 09:19:17 +1000 Subject: [PATCH 16/32] Add new test: suffix (merge #59) --- build/int.cloudbuild.yaml | 24 +++ infra/examples/simple_example/main.tf | 2 +- infra/examples/suffix_example/README.md | 27 ++++ infra/examples/suffix_example/main.tf | 21 +++ infra/examples/suffix_example/outputs.tf | 26 +++ infra/examples/suffix_example/variables.tf | 20 +++ infra/examples/suffix_example/versions.tf | 25 +++ .../suffix_example/simple_example_test.go | 151 ++++++++++++++++++ 8 files changed, 295 insertions(+), 1 deletion(-) create mode 100644 infra/examples/suffix_example/README.md create mode 100644 infra/examples/suffix_example/main.tf create mode 100644 infra/examples/suffix_example/outputs.tf create mode 100644 infra/examples/suffix_example/variables.tf create mode 100644 infra/examples/suffix_example/versions.tf create mode 100644 infra/test/integration/suffix_example/simple_example_test.go diff --git a/build/int.cloudbuild.yaml b/build/int.cloudbuild.yaml index 1c61fd84..16eaddf5 100644 --- a/build/int.cloudbuild.yaml +++ b/build/int.cloudbuild.yaml @@ -22,6 +22,12 @@ steps: - 'TF_VAR_org_id=$_ORG_ID' - 'TF_VAR_folder_id=$_FOLDER_ID' - 'TF_VAR_billing_account=$_BILLING_ACCOUNT' + +- id: init-all + name: 'gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS' + args: ['/bin/bash', '-c', 'cft test run all --stage init --verbose'] + +# Simple example - one deployment per project - id: simple-example-init dir: infra name: 'gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS' @@ -38,6 +44,24 @@ steps: dir: infra name: 'gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS' args: ['/bin/bash', '-c', 'cft test run TestSimpleExample --stage teardown --verbose'] + +# Suffix example - supports multiple deployments per project +- id: suffix-example-init + dir: infra + name: 'gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS' + args: ['/bin/bash', '-c', 'cft test run TestSuffixExample --stage init --verbose'] +- id: suffix-example-apply + dir: infra + name: 'gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS' + args: ['/bin/bash', '-c', 'cft test run TestSuffixExample --stage apply --verbose'] +- id: suffix-example-verify + dir: infra + name: 'gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS' + args: ['/bin/bash', '-c', 'cft test run TestSuffixExample --stage verify --verbose'] +- id: suffix-example-teardown + dir: infra + name: 'gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS' + args: ['/bin/bash', '-c', 'cft test run TestSuffixExample --stage teardown --verbose'] tags: - 'ci' - 'integration' diff --git a/infra/examples/simple_example/main.tf b/infra/examples/simple_example/main.tf index f4d2207a..ce29d854 100644 --- a/infra/examples/simple_example/main.tf +++ b/infra/examples/simple_example/main.tf @@ -16,6 +16,6 @@ module "dynamic-python-webapp" { source = "../.." - project_id = var.project_id + random_suffix = false } diff --git a/infra/examples/suffix_example/README.md b/infra/examples/suffix_example/README.md new file mode 100644 index 00000000..bbb60726 --- /dev/null +++ b/infra/examples/suffix_example/README.md @@ -0,0 +1,27 @@ +# Simple Example + +This example illustrates how to use the `dynamic-python-webapp` module. + +This example uses a randomly generated suffix when naming resources to support multiple deployments per project. + + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| project\_id | The ID of the project in which to provision resources. | `string` | n/a | yes | + +## Outputs + +| Name | Description | +|------|-------------| +| firebase\_url | Firebase URL | +| usage | Connection details for the project | + + + +To provision this example, run the following from within this directory: +- `terraform init` to get the plugins +- `terraform plan` to see the infrastructure plan +- `terraform apply` to apply the infrastructure build +- `terraform destroy` to destroy the built infrastructure diff --git a/infra/examples/suffix_example/main.tf b/infra/examples/suffix_example/main.tf new file mode 100644 index 00000000..f4d2207a --- /dev/null +++ b/infra/examples/suffix_example/main.tf @@ -0,0 +1,21 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +module "dynamic-python-webapp" { + source = "../.." + + project_id = var.project_id +} diff --git a/infra/examples/suffix_example/outputs.tf b/infra/examples/suffix_example/outputs.tf new file mode 100644 index 00000000..6f6f3abe --- /dev/null +++ b/infra/examples/suffix_example/outputs.tf @@ -0,0 +1,26 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +output "usage" { + sensitive = true + description = "Connection details for the project" + value = module.dynamic-python-webapp.usage +} + +output "firebase_url" { + description = "Firebase URL" + value = module.dynamic-python-webapp.firebase_url +} diff --git a/infra/examples/suffix_example/variables.tf b/infra/examples/suffix_example/variables.tf new file mode 100644 index 00000000..10a4e2da --- /dev/null +++ b/infra/examples/suffix_example/variables.tf @@ -0,0 +1,20 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +variable "project_id" { + description = "The ID of the project in which to provision resources." + type = string +} diff --git a/infra/examples/suffix_example/versions.tf b/infra/examples/suffix_example/versions.tf new file mode 100644 index 00000000..96fc6080 --- /dev/null +++ b/infra/examples/suffix_example/versions.tf @@ -0,0 +1,25 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +terraform { + required_providers { + google = { + source = "hashicorp/google" + version = "~> 4.0" + } + } + required_version = ">= 0.13" +} diff --git a/infra/test/integration/suffix_example/simple_example_test.go b/infra/test/integration/suffix_example/simple_example_test.go new file mode 100644 index 00000000..2f5717aa --- /dev/null +++ b/infra/test/integration/suffix_example/simple_example_test.go @@ -0,0 +1,151 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package multiple_buckets + +import ( + "io" + "net/http" + "strings" + "testing" + "time" + + "github.com/GoogleCloudPlatform/cloud-foundation-toolkit/infra/blueprint-test/pkg/gcloud" + "github.com/GoogleCloudPlatform/cloud-foundation-toolkit/infra/blueprint-test/pkg/tft" + "github.com/GoogleCloudPlatform/cloud-foundation-toolkit/infra/blueprint-test/pkg/utils" + "github.com/gruntwork-io/terratest/modules/terraform" + "github.com/stretchr/testify/assert" +) + +func TestSimpleExample(t *testing.T) { + example := tft.NewTFBlueprintTest(t) + + example.DefineApply(func(assert *assert.Assertions) { + example.DefaultApply(assert) + + // This module deploys a 'placeholder' Firebase Hosting release early + // in the process, to prevent a "Site Not Found" displaying when Terraform + // has finished applying, but the deployment is not yet complete. + // + // This extension of apply is meant to emulate that behavior. We confirm + // the placeholder behavior here to boost confidence that the frontend test in + // example.DefineVerify proves the placeholder page is replaced. + // + // If the check is flaky, remove it in favor of + // a simpler HTTP request. + // + // https://github.com/GoogleCloudPlatform/terraform-dynamic-python-webapp/issues/64 + firebase_url := terraform.OutputRequired(t, example.GetTFOptions(), "firebase_url") + t.Log("Firebase Hosting should be running at ", firebase_url) + assertResponseContains(assert, firebase_url, "Your application is still deploying") + }) + + example.DefineVerify(func(assert *assert.Assertions) { + example.DefaultVerify(assert) + + projectID := example.GetTFSetupStringOutput("project_id") + firebase_url := terraform.OutputRequired(t, example.GetTFOptions(), "firebase_url") + + flagshipProduct := "Sparkly Avocado" + region := "us-central1" + t.Logf("Using Project ID %q", projectID) + + { + // Check that the Cloud Storage API is enabled + services := gcloud.Run(t, "services list", gcloud.WithCommonArgs([]string{"--project", projectID, "--format", "json"})).Array() + match := utils.GetFirstMatchResult(t, services, "config.name", "storage.googleapis.com") + assert.Equal("ENABLED", match.Get("state").String(), "storage service should be enabled") + } + + { + // Check that the Cloud Run service is deployed, is serving, and accepts unauthenticated requests + cloudRunServices := gcloud.Run(t, "run services list", gcloud.WithCommonArgs([]string{"--project", projectID, "--format", "json"})).Array() + nbServices := len(cloudRunServices) + assert.Equal(1, nbServices, "we expected a single Cloud Run service to be deployed, found %d services", nbServices) + match := utils.GetFirstMatchResult(t, cloudRunServices, "kind", "Service") + serviceURL := match.Get("status.url").String() + assert.Truef(strings.HasSuffix(serviceURL, ".run.app"), "unexpected service URL %q", serviceURL) + t.Log("Cloud Run service is running at", serviceURL) + + // The Cloud Run service is the app's API backend (it does not serve the Avocano homepage) + assertResponseContains(assert, serviceURL, "/api", "/admin") + + // The data is populated by two Cloud Run jobs, so wait for the final job to finish before continuing. + // A job execution is completed if it has a completed time. + isJobFinished := func() (bool, error) { + clientJobExecs := gcloud.Run(t, "run jobs executions list ", gcloud.WithCommonArgs([]string{"--filter", "metadata.name~client", "--project", projectID, "--region", region, "--format", "json"})).Array() + + if len(clientJobExecs) == 0 { + t.Log("Cloud Run job been executed. Retrying...") + return true, nil + } + + match := utils.GetFirstMatchResult(t, clientJobExecs, "kind", "Execution") + completionTime := match.Get("status.completionTime").String() + + if completionTime == "" { + // retry + t.Log("Cloud Run job execution hasn't completed. Retrying...") + return true, nil + } + + t.Log("Cloud Run job completed", completionTime) + assert.NotEqual(completionTime, "", "completedTime must have a value") + + succeededCount := match.Get("status.succeededCount").Int() + assert.Equal(succeededCount, int64(1), "succeededCount must not be 0") + + return false, nil + } + utils.Poll(t, isJobFinished, 10, time.Second*10) + + // The API must return a list that includes our flagship product + assertResponseContains(assert, serviceURL+"/api/products/", flagshipProduct) + } + { + // Check that the Avocano front page is deployed to Firebase Hosting, and serving + t.Log("Firebase Hosting should be running at ", firebase_url) + assertResponseContains(assert, firebase_url, "Avocano") + } + }) + example.Test() +} + +func assertResponseContains(assert *assert.Assertions, url string, text ...string) { + code, responseBody, err := httpGetRequest(url) + assert.Nil(err) + assert.GreaterOrEqual(code, 200) + assert.LessOrEqual(code, 299) + for _, fragment := range text { + assert.Containsf(responseBody, fragment, "couldn't find %q in response body", fragment) + } +} + +func assertErrorResponseContains(assert *assert.Assertions, url string, wantCode int, text string) { + code, responseBody, err := httpGetRequest(url) + assert.Nil(err) + assert.Equal(code, wantCode) + assert.Containsf(responseBody, text, "couldn't find %q in response body", text) +} + +func httpGetRequest(url string) (statusCode int, body string, err error) { + res, err := http.Get(url) + if err != nil { + return 0, "", err + } + defer res.Body.Close() + + buffer, err := io.ReadAll(res.Body) + return res.StatusCode, string(buffer), err +} From 285ec74a8659c596f76c2a122d6d51b35d5556a7 Mon Sep 17 00:00:00 2001 From: Katie McLaughlin Date: Wed, 17 May 2023 09:28:14 +1000 Subject: [PATCH 17/32] tf fmt --- infra/examples/simple_example/main.tf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/infra/examples/simple_example/main.tf b/infra/examples/simple_example/main.tf index ce29d854..a7c11857 100644 --- a/infra/examples/simple_example/main.tf +++ b/infra/examples/simple_example/main.tf @@ -15,7 +15,7 @@ */ module "dynamic-python-webapp" { - source = "../.." - project_id = var.project_id + source = "../.." + project_id = var.project_id random_suffix = false } From 16576f144c80ee7c937fd64592d37dc8e2abf4a4 Mon Sep 17 00:00:00 2001 From: Katie McLaughlin Date: Wed, 17 May 2023 09:28:38 +1000 Subject: [PATCH 18/32] whitespace --- infra/examples/suffix_example/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infra/examples/suffix_example/README.md b/infra/examples/suffix_example/README.md index bbb60726..321e2ea2 100644 --- a/infra/examples/suffix_example/README.md +++ b/infra/examples/suffix_example/README.md @@ -2,7 +2,7 @@ This example illustrates how to use the `dynamic-python-webapp` module. -This example uses a randomly generated suffix when naming resources to support multiple deployments per project. +This example uses a randomly generated suffix when naming resources to support multiple deployments per project. ## Inputs From b9074a6ffa4801560bf1649b1cb2a9025d2ab583 Mon Sep 17 00:00:00 2001 From: Katie McLaughlin Date: Wed, 17 May 2023 11:12:38 +1000 Subject: [PATCH 19/32] DEBUG: try optional sites to mitigate Error 409 --- infra/firebase.tf | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/infra/firebase.tf b/infra/firebase.tf index d46d0492..c31261eb 100644 --- a/infra/firebase.tf +++ b/infra/firebase.tf @@ -22,9 +22,13 @@ resource "google_firebase_project" "default" { } resource "google_firebase_hosting_site" "client" { + + # By default, a firebase site will be named "project_id". Only create a custom site if using suffixes + count = var.random_suffix ? 1 : 0 + provider = google-beta project = google_firebase_project.default.project - site_id = var.random_suffix ? "${var.project_id}-${random_id.suffix.hex}" : var.project_id + site_id = "${var.project_id}-${random_id.suffix.hex}" depends_on = [google_project_service.enabled] } From 2ab0855c6a15f448848014ea31ee2eb078f846b2 Mon Sep 17 00:00:00 2001 From: Katie McLaughlin Date: Wed, 17 May 2023 12:17:33 +1000 Subject: [PATCH 20/32] fix: conditional site calls for conditional outputs --- infra/outputs.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infra/outputs.tf b/infra/outputs.tf index 8fb60d8e..e769a3bf 100644 --- a/infra/outputs.tf +++ b/infra/outputs.tf @@ -16,7 +16,7 @@ locals { server_url = google_cloud_run_v2_service.server.uri - firebase_url = google_firebase_hosting_site.client.default_url + firebase_url = var.random_suffix ? google_firebase_hosting_site.client.default_url : "https://${var.project_id}.web.app" } output "firebase_url" { From 74481edf6d2ec58a70e4b41a1fb0f53b8938c182 Mon Sep 17 00:00:00 2001 From: Katie McLaughlin Date: Wed, 17 May 2023 12:37:03 +1000 Subject: [PATCH 21/32] count index for firebase url --- infra/outputs.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infra/outputs.tf b/infra/outputs.tf index e769a3bf..61173633 100644 --- a/infra/outputs.tf +++ b/infra/outputs.tf @@ -16,7 +16,7 @@ locals { server_url = google_cloud_run_v2_service.server.uri - firebase_url = var.random_suffix ? google_firebase_hosting_site.client.default_url : "https://${var.project_id}.web.app" + firebase_url = var.random_suffix ? google_firebase_hosting_site.client[0].default_url : "https://${var.project_id}.web.app" } output "firebase_url" { From b7a857ea63f2b9551b4cf8836424e083830636b7 Mon Sep 17 00:00:00 2001 From: Katie McLaughlin Date: Wed, 17 May 2023 13:06:43 +1000 Subject: [PATCH 22/32] rename packages, name files correctly --- .../test/integration/simple_example/simple_example_test.go | 4 ++-- .../{simple_example_test.go => suffix_example_test.go} | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) rename infra/test/integration/suffix_example/{simple_example_test.go => suffix_example_test.go} (97%) diff --git a/infra/test/integration/simple_example/simple_example_test.go b/infra/test/integration/simple_example/simple_example_test.go index 2f5717aa..43d48ab2 100644 --- a/infra/test/integration/simple_example/simple_example_test.go +++ b/infra/test/integration/simple_example/simple_example_test.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package multiple_buckets +package simple_example import ( "io" @@ -48,7 +48,7 @@ func TestSimpleExample(t *testing.T) { // https://github.com/GoogleCloudPlatform/terraform-dynamic-python-webapp/issues/64 firebase_url := terraform.OutputRequired(t, example.GetTFOptions(), "firebase_url") t.Log("Firebase Hosting should be running at ", firebase_url) - assertResponseContains(assert, firebase_url, "Your application is still deploying") + assertResponseContains(assert, firebase_url, "Your application is still deploying") }) example.DefineVerify(func(assert *assert.Assertions) { diff --git a/infra/test/integration/suffix_example/simple_example_test.go b/infra/test/integration/suffix_example/suffix_example_test.go similarity index 97% rename from infra/test/integration/suffix_example/simple_example_test.go rename to infra/test/integration/suffix_example/suffix_example_test.go index 2f5717aa..05672b6f 100644 --- a/infra/test/integration/suffix_example/simple_example_test.go +++ b/infra/test/integration/suffix_example/suffix_example_test.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package multiple_buckets +package suffix_buckets import ( "io" @@ -28,7 +28,7 @@ import ( "github.com/stretchr/testify/assert" ) -func TestSimpleExample(t *testing.T) { +func TestSuffixExample(t *testing.T) { example := tft.NewTFBlueprintTest(t) example.DefineApply(func(assert *assert.Assertions) { @@ -48,7 +48,7 @@ func TestSimpleExample(t *testing.T) { // https://github.com/GoogleCloudPlatform/terraform-dynamic-python-webapp/issues/64 firebase_url := terraform.OutputRequired(t, example.GetTFOptions(), "firebase_url") t.Log("Firebase Hosting should be running at ", firebase_url) - assertResponseContains(assert, firebase_url, "Your application is still deploying") + assertResponseContains(assert, firebase_url, "Your application is still deploying") }) example.DefineVerify(func(assert *assert.Assertions) { From 0ab98cf04dfcd0aba8af102cff57f377c2aff8b2 Mon Sep 17 00:00:00 2001 From: Katie McLaughlin Date: Wed, 31 May 2023 12:53:28 +1000 Subject: [PATCH 23/32] re-order js deps --- app/placeholder/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/placeholder/Dockerfile b/app/placeholder/Dockerfile index e35a2656..db3c07a2 100644 --- a/app/placeholder/Dockerfile +++ b/app/placeholder/Dockerfile @@ -17,7 +17,7 @@ ARG PROJECT_ID=YOURPROJECTID FROM gcr.io/$PROJECT_ID/firebase +RUN npm install -g json COPY . ./ -RUN npm install -g json ENTRYPOINT ./placeholder-deploy.sh From 9ec351bd49871258f98063488d70fa0e4ef248ff Mon Sep 17 00:00:00 2001 From: Katie McLaughlin Date: Wed, 31 May 2023 13:01:04 +1000 Subject: [PATCH 24/32] Use envsubst and template file over raw json --- app/placeholder/Dockerfile | 1 - app/placeholder/firebaserc.tmpl | 13 +++++++++++++ app/placeholder/placeholder-deploy.sh | 5 +++-- 3 files changed, 16 insertions(+), 3 deletions(-) create mode 100644 app/placeholder/firebaserc.tmpl diff --git a/app/placeholder/Dockerfile b/app/placeholder/Dockerfile index db3c07a2..5b0a5972 100644 --- a/app/placeholder/Dockerfile +++ b/app/placeholder/Dockerfile @@ -17,7 +17,6 @@ ARG PROJECT_ID=YOURPROJECTID FROM gcr.io/$PROJECT_ID/firebase -RUN npm install -g json COPY . ./ ENTRYPOINT ./placeholder-deploy.sh diff --git a/app/placeholder/firebaserc.tmpl b/app/placeholder/firebaserc.tmpl new file mode 100644 index 00000000..b5160cff --- /dev/null +++ b/app/placeholder/firebaserc.tmpl @@ -0,0 +1,13 @@ +{ + "projects": {}, + "targets": { + "$PROJECT_ID": { + "hosting": { + "$SUFFIX": [ + "${PROJECT_ID}-${SUFFIX}" + ] + } + } + }, + "etags": {} +} \ No newline at end of file diff --git a/app/placeholder/placeholder-deploy.sh b/app/placeholder/placeholder-deploy.sh index 3fe801af..c3ce9941 100755 --- a/app/placeholder/placeholder-deploy.sh +++ b/app/placeholder/placeholder-deploy.sh @@ -17,13 +17,14 @@ # any errors? exit immediately. set -e -# if deploying with a suffix, adjust the config to suit the custom site +# if deploying with a suffix (from infra/jobs.tf), adjust the config to suit the custom site # https://firebase.google.com/docs/hosting/multisites#set_up_deploy_targets if [[ -n $SUFFIX ]]; then json -I -f firebase.json -e "this.hosting.target='$SUFFIX'" UPDATED=true - echo "{\"projects\": {}, \"targets\": {\"${PROJECT_ID}\": {\"hosting\": {\"${SUFFIX}\": [\"${PROJECT_ID}-${SUFFIX}\"]}}},\"etags\": {}}" | json > .firebaserc + # Use template file to generate configuration + envsubst < firebaserc.tmpl > .firebaserc echo "Customised .firebaserc created to support site." cat .firebaserc fi From 168394f405052fdf0f99c2f3cead6c7a026b9880 Mon Sep 17 00:00:00 2001 From: Katie McLaughlin Date: Wed, 31 May 2023 13:01:32 +1000 Subject: [PATCH 25/32] Add additional inline documenation on envvar usage --- infra/jobs.tf | 3 +++ 1 file changed, 3 insertions(+) diff --git a/infra/jobs.tf b/infra/jobs.tf index ccc66586..9c97c3f9 100644 --- a/infra/jobs.tf +++ b/infra/jobs.tf @@ -115,6 +115,9 @@ resource "google_cloud_run_v2_job" "client" { service_account = google_service_account.client.email containers { image = local.client_image + + # Variables used to customise Firebase configuration on deployment + # https://github.com/GoogleCloudPlatform/avocano/blob/main/client/docker-deploy.sh env { name = "SUFFIX" value = var.random_suffix ? random_id.suffix.hex : "" From ae2350f0700a7cad4bd018eb701f410f0f38fba1 Mon Sep 17 00:00:00 2001 From: Katie McLaughlin Date: Wed, 31 May 2023 13:05:45 +1000 Subject: [PATCH 26/32] attempt: parallel builds --- build/int.cloudbuild.yaml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/build/int.cloudbuild.yaml b/build/int.cloudbuild.yaml index 16eaddf5..c7f2ee3b 100644 --- a/build/int.cloudbuild.yaml +++ b/build/int.cloudbuild.yaml @@ -23,42 +23,51 @@ steps: - 'TF_VAR_folder_id=$_FOLDER_ID' - 'TF_VAR_billing_account=$_BILLING_ACCOUNT' +# Initialize all tests, then run two parallel sets of tests. - id: init-all name: 'gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS' args: ['/bin/bash', '-c', 'cft test run all --stage init --verbose'] # Simple example - one deployment per project - id: simple-example-init + waitFor: ['init-all'] dir: infra name: 'gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS' args: ['/bin/bash', '-c', 'cft test run TestSimpleExample --stage init --verbose'] - id: simple-example-apply + waitFor: ['simple-example-init'] dir: infra name: 'gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS' args: ['/bin/bash', '-c', 'cft test run TestSimpleExample --stage apply --verbose'] - id: simple-example-verify + waitFor: ['simple-example-apply'] dir: infra name: 'gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS' args: ['/bin/bash', '-c', 'cft test run TestSimpleExample --stage verify --verbose'] - id: simple-example-teardown + waitFor: ['simple-example-verify'] dir: infra name: 'gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS' args: ['/bin/bash', '-c', 'cft test run TestSimpleExample --stage teardown --verbose'] # Suffix example - supports multiple deployments per project - id: suffix-example-init + waitFor: ['init-all'] dir: infra name: 'gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS' args: ['/bin/bash', '-c', 'cft test run TestSuffixExample --stage init --verbose'] - id: suffix-example-apply + waitFor: ['suffix-example-init'] dir: infra name: 'gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS' args: ['/bin/bash', '-c', 'cft test run TestSuffixExample --stage apply --verbose'] - id: suffix-example-verify + waitFor: ['suffix-example-apply'] dir: infra name: 'gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS' args: ['/bin/bash', '-c', 'cft test run TestSuffixExample --stage verify --verbose'] - id: suffix-example-teardown + waitFor: ['suffix-example-verify'] dir: infra name: 'gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS' args: ['/bin/bash', '-c', 'cft test run TestSuffixExample --stage teardown --verbose'] From 64e275b4599630764ae1d07a1112dbe8f3064a5f Mon Sep 17 00:00:00 2001 From: Katie McLaughlin Date: Wed, 31 May 2023 13:15:59 +1000 Subject: [PATCH 27/32] fix: lint --- app/placeholder/firebaserc.tmpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/placeholder/firebaserc.tmpl b/app/placeholder/firebaserc.tmpl index b5160cff..c7491bcc 100644 --- a/app/placeholder/firebaserc.tmpl +++ b/app/placeholder/firebaserc.tmpl @@ -10,4 +10,4 @@ } }, "etags": {} -} \ No newline at end of file +} From 85c019b29cbcf607752477c2bce42a5680d03d38 Mon Sep 17 00:00:00 2001 From: Katie McLaughlin Date: Wed, 31 May 2023 15:34:05 +1000 Subject: [PATCH 28/32] update tests to not expect to be run in isolation (code duplication fix pending) --- infra/examples/suffix_example/outputs.tf | 11 +++++++++++ infra/outputs.tf | 10 ++++++++++ .../integration/simple_example/simple_example_test.go | 10 ++++++---- .../integration/suffix_example/suffix_example_test.go | 10 ++++++---- 4 files changed, 33 insertions(+), 8 deletions(-) diff --git a/infra/examples/suffix_example/outputs.tf b/infra/examples/suffix_example/outputs.tf index 6f6f3abe..d9b7bd2f 100644 --- a/infra/examples/suffix_example/outputs.tf +++ b/infra/examples/suffix_example/outputs.tf @@ -24,3 +24,14 @@ output "firebase_url" { description = "Firebase URL" value = module.dynamic-python-webapp.firebase_url } + +output "server_service_name" { + description = "Server Cloud Run service name" + value = module.dynamic-python-webapp.server_service_name +} + +output "client_job_name" { + description = "Client Cloud Run job name" + value = module.dynamic-python-webapp.client_job_name +} + diff --git a/infra/outputs.tf b/infra/outputs.tf index 61173633..a273850b 100644 --- a/infra/outputs.tf +++ b/infra/outputs.tf @@ -63,3 +63,13 @@ output "usage" { Password: ${google_secret_manager_secret_version.django_admin_password.secret_data} EOF } + +output "server_service_name" { + description = "Name of the Cloud Run service, hosting the server API" + value = google_cloud_run_v2_service.server.name +} + +output "client_job_name" { + description = "Name of the Cloud Run Job, deploying the front end" + value = google_cloud_run_v2_job.client.name +} \ No newline at end of file diff --git a/infra/test/integration/simple_example/simple_example_test.go b/infra/test/integration/simple_example/simple_example_test.go index 43d48ab2..82ed17bd 100644 --- a/infra/test/integration/simple_example/simple_example_test.go +++ b/infra/test/integration/simple_example/simple_example_test.go @@ -56,6 +56,8 @@ func TestSimpleExample(t *testing.T) { projectID := example.GetTFSetupStringOutput("project_id") firebase_url := terraform.OutputRequired(t, example.GetTFOptions(), "firebase_url") + server_service_name := terraform.OutputRequired(t, example.GetTFOptions(), "server_service_name") + client_job_name := terraform.OutputRequired(t, example.GetTFOptions(), "client_job_name") flagshipProduct := "Sparkly Avocado" region := "us-central1" @@ -69,10 +71,10 @@ func TestSimpleExample(t *testing.T) { } { - // Check that the Cloud Run service is deployed, is serving, and accepts unauthenticated requests - cloudRunServices := gcloud.Run(t, "run services list", gcloud.WithCommonArgs([]string{"--project", projectID, "--format", "json"})).Array() + // Check that the expected Cloud Run service is deployed, is serving, and accepts unauthenticated requests + cloudRunServices := gcloud.Run(t, "run services list", gcloud.WithCommonArgs([]string{"--filter", "metadata.name~" + server_service_name, "--project", projectID, "--format", "json"})).Array() nbServices := len(cloudRunServices) - assert.Equal(1, nbServices, "we expected a single Cloud Run service to be deployed, found %d services", nbServices) + assert.Equal(1, nbServices, "we expected a single Cloud Run service called %s to be deployed, found %d services", server_service_name, nbServices) match := utils.GetFirstMatchResult(t, cloudRunServices, "kind", "Service") serviceURL := match.Get("status.url").String() assert.Truef(strings.HasSuffix(serviceURL, ".run.app"), "unexpected service URL %q", serviceURL) @@ -84,7 +86,7 @@ func TestSimpleExample(t *testing.T) { // The data is populated by two Cloud Run jobs, so wait for the final job to finish before continuing. // A job execution is completed if it has a completed time. isJobFinished := func() (bool, error) { - clientJobExecs := gcloud.Run(t, "run jobs executions list ", gcloud.WithCommonArgs([]string{"--filter", "metadata.name~client", "--project", projectID, "--region", region, "--format", "json"})).Array() + clientJobExecs := gcloud.Run(t, "run jobs executions list ", gcloud.WithCommonArgs([]string{"--filter", "metadata.name~" + client_job_name, "--project", projectID, "--region", region, "--format", "json"})).Array() if len(clientJobExecs) == 0 { t.Log("Cloud Run job been executed. Retrying...") diff --git a/infra/test/integration/suffix_example/suffix_example_test.go b/infra/test/integration/suffix_example/suffix_example_test.go index 05672b6f..733f1a5d 100644 --- a/infra/test/integration/suffix_example/suffix_example_test.go +++ b/infra/test/integration/suffix_example/suffix_example_test.go @@ -56,6 +56,8 @@ func TestSuffixExample(t *testing.T) { projectID := example.GetTFSetupStringOutput("project_id") firebase_url := terraform.OutputRequired(t, example.GetTFOptions(), "firebase_url") + server_service_name := terraform.OutputRequired(t, example.GetTFOptions(), "server_service_name") + client_job_name := terraform.OutputRequired(t, example.GetTFOptions(), "client_job_name") flagshipProduct := "Sparkly Avocado" region := "us-central1" @@ -69,10 +71,10 @@ func TestSuffixExample(t *testing.T) { } { - // Check that the Cloud Run service is deployed, is serving, and accepts unauthenticated requests - cloudRunServices := gcloud.Run(t, "run services list", gcloud.WithCommonArgs([]string{"--project", projectID, "--format", "json"})).Array() + // Check that the expected Cloud Run service is deployed, is serving, and accepts unauthenticated requests + cloudRunServices := gcloud.Run(t, "run services list", gcloud.WithCommonArgs([]string{"--filter", "metadata.name~" + server_service_name, "--project", projectID, "--format", "json"})).Array() nbServices := len(cloudRunServices) - assert.Equal(1, nbServices, "we expected a single Cloud Run service to be deployed, found %d services", nbServices) + assert.Equal(1, nbServices, "we expected a single Cloud Run service called %s to be deployed, found %d services", server_service_name, nbServices) match := utils.GetFirstMatchResult(t, cloudRunServices, "kind", "Service") serviceURL := match.Get("status.url").String() assert.Truef(strings.HasSuffix(serviceURL, ".run.app"), "unexpected service URL %q", serviceURL) @@ -84,7 +86,7 @@ func TestSuffixExample(t *testing.T) { // The data is populated by two Cloud Run jobs, so wait for the final job to finish before continuing. // A job execution is completed if it has a completed time. isJobFinished := func() (bool, error) { - clientJobExecs := gcloud.Run(t, "run jobs executions list ", gcloud.WithCommonArgs([]string{"--filter", "metadata.name~client", "--project", projectID, "--region", region, "--format", "json"})).Array() + clientJobExecs := gcloud.Run(t, "run jobs executions list ", gcloud.WithCommonArgs([]string{"--filter", "metadata.name~" + client_job_name, "--project", projectID, "--region", region, "--format", "json"})).Array() if len(clientJobExecs) == 0 { t.Log("Cloud Run job been executed. Retrying...") From 49536e25483570056673719845c0e24d8f03f6b9 Mon Sep 17 00:00:00 2001 From: Katie McLaughlin Date: Wed, 31 May 2023 15:39:34 +1000 Subject: [PATCH 29/32] lint, missing outputs --- infra/examples/simple_example/outputs.tf | 10 ++++++++++ infra/outputs.tf | 10 +++++----- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/infra/examples/simple_example/outputs.tf b/infra/examples/simple_example/outputs.tf index 6f6f3abe..1fe64cdf 100644 --- a/infra/examples/simple_example/outputs.tf +++ b/infra/examples/simple_example/outputs.tf @@ -24,3 +24,13 @@ output "firebase_url" { description = "Firebase URL" value = module.dynamic-python-webapp.firebase_url } + +output "server_service_name" { + description = "Server Cloud Run service name" + value = module.dynamic-python-webapp.server_service_name +} + +output "client_job_name" { + description = "Client Cloud Run job name" + value = module.dynamic-python-webapp.client_job_name +} diff --git a/infra/outputs.tf b/infra/outputs.tf index a273850b..563c5f3b 100644 --- a/infra/outputs.tf +++ b/infra/outputs.tf @@ -64,12 +64,12 @@ output "usage" { EOF } -output "server_service_name" { +output "server_service_name" { description = "Name of the Cloud Run service, hosting the server API" - value = google_cloud_run_v2_service.server.name + value = google_cloud_run_v2_service.server.name } -output "client_job_name" { +output "client_job_name" { description = "Name of the Cloud Run Job, deploying the front end" - value = google_cloud_run_v2_job.client.name -} \ No newline at end of file + value = google_cloud_run_v2_job.client.name +} From 111ab4b46546a77a5f20b1fb014de3b0782a6ebd Mon Sep 17 00:00:00 2001 From: Katie McLaughlin Date: Wed, 31 May 2023 05:50:52 +0000 Subject: [PATCH 30/32] make generate_docs --- infra/README.md | 2 ++ infra/examples/simple_example/README.md | 2 ++ infra/examples/suffix_example/README.md | 2 ++ infra/metadata.yaml | 6 ++++++ 4 files changed, 12 insertions(+) diff --git a/infra/README.md b/infra/README.md index 8169a346..d602c48b 100644 --- a/infra/README.md +++ b/infra/README.md @@ -62,10 +62,12 @@ Functional examples are included in the | Name | Description | |------|-------------| +| client\_job\_name | Name of the Cloud Run Job, deploying the front end | | django\_admin\_password | Djando Admin password | | django\_admin\_url | Djando Admin URL | | firebase\_url | Firebase URL | | neos\_toc\_url | Neos Tutorial URL | +| server\_service\_name | Name of the Cloud Run service, hosting the server API | | usage | Next steps for usage | diff --git a/infra/examples/simple_example/README.md b/infra/examples/simple_example/README.md index ef26cb31..c2a6140c 100644 --- a/infra/examples/simple_example/README.md +++ b/infra/examples/simple_example/README.md @@ -13,7 +13,9 @@ This example illustrates how to use the `dynamic-python-webapp` module. | Name | Description | |------|-------------| +| client\_job\_name | Client Cloud Run job name | | firebase\_url | Firebase URL | +| server\_service\_name | Server Cloud Run service name | | usage | Connection details for the project | diff --git a/infra/examples/suffix_example/README.md b/infra/examples/suffix_example/README.md index 321e2ea2..52395783 100644 --- a/infra/examples/suffix_example/README.md +++ b/infra/examples/suffix_example/README.md @@ -15,7 +15,9 @@ This example uses a randomly generated suffix when naming resources to support m | Name | Description | |------|-------------| +| client\_job\_name | Client Cloud Run job name | | firebase\_url | Firebase URL | +| server\_service\_name | Server Cloud Run service name | | usage | Connection details for the project | diff --git a/infra/metadata.yaml b/infra/metadata.yaml index 4b79b18b..85aa3300 100644 --- a/infra/metadata.yaml +++ b/infra/metadata.yaml @@ -45,6 +45,8 @@ spec: examples: - name: simple_example location: examples/simple_example + - name: suffix_example + location: examples/suffix_example interfaces: variables: - name: client_image_host @@ -100,6 +102,8 @@ spec: varType: string defaultValue: us-central1-c outputs: + - name: client_job_name + description: Name of the Cloud Run Job, deploying the front end - name: django_admin_password description: Djando Admin password - name: django_admin_url @@ -108,6 +112,8 @@ spec: description: Firebase URL - name: neos_toc_url description: Neos Tutorial URL + - name: server_service_name + description: Name of the Cloud Run service, hosting the server API - name: usage description: Next steps for usage requirements: From 05dddb00f32d12e9c8c6cf88a40bb44cea965352 Mon Sep 17 00:00:00 2001 From: Katie McLaughlin Date: Wed, 31 May 2023 16:18:38 +1000 Subject: [PATCH 31/32] fix: use exact matching on service listing --- infra/test/integration/simple_example/simple_example_test.go | 2 +- infra/test/integration/suffix_example/suffix_example_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/infra/test/integration/simple_example/simple_example_test.go b/infra/test/integration/simple_example/simple_example_test.go index 82ed17bd..d401ac40 100644 --- a/infra/test/integration/simple_example/simple_example_test.go +++ b/infra/test/integration/simple_example/simple_example_test.go @@ -72,7 +72,7 @@ func TestSimpleExample(t *testing.T) { { // Check that the expected Cloud Run service is deployed, is serving, and accepts unauthenticated requests - cloudRunServices := gcloud.Run(t, "run services list", gcloud.WithCommonArgs([]string{"--filter", "metadata.name~" + server_service_name, "--project", projectID, "--format", "json"})).Array() + cloudRunServices := gcloud.Run(t, "run services list", gcloud.WithCommonArgs([]string{"--filter", "metadata.name=" + server_service_name, "--project", projectID, "--format", "json"})).Array() nbServices := len(cloudRunServices) assert.Equal(1, nbServices, "we expected a single Cloud Run service called %s to be deployed, found %d services", server_service_name, nbServices) match := utils.GetFirstMatchResult(t, cloudRunServices, "kind", "Service") diff --git a/infra/test/integration/suffix_example/suffix_example_test.go b/infra/test/integration/suffix_example/suffix_example_test.go index 733f1a5d..abf8ee19 100644 --- a/infra/test/integration/suffix_example/suffix_example_test.go +++ b/infra/test/integration/suffix_example/suffix_example_test.go @@ -72,7 +72,7 @@ func TestSuffixExample(t *testing.T) { { // Check that the expected Cloud Run service is deployed, is serving, and accepts unauthenticated requests - cloudRunServices := gcloud.Run(t, "run services list", gcloud.WithCommonArgs([]string{"--filter", "metadata.name~" + server_service_name, "--project", projectID, "--format", "json"})).Array() + cloudRunServices := gcloud.Run(t, "run services list", gcloud.WithCommonArgs([]string{"--filter", "metadata.name=" + server_service_name, "--project", projectID, "--format", "json"})).Array() nbServices := len(cloudRunServices) assert.Equal(1, nbServices, "we expected a single Cloud Run service called %s to be deployed, found %d services", server_service_name, nbServices) match := utils.GetFirstMatchResult(t, cloudRunServices, "kind", "Service") From 7ba0c295c1dd95616458bc783d6fe608dd99aea9 Mon Sep 17 00:00:00 2001 From: Adam Ross Date: Thu, 1 Jun 2023 15:31:05 -0700 Subject: [PATCH 32/32] testing: de-duplicate code --- .../simple_example/simple_example_test.go | 132 +-------------- .../suffix_example/suffix_example_test.go | 134 +-------------- infra/test/integration/testhelper.go | 153 ++++++++++++++++++ 3 files changed, 158 insertions(+), 261 deletions(-) create mode 100644 infra/test/integration/testhelper.go diff --git a/infra/test/integration/simple_example/simple_example_test.go b/infra/test/integration/simple_example/simple_example_test.go index d401ac40..cc6c3013 100644 --- a/infra/test/integration/simple_example/simple_example_test.go +++ b/infra/test/integration/simple_example/simple_example_test.go @@ -15,139 +15,11 @@ package simple_example import ( - "io" - "net/http" - "strings" "testing" - "time" - "github.com/GoogleCloudPlatform/cloud-foundation-toolkit/infra/blueprint-test/pkg/gcloud" - "github.com/GoogleCloudPlatform/cloud-foundation-toolkit/infra/blueprint-test/pkg/tft" - "github.com/GoogleCloudPlatform/cloud-foundation-toolkit/infra/blueprint-test/pkg/utils" - "github.com/gruntwork-io/terratest/modules/terraform" - "github.com/stretchr/testify/assert" + test "github.com/terraform-google-modules/dynamic-python-webapp/test/integration" ) func TestSimpleExample(t *testing.T) { - example := tft.NewTFBlueprintTest(t) - - example.DefineApply(func(assert *assert.Assertions) { - example.DefaultApply(assert) - - // This module deploys a 'placeholder' Firebase Hosting release early - // in the process, to prevent a "Site Not Found" displaying when Terraform - // has finished applying, but the deployment is not yet complete. - // - // This extension of apply is meant to emulate that behavior. We confirm - // the placeholder behavior here to boost confidence that the frontend test in - // example.DefineVerify proves the placeholder page is replaced. - // - // If the check is flaky, remove it in favor of - // a simpler HTTP request. - // - // https://github.com/GoogleCloudPlatform/terraform-dynamic-python-webapp/issues/64 - firebase_url := terraform.OutputRequired(t, example.GetTFOptions(), "firebase_url") - t.Log("Firebase Hosting should be running at ", firebase_url) - assertResponseContains(assert, firebase_url, "Your application is still deploying") - }) - - example.DefineVerify(func(assert *assert.Assertions) { - example.DefaultVerify(assert) - - projectID := example.GetTFSetupStringOutput("project_id") - firebase_url := terraform.OutputRequired(t, example.GetTFOptions(), "firebase_url") - server_service_name := terraform.OutputRequired(t, example.GetTFOptions(), "server_service_name") - client_job_name := terraform.OutputRequired(t, example.GetTFOptions(), "client_job_name") - - flagshipProduct := "Sparkly Avocado" - region := "us-central1" - t.Logf("Using Project ID %q", projectID) - - { - // Check that the Cloud Storage API is enabled - services := gcloud.Run(t, "services list", gcloud.WithCommonArgs([]string{"--project", projectID, "--format", "json"})).Array() - match := utils.GetFirstMatchResult(t, services, "config.name", "storage.googleapis.com") - assert.Equal("ENABLED", match.Get("state").String(), "storage service should be enabled") - } - - { - // Check that the expected Cloud Run service is deployed, is serving, and accepts unauthenticated requests - cloudRunServices := gcloud.Run(t, "run services list", gcloud.WithCommonArgs([]string{"--filter", "metadata.name=" + server_service_name, "--project", projectID, "--format", "json"})).Array() - nbServices := len(cloudRunServices) - assert.Equal(1, nbServices, "we expected a single Cloud Run service called %s to be deployed, found %d services", server_service_name, nbServices) - match := utils.GetFirstMatchResult(t, cloudRunServices, "kind", "Service") - serviceURL := match.Get("status.url").String() - assert.Truef(strings.HasSuffix(serviceURL, ".run.app"), "unexpected service URL %q", serviceURL) - t.Log("Cloud Run service is running at", serviceURL) - - // The Cloud Run service is the app's API backend (it does not serve the Avocano homepage) - assertResponseContains(assert, serviceURL, "/api", "/admin") - - // The data is populated by two Cloud Run jobs, so wait for the final job to finish before continuing. - // A job execution is completed if it has a completed time. - isJobFinished := func() (bool, error) { - clientJobExecs := gcloud.Run(t, "run jobs executions list ", gcloud.WithCommonArgs([]string{"--filter", "metadata.name~" + client_job_name, "--project", projectID, "--region", region, "--format", "json"})).Array() - - if len(clientJobExecs) == 0 { - t.Log("Cloud Run job been executed. Retrying...") - return true, nil - } - - match := utils.GetFirstMatchResult(t, clientJobExecs, "kind", "Execution") - completionTime := match.Get("status.completionTime").String() - - if completionTime == "" { - // retry - t.Log("Cloud Run job execution hasn't completed. Retrying...") - return true, nil - } - - t.Log("Cloud Run job completed", completionTime) - assert.NotEqual(completionTime, "", "completedTime must have a value") - - succeededCount := match.Get("status.succeededCount").Int() - assert.Equal(succeededCount, int64(1), "succeededCount must not be 0") - - return false, nil - } - utils.Poll(t, isJobFinished, 10, time.Second*10) - - // The API must return a list that includes our flagship product - assertResponseContains(assert, serviceURL+"/api/products/", flagshipProduct) - } - { - // Check that the Avocano front page is deployed to Firebase Hosting, and serving - t.Log("Firebase Hosting should be running at ", firebase_url) - assertResponseContains(assert, firebase_url, "Avocano") - } - }) - example.Test() -} - -func assertResponseContains(assert *assert.Assertions, url string, text ...string) { - code, responseBody, err := httpGetRequest(url) - assert.Nil(err) - assert.GreaterOrEqual(code, 200) - assert.LessOrEqual(code, 299) - for _, fragment := range text { - assert.Containsf(responseBody, fragment, "couldn't find %q in response body", fragment) - } -} - -func assertErrorResponseContains(assert *assert.Assertions, url string, wantCode int, text string) { - code, responseBody, err := httpGetRequest(url) - assert.Nil(err) - assert.Equal(code, wantCode) - assert.Containsf(responseBody, text, "couldn't find %q in response body", text) -} - -func httpGetRequest(url string) (statusCode int, body string, err error) { - res, err := http.Get(url) - if err != nil { - return 0, "", err - } - defer res.Body.Close() - - buffer, err := io.ReadAll(res.Body) - return res.StatusCode, string(buffer), err + test.AssertExample(t) } diff --git a/infra/test/integration/suffix_example/suffix_example_test.go b/infra/test/integration/suffix_example/suffix_example_test.go index abf8ee19..0cb22e43 100644 --- a/infra/test/integration/suffix_example/suffix_example_test.go +++ b/infra/test/integration/suffix_example/suffix_example_test.go @@ -12,142 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -package suffix_buckets +package simple_example import ( - "io" - "net/http" - "strings" "testing" - "time" - "github.com/GoogleCloudPlatform/cloud-foundation-toolkit/infra/blueprint-test/pkg/gcloud" - "github.com/GoogleCloudPlatform/cloud-foundation-toolkit/infra/blueprint-test/pkg/tft" - "github.com/GoogleCloudPlatform/cloud-foundation-toolkit/infra/blueprint-test/pkg/utils" - "github.com/gruntwork-io/terratest/modules/terraform" - "github.com/stretchr/testify/assert" + test "github.com/terraform-google-modules/dynamic-python-webapp/test/integration" ) func TestSuffixExample(t *testing.T) { - example := tft.NewTFBlueprintTest(t) - - example.DefineApply(func(assert *assert.Assertions) { - example.DefaultApply(assert) - - // This module deploys a 'placeholder' Firebase Hosting release early - // in the process, to prevent a "Site Not Found" displaying when Terraform - // has finished applying, but the deployment is not yet complete. - // - // This extension of apply is meant to emulate that behavior. We confirm - // the placeholder behavior here to boost confidence that the frontend test in - // example.DefineVerify proves the placeholder page is replaced. - // - // If the check is flaky, remove it in favor of - // a simpler HTTP request. - // - // https://github.com/GoogleCloudPlatform/terraform-dynamic-python-webapp/issues/64 - firebase_url := terraform.OutputRequired(t, example.GetTFOptions(), "firebase_url") - t.Log("Firebase Hosting should be running at ", firebase_url) - assertResponseContains(assert, firebase_url, "Your application is still deploying") - }) - - example.DefineVerify(func(assert *assert.Assertions) { - example.DefaultVerify(assert) - - projectID := example.GetTFSetupStringOutput("project_id") - firebase_url := terraform.OutputRequired(t, example.GetTFOptions(), "firebase_url") - server_service_name := terraform.OutputRequired(t, example.GetTFOptions(), "server_service_name") - client_job_name := terraform.OutputRequired(t, example.GetTFOptions(), "client_job_name") - - flagshipProduct := "Sparkly Avocado" - region := "us-central1" - t.Logf("Using Project ID %q", projectID) - - { - // Check that the Cloud Storage API is enabled - services := gcloud.Run(t, "services list", gcloud.WithCommonArgs([]string{"--project", projectID, "--format", "json"})).Array() - match := utils.GetFirstMatchResult(t, services, "config.name", "storage.googleapis.com") - assert.Equal("ENABLED", match.Get("state").String(), "storage service should be enabled") - } - - { - // Check that the expected Cloud Run service is deployed, is serving, and accepts unauthenticated requests - cloudRunServices := gcloud.Run(t, "run services list", gcloud.WithCommonArgs([]string{"--filter", "metadata.name=" + server_service_name, "--project", projectID, "--format", "json"})).Array() - nbServices := len(cloudRunServices) - assert.Equal(1, nbServices, "we expected a single Cloud Run service called %s to be deployed, found %d services", server_service_name, nbServices) - match := utils.GetFirstMatchResult(t, cloudRunServices, "kind", "Service") - serviceURL := match.Get("status.url").String() - assert.Truef(strings.HasSuffix(serviceURL, ".run.app"), "unexpected service URL %q", serviceURL) - t.Log("Cloud Run service is running at", serviceURL) - - // The Cloud Run service is the app's API backend (it does not serve the Avocano homepage) - assertResponseContains(assert, serviceURL, "/api", "/admin") - - // The data is populated by two Cloud Run jobs, so wait for the final job to finish before continuing. - // A job execution is completed if it has a completed time. - isJobFinished := func() (bool, error) { - clientJobExecs := gcloud.Run(t, "run jobs executions list ", gcloud.WithCommonArgs([]string{"--filter", "metadata.name~" + client_job_name, "--project", projectID, "--region", region, "--format", "json"})).Array() - - if len(clientJobExecs) == 0 { - t.Log("Cloud Run job been executed. Retrying...") - return true, nil - } - - match := utils.GetFirstMatchResult(t, clientJobExecs, "kind", "Execution") - completionTime := match.Get("status.completionTime").String() - - if completionTime == "" { - // retry - t.Log("Cloud Run job execution hasn't completed. Retrying...") - return true, nil - } - - t.Log("Cloud Run job completed", completionTime) - assert.NotEqual(completionTime, "", "completedTime must have a value") - - succeededCount := match.Get("status.succeededCount").Int() - assert.Equal(succeededCount, int64(1), "succeededCount must not be 0") - - return false, nil - } - utils.Poll(t, isJobFinished, 10, time.Second*10) - - // The API must return a list that includes our flagship product - assertResponseContains(assert, serviceURL+"/api/products/", flagshipProduct) - } - { - // Check that the Avocano front page is deployed to Firebase Hosting, and serving - t.Log("Firebase Hosting should be running at ", firebase_url) - assertResponseContains(assert, firebase_url, "Avocano") - } - }) - example.Test() -} - -func assertResponseContains(assert *assert.Assertions, url string, text ...string) { - code, responseBody, err := httpGetRequest(url) - assert.Nil(err) - assert.GreaterOrEqual(code, 200) - assert.LessOrEqual(code, 299) - for _, fragment := range text { - assert.Containsf(responseBody, fragment, "couldn't find %q in response body", fragment) - } -} - -func assertErrorResponseContains(assert *assert.Assertions, url string, wantCode int, text string) { - code, responseBody, err := httpGetRequest(url) - assert.Nil(err) - assert.Equal(code, wantCode) - assert.Containsf(responseBody, text, "couldn't find %q in response body", text) -} - -func httpGetRequest(url string) (statusCode int, body string, err error) { - res, err := http.Get(url) - if err != nil { - return 0, "", err - } - defer res.Body.Close() - - buffer, err := io.ReadAll(res.Body) - return res.StatusCode, string(buffer), err + test.AssertExample(t) } diff --git a/infra/test/integration/testhelper.go b/infra/test/integration/testhelper.go new file mode 100644 index 00000000..10180b56 --- /dev/null +++ b/infra/test/integration/testhelper.go @@ -0,0 +1,153 @@ +package test + +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import ( + "io" + "net/http" + "strings" + "testing" + "time" + + "github.com/GoogleCloudPlatform/cloud-foundation-toolkit/infra/blueprint-test/pkg/gcloud" + "github.com/GoogleCloudPlatform/cloud-foundation-toolkit/infra/blueprint-test/pkg/tft" + "github.com/GoogleCloudPlatform/cloud-foundation-toolkit/infra/blueprint-test/pkg/utils" + "github.com/gruntwork-io/terratest/modules/terraform" + "github.com/stretchr/testify/assert" +) + +func AssertExample(t *testing.T) { + example := tft.NewTFBlueprintTest(t) + + example.DefineApply(func(assert *assert.Assertions) { + example.DefaultApply(assert) + + // This module deploys a 'placeholder' Firebase Hosting release early + // in the process, to prevent a "Site Not Found" displaying when Terraform + // has finished applying, but the deployment is not yet complete. + // + // This extension of apply is meant to emulate that behavior. We confirm + // the placeholder behavior here to boost confidence that the frontend test in + // example.DefineVerify proves the placeholder page is replaced. + // + // If the check is flaky, remove it in favor of + // a simpler HTTP request. + // + // https://github.com/GoogleCloudPlatform/terraform-dynamic-python-webapp/issues/64 + firebase_url := terraform.OutputRequired(t, example.GetTFOptions(), "firebase_url") + t.Log("Firebase Hosting should be running at ", firebase_url) + assertResponseContains(assert, firebase_url, "Your application is still deploying") + }) + + example.DefineVerify(func(assert *assert.Assertions) { + example.DefaultVerify(assert) + + projectID := example.GetTFSetupStringOutput("project_id") + firebase_url := terraform.OutputRequired(t, example.GetTFOptions(), "firebase_url") + server_service_name := terraform.OutputRequired(t, example.GetTFOptions(), "server_service_name") + client_job_name := terraform.OutputRequired(t, example.GetTFOptions(), "client_job_name") + + flagshipProduct := "Sparkly Avocado" + region := "us-central1" + t.Logf("Using Project ID %q", projectID) + + { + // Check that the Cloud Storage API is enabled + services := gcloud.Run(t, "services list", gcloud.WithCommonArgs([]string{"--project", projectID, "--format", "json"})).Array() + match := utils.GetFirstMatchResult(t, services, "config.name", "storage.googleapis.com") + assert.Equal("ENABLED", match.Get("state").String(), "storage service should be enabled") + } + + { + // Check that the expected Cloud Run service is deployed, is serving, and accepts unauthenticated requests + cloudRunServices := gcloud.Run(t, "run services list", gcloud.WithCommonArgs([]string{"--filter", "metadata.name=" + server_service_name, "--project", projectID, "--format", "json"})).Array() + nbServices := len(cloudRunServices) + assert.Equal(1, nbServices, "we expected a single Cloud Run service called %s to be deployed, found %d services", server_service_name, nbServices) + match := utils.GetFirstMatchResult(t, cloudRunServices, "kind", "Service") + serviceURL := match.Get("status.url").String() + assert.Truef(strings.HasSuffix(serviceURL, ".run.app"), "unexpected service URL %q", serviceURL) + t.Log("Cloud Run service is running at", serviceURL) + + // The Cloud Run service is the app's API backend (it does not serve the Avocano homepage) + assertResponseContains(assert, serviceURL, "/api", "/admin") + + // The data is populated by two Cloud Run jobs, so wait for the final job to finish before continuing. + // A job execution is completed if it has a completed time. + isJobFinished := func() (bool, error) { + clientJobExecs := gcloud.Run(t, "run jobs executions list ", gcloud.WithCommonArgs([]string{"--filter", "metadata.name~" + client_job_name, "--project", projectID, "--region", region, "--format", "json"})).Array() + + if len(clientJobExecs) == 0 { + t.Log("Cloud Run job been executed. Retrying...") + return true, nil + } + + match := utils.GetFirstMatchResult(t, clientJobExecs, "kind", "Execution") + completionTime := match.Get("status.completionTime").String() + + if completionTime == "" { + // retry + t.Log("Cloud Run job execution hasn't completed. Retrying...") + return true, nil + } + + t.Log("Cloud Run job completed", completionTime) + assert.NotEqual(completionTime, "", "completedTime must have a value") + + succeededCount := match.Get("status.succeededCount").Int() + assert.Equal(succeededCount, int64(1), "succeededCount must not be 0") + + return false, nil + } + utils.Poll(t, isJobFinished, 10, time.Second*10) + + // The API must return a list that includes our flagship product + assertResponseContains(assert, serviceURL+"/api/products/", flagshipProduct) + } + { + // Check that the Avocano front page is deployed to Firebase Hosting, and serving + t.Log("Firebase Hosting should be running at ", firebase_url) + assertResponseContains(assert, firebase_url, "Avocano") + } + }) + example.Test() +} + +func assertResponseContains(assert *assert.Assertions, url string, text ...string) { + code, responseBody, err := httpGetRequest(url) + assert.Nil(err) + assert.GreaterOrEqual(code, 200) + assert.LessOrEqual(code, 299) + for _, fragment := range text { + assert.Containsf(responseBody, fragment, "couldn't find %q in response body", fragment) + } +} + +func assertErrorResponseContains(assert *assert.Assertions, url string, wantCode int, text string) { + code, responseBody, err := httpGetRequest(url) + assert.Nil(err) + assert.Equal(code, wantCode) + assert.Containsf(responseBody, text, "couldn't find %q in response body", text) +} + +func httpGetRequest(url string) (statusCode int, body string, err error) { + res, err := http.Get(url) + if err != nil { + return 0, "", err + } + defer res.Body.Close() + + buffer, err := io.ReadAll(res.Body) + return res.StatusCode, string(buffer), err +}