diff --git a/README.md b/README.md index 0789bdd0..01809cee 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,7 @@ The following dependencies must be available: A service account with the following roles must be used to provision the resources of this module: +- roles/cloudbuild.builds.editor - roles/cloudsql.admin - roles/compute.admin - roles/compute.networkAdmin @@ -75,6 +76,7 @@ the resources of this module: - roles/firebasehosting.admin - roles/iam.serviceAccountAdmin - roles/iam.serviceAccountUser +- roles/pubsub.editor - roles/resourcemanager.projectIamAdmin - roles/run.admin - roles/secretmanager.admin diff --git a/app/placeholder/Dockerfile b/app/placeholder/Dockerfile index 81870466..45b23c26 100644 --- a/app/placeholder/Dockerfile +++ b/app/placeholder/Dockerfile @@ -16,9 +16,10 @@ # Execute with "docker run --build-arg PROJECT_ID=$PROJECT_ID ..." ARG PROJECT_ID=YOURPROJECTID FROM gcr.io/$PROJECT_ID/firebase +WORKDIR /app -RUN apk add gettext +RUN apk add gettext curl RUN npm install -g json -COPY . ./ +COPY . /app/ -ENTRYPOINT ./placeholder-deploy.sh +ENTRYPOINT ["/app/placeholder-deploy.sh"] diff --git a/app/placeholder/placeholder-deploy.sh b/app/placeholder/placeholder-deploy.sh index c3ce9941..d55df618 100755 --- a/app/placeholder/placeholder-deploy.sh +++ b/app/placeholder/placeholder-deploy.sh @@ -17,6 +17,24 @@ # any errors? exit immediately. set -e +# Ensure we got to the right directory. Cloud Build may start us in /workspace +cd /app + +# escape if firebase_url not defined (mandatory, required later) +if [[ -z $FIREBASE_URL ]]; then + echo "FIREBASE_URL not defined. Cannot deploy. Exiting." + exit 1 +fi + +# Only run the placeholder script if the site has been deployed before. +# Check if the firebase url has "Site Not Found" (the pre-deployment state) +if curl "$FIREBASE_URL" | grep -q "Site Not Found"; then + echo "Firebase site $FIREBASE_URL hasn't been deployed before, so it needs a placeholder." +else + echo "Firebase site $FIREBASE_URL has been deployed before. Not going to deploy placeholder. Exiting." + exit 0 +fi + # 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 @@ -24,7 +42,7 @@ if [[ -n $SUFFIX ]]; then UPDATED=true # Use template file to generate configuration - envsubst < firebaserc.tmpl > .firebaserc + envsubst .firebaserc echo "Customised .firebaserc created to support site." cat .firebaserc fi @@ -35,6 +53,10 @@ if [[ -n $UPDATED ]]; then cat firebase.json fi +# Finally, deploy the application echo "Deploying placeholder to Firebase..." - firebase deploy --project "$PROJECT_ID" --only hosting + +# Setup for greater chances of success by explicitly purging cache +echo "Purging firebase cache" +curl -X PURGE "${FIREBASE_URL}/" diff --git a/infra/README.md b/infra/README.md index 977d1d65..cf4de0c1 100644 --- a/infra/README.md +++ b/infra/README.md @@ -48,7 +48,7 @@ Functional examples are included in the | database\_name | Cloud SQL database name | `string` | `"django"` | no | | database\_username | Cloud SQL database name | `string` | `"server"` | no | | enable\_apis | Whether or not to enable underlying apis in this solution. | `bool` | `true` | no | -| image\_version | Version of the Container Registry image to use | `string` | `"v1.8.2"` | no | +| image\_version | Version of the Container Registry image to use | `string` | `"v1.9.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 | @@ -89,6 +89,7 @@ The following dependencies must be available: A service account with the following roles must be used to provision the resources of this module: +- roles/cloudbuild.builds.editor - roles/cloudsql.admin - roles/compute.admin - roles/compute.networkAdmin @@ -96,6 +97,7 @@ the resources of this module: - roles/firebasehosting.admin - roles/iam.serviceAccountAdmin - roles/iam.serviceAccountUser +- roles/pubsub.editor - roles/resourcemanager.projectIamAdmin - roles/run.admin - roles/secretmanager.admin diff --git a/infra/apis.tf b/infra/apis.tf index 583e47c4..2916801f 100644 --- a/infra/apis.tf +++ b/infra/apis.tf @@ -50,6 +50,7 @@ module "project_services" { "compute.googleapis.com", "firebase.googleapis.com", "firebasehosting.googleapis.com", + "pubsub.googleapis.com", "iam.googleapis.com", "run.googleapis.com", "secretmanager.googleapis.com", diff --git a/infra/containers.tf b/infra/containers.tf index 1fefdade..111cadad 100644 --- a/infra/containers.tf +++ b/infra/containers.tf @@ -19,5 +19,5 @@ locals { server_image = "gcr.io/${var.server_image_host}/server:${var.image_version}" client_image = "gcr.io/${var.client_image_host}/client:${var.image_version}" - placeholder_image = "gcr.io/hsa-public/avocano-placeholder:latest" + placeholder_image = "gcr.io/hsa-public/avocano-placeholder:postjsstrigger" # TODO(glasnt): revert to tag "latest" } diff --git a/infra/iam.tf b/infra/iam.tf index 2513e9eb..74e545e4 100644 --- a/infra/iam.tf +++ b/infra/iam.tf @@ -14,15 +14,31 @@ * limitations under the License. */ -# Service Accounts - locals { - # Helpers for the clunky formatting of these values - automation_SA = "serviceAccount:${google_service_account.automation.email}" - server_SA = "serviceAccount:${google_service_account.server.email}" - client_SA = "serviceAccount:${google_service_account.client.email}" + # Lists of required roles + server_iam_members = [ + "roles/cloudsql.client", + "roles/run.viewer", + "roles/cloudtrace.agent" + ] + client_iam_members = [ + "roles/run.viewer", + "roles/firebasehosting.admin", + ] + automation_iam_members = [ + "roles/cloudsql.client" + ] + init_iam_members = [ + "roles/logging.logWriter", + "roles/cloudbuild.builds.builder", + "roles/iam.serviceAccountUser", + "roles/run.developer", + "roles/firebasehosting.admin" + ] } +# Accounts + resource "google_service_account" "server" { account_id = var.random_suffix ? "api-backend-${random_id.suffix.hex}" : "api-backend" display_name = "API Backend service account" @@ -41,66 +57,44 @@ resource "google_service_account" "automation" { depends_on = [module.project_services] } -resource "google_service_account" "compute" { - account_id = var.random_suffix ? "compute-startup-${random_id.suffix.hex}" : "compute-startup" - display_name = "Head Start App Compute Instance SA" + +resource "google_service_account" "init" { + account_id = var.random_suffix ? "init-startup-${random_id.suffix.hex}" : "init-startup" + display_name = "Jump Start App Init SA" depends_on = [module.project_services] count = var.init ? 1 : 0 } -# The Cloud Run server can access the database +# Permissions + resource "google_project_iam_member" "server_permissions" { - project = var.project_id - role = "roles/cloudsql.client" - member = local.server_SA - depends_on = [google_service_account.server] -} + count = length(local.server_iam_members) -# Cloud Build can access the database -resource "google_project_iam_member" "build_permissions" { - project = var.project_id - role = "roles/cloudsql.client" - member = local.automation_SA - depends_on = [google_service_account.automation] + project = var.project_id + role = local.server_iam_members[count.index] + member = "serviceAccount:${google_service_account.server.email}" } -# Server needs introspection permissions -resource "google_project_iam_member" "server_introspection" { - project = var.project_id - role = "roles/run.viewer" - member = local.server_SA - depends_on = [google_service_account.server] -} +resource "google_project_iam_member" "client_permissions" { + count = length(local.client_iam_members) -# Client needs introspection permissions -resource "google_project_iam_member" "client_introspection" { - project = var.project_id - role = "roles/run.viewer" - member = local.client_SA - depends_on = [google_service_account.client] + project = var.project_id + role = local.client_iam_members[count.index] + member = "serviceAccount:${google_service_account.client.email}" } -# Client may need permission to deploy the front end -resource "google_project_iam_member" "client_permissions" { - project = var.project_id - role = "roles/firebasehosting.admin" - member = local.client_SA - depends_on = [google_service_account.client] -} +resource "google_project_iam_member" "automation_permissions" { + count = length(local.automation_iam_members) -# GCE instance needs access to start Jobs -resource "google_project_iam_member" "computestartup_permissions" { - project = var.project_id - role = "roles/run.developer" - member = "serviceAccount:${google_service_account.compute[0].email}" - depends_on = [google_service_account.compute] - count = var.init ? 1 : 0 + project = var.project_id + role = local.automation_iam_members[count.index] + member = "serviceAccount:${google_service_account.automation.email}" } -# Server needs to write to Cloud Trace -resource "google_project_iam_member" "server_traceagent" { - project = var.project_id - role = "roles/cloudtrace.agent" - member = local.server_SA - depends_on = [google_service_account.server] +resource "google_project_iam_member" "init_permissions" { + count = length(local.init_iam_members) + + project = var.project_id + role = local.init_iam_members[count.index] + member = "serviceAccount:${google_service_account.init[0].email}" } diff --git a/infra/jobs.tf b/infra/jobs.tf index 74583cdc..4cf808fa 100644 --- a/infra/jobs.tf +++ b/infra/jobs.tf @@ -14,54 +14,7 @@ * limitations under the License. */ -resource "google_cloud_run_v2_job" "setup" { - name = var.random_suffix ? "setup-${random_id.suffix.hex}" : "setup" - location = var.region - - labels = var.labels - - template { - template { - service_account = google_service_account.automation.email - containers { - image = local.server_image - command = ["setup"] - env { - name = "DJANGO_ENV" - value_source { - secret_key_ref { - secret = google_secret_manager_secret.django_settings.secret_id - version = "latest" - } - } - } - env { - name = "DJANGO_SUPERUSER_PASSWORD" - value_source { - secret_key_ref { - secret = google_secret_manager_secret.django_admin_password.secret_id - version = "latest" - } - } - } - volume_mounts { - name = "cloudsql" - mount_path = "/cloudsql" - } - } - volumes { - name = "cloudsql" - cloud_sql_instance { - instances = [google_sql_database_instance.postgres.connection_name] - } - } - } - } - depends_on = [ - google_secret_manager_secret_version.django_settings - ] -} - +# Job to apply server database updates resource "google_cloud_run_v2_job" "migrate" { name = var.random_suffix ? "migrate-${random_id.suffix.hex}" : "migrate" location = var.region @@ -101,75 +54,4 @@ resource "google_cloud_run_v2_job" "migrate" { ] } - - -resource "google_cloud_run_v2_job" "client" { - - name = var.random_suffix ? "client-${random_id.suffix.hex}" : "client" - location = var.region - - labels = var.labels - - template { - template { - 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 : "" - } - env { - name = "SERVICE_NAME" - value = google_cloud_run_v2_service.server.name - } - env { - name = "REGION" - value = var.region - } - env { - name = "PROJECT_ID" - value = var.project_id - } - - } - } - } - - depends_on = [ - module.project_services - ] -} - - -resource "google_cloud_run_v2_job" "placeholder" { - name = var.random_suffix ? "placeholder-${random_id.suffix.hex}" : "placeholder" - location = var.region - - labels = var.labels - - template { - template { - service_account = google_service_account.client.email - containers { - image = local.placeholder_image - env { - name = "PROJECT_ID" - value = var.project_id - } - - env { - name = "SUFFIX" - value = var.random_suffix ? random_id.suffix.hex : "" - } - } - } - } - - depends_on = [ - module.project_services - ] -} +# Other jobs defined in postdeployment.tf diff --git a/infra/metadata.yaml b/infra/metadata.yaml index 04c0cd4c..17bfe30d 100644 --- a/infra/metadata.yaml +++ b/infra/metadata.yaml @@ -69,7 +69,7 @@ spec: - name: image_version description: Version of the Container Registry image to use varType: string - defaultValue: v1.8.2 + defaultValue: v1.9.0 - name: init description: Initialize database? varType: bool @@ -125,6 +125,7 @@ spec: roles: - level: Project roles: + - roles/cloudbuild.builds.editor - roles/cloudsql.admin - roles/compute.admin - roles/compute.networkAdmin @@ -132,6 +133,7 @@ spec: - roles/firebasehosting.admin - roles/iam.serviceAccountAdmin - roles/iam.serviceAccountUser + - roles/pubsub.editor - roles/resourcemanager.projectIamAdmin - roles/run.admin - roles/secretmanager.admin @@ -158,6 +160,7 @@ spec: - firebase.googleapis.com - firebasehosting.googleapis.com - iam.googleapis.com + - pubsub.googleapis.com - run.googleapis.com - secretmanager.googleapis.com - sqladmin.googleapis.com diff --git a/infra/outputs.tf b/infra/outputs.tf index a6790c00..ab48df7f 100644 --- a/infra/outputs.tf +++ b/infra/outputs.tf @@ -71,5 +71,5 @@ output "server_service_name" { output "client_job_name" { description = "Name of the Cloud Run Job, deploying the front end" - value = google_cloud_run_v2_job.client.name + value = local.client_job_name } diff --git a/infra/postdeployment.tf b/infra/postdeployment.tf index 0ca6b1dd..6a0383b1 100644 --- a/infra/postdeployment.tf +++ b/infra/postdeployment.tf @@ -14,126 +14,195 @@ * limitations under the License. */ -resource "google_compute_network" "gce_init" { - count = var.init ? 1 : 0 +locals { + random_suffix_value = var.random_suffix ? random_id.suffix.hex : "" # literal value (NNNN) + random_suffix_append = var.random_suffix ? "-${random_id.suffix.hex}" : "" # appended value (-NNNN) - name = var.random_suffix ? "gce-init-network-${random_id.suffix.hex}" : "gce-init-network" - auto_create_subnetworks = false - routing_mode = "GLOBAL" - project = var.project_id - delete_default_routes_on_create = false - mtu = 0 + setup_job_name = "setup${local.random_suffix_append}" + client_job_name = "client${local.random_suffix_append}" - depends_on = [module.project_services] + gcloud_step_container = "gcr.io/google.com/cloudsdktool/cloud-sdk:slim" } -resource "google_compute_subnetwork" "gce_init" { - count = var.init ? 1 : 0 - - name = var.random_suffix ? "subnet-gce-init-${random_id.suffix.hex}" : "subnet-gce-init" - network = google_compute_network.gce_init[0].id - ip_cidr_range = "10.10.10.0/24" - region = var.region +# used to collect access token, for authenticated POST commands +data "google_client_config" "current" { +} - depends_on = [module.project_services] +# topic that is never used, except as a configuration type for Cloud Build Triggers +resource "google_pubsub_topic" "faux" { + name = "faux-topic${local.random_suffix_append}" } -resource "google_compute_instance" "gce_init" { +## Placeholder - deploys a placeholder website - uses prebuilt image in /app/placeholder +resource "google_cloudbuild_trigger" "placeholder" { count = var.init ? 1 : 0 - depends_on = [ - module.project_services, - google_sql_database_instance.postgres, - google_cloud_run_v2_job.setup, - google_cloud_run_v2_job.client, - ] - - name = var.random_suffix ? "head-start-initialize-${random_id.suffix.hex}" : "head-start-initialize" - machine_type = "n1-standard-1" - zone = var.zone - desired_status = "RUNNING" # https://github.com/GoogleCloudPlatform/terraform-dynamic-python-webapp/pull/75#issuecomment-1547198414 + name = "placeholder${local.random_suffix_append}" + location = var.region - allow_stopping_for_update = true + description = "Deploy a placeholder Firebase website" - boot_disk { - initialize_params { - image = "debian-cloud/debian-11" - } + pubsub_config { + topic = google_pubsub_topic.faux.id } - network_interface { - network = google_compute_network.gce_init[0].self_link - subnetwork = google_compute_subnetwork.gce_init[0].self_link - - access_config { - // Ephemeral public IP + service_account = google_service_account.init[0].id + + build { + step { + id = "deploy-placeholder" + name = local.placeholder_image + env = [ + "PROJECT_ID=${var.project_id}", + "SUFFIX=${local.random_suffix_value}", + "FIREBASE_URL=${local.firebase_url}", + ] } - } - service_account { - email = google_service_account.compute[0].email - scopes = ["cloud-platform"] # TODO: Restrict?? + options { + logging = "CLOUD_LOGGING_ONLY" + } } - metadata_startup_script = <