diff --git a/app/init/Dockerfile b/app/init/Dockerfile new file mode 100644 index 00000000..bbe83037 --- /dev/null +++ b/app/init/Dockerfile @@ -0,0 +1,17 @@ +# 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. + +FROM gcr.io/google.com/cloudsdktool/cloud-sdk:slim +COPY init-execute.sh . +ENTRYPOINT ./init-execute.sh diff --git a/app/init/README.md b/app/init/README.md new file mode 100644 index 00000000..72de895a --- /dev/null +++ b/app/init/README.md @@ -0,0 +1,10 @@ +# Init + +This folder contains the source for a container image that runs the tasks required to setup the application on first deployment. + +The container is designed to be executed as a Cloud Run job, with the `roles/run.developer` role, to run the `init-execute.sh` script: + + * execute the `setup` job, [primes the the database](https://github.com/GoogleCloudPlatform/avocano/blob/main/server/scripts/prime_database.sh) script + * execute the `client` job, that runs the [client deployment](https://github.com/GoogleCloudPlatform/avocano/blob/main/client/docker-deploy.sh) + * purges cache and warms API. + diff --git a/app/init/init-execute.sh b/app/init/init-execute.sh new file mode 100755 index 00000000..6c99e9dc --- /dev/null +++ b/app/init/init-execute.sh @@ -0,0 +1,56 @@ +#!/bin/bash +# 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. + +# Script to assist in Dockerfile-based deployments. +# any errors? exit immediately. +set -e + +# escape if project_id not defined (mandatory, required later) +if [[ -z $PROJECT_ID ]]; then + echo "PROJECT_ID not defined. Cannot deploy. Exiting." + exit 1 +fi + +# 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 + +# Define common defaults, all overrideable. +REGION="${REGION:-us-central1}" +SETUP_JOB="${SETUP_JOB:-setup}" +CLIENT_JOB="${CLIENT_JOB:-client}" + +echo "*** Executing initization job ***" +echo "PROJECT_ID: $PROJECT_ID" +echo "REGION: $REGION" +echo "SETUP JOB: $SETUP_JOB" +echo "CLIENT JOB: $CLIENT_JOB" +echo "FIREBASE URL: $FIREBASE_URL" +echo "SERVER URL: $SERVER_URL" +echo "" + +echo "Running init database migration..." +gcloud run jobs execute "$SETUP_JOB" --wait --project "$PROJECT_ID" --region "$REGION" + +echo "Running client deploy..." +gcloud run jobs execute "$CLIENT_JOB" --wait --project "$PROJECT_ID" --region "$REGION" + +echo "Purge Firebase cache" +echo curl -X PURGE "${FIREBASE_URL}/" + +echo "Warm up API" +curl "${SERVER_URL}/api/products/?warmup" diff --git a/app/init/init-image.cloudbuild.yaml b/app/init/init-image.cloudbuild.yaml new file mode 100644 index 00000000..7b854213 --- /dev/null +++ b/app/init/init-image.cloudbuild.yaml @@ -0,0 +1,36 @@ +# 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. + +# Build placeholder site code into container + +steps: + - id: build + name: "gcr.io/cloud-builders/docker" + dir: app/init + args: + [ + "build", + "-t", + "gcr.io/$PROJECT_ID/$_IMAGE_NAME", + ".", + ] + +images: + - gcr.io/$PROJECT_ID/$_IMAGE_NAME + +substitutions: + _IMAGE_NAME: avocano-init + +options: + dynamic_substitutions: true diff --git a/app/placeholder/Dockerfile b/app/placeholder/Dockerfile index 81870466..5bc8abe6 100644 --- a/app/placeholder/Dockerfile +++ b/app/placeholder/Dockerfile @@ -17,7 +17,7 @@ ARG PROJECT_ID=YOURPROJECTID FROM gcr.io/$PROJECT_ID/firebase -RUN apk add gettext +RUN apk add gettext curl RUN npm install -g json COPY . ./ diff --git a/app/placeholder/placeholder-deploy.sh b/app/placeholder/placeholder-deploy.sh index c3ce9941..46cc3118 100755 --- a/app/placeholder/placeholder-deploy.sh +++ b/app/placeholder/placeholder-deploy.sh @@ -17,6 +17,21 @@ # any errors? exit immediately. set -e +# 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 +39,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 +50,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/containers.tf b/infra/containers.tf index 1fefdade..ee70766c 100644 --- a/infra/containers.tf +++ b/infra/containers.tf @@ -19,5 +19,6 @@ 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:postjsscurl" # TODO(glasnt): revert to tag "latest" + init_image = "gcr.io/hsa-public/avocano-init:postjsscurl" # TODO(glasnt): revert to tag "latest" } diff --git a/infra/iam.tf b/infra/iam.tf index 2513e9eb..7dd2f0fd 100644 --- a/infra/iam.tf +++ b/infra/iam.tf @@ -41,9 +41,9 @@ 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 } @@ -88,12 +88,12 @@ resource "google_project_iam_member" "client_permissions" { depends_on = [google_service_account.client] } -# GCE instance needs access to start Jobs -resource "google_project_iam_member" "computestartup_permissions" { +# Init process needs access to start Jobs +resource "google_project_iam_member" "initstartup_permissions" { project = var.project_id role = "roles/run.developer" - member = "serviceAccount:${google_service_account.compute[0].email}" - depends_on = [google_service_account.compute] + member = "serviceAccount:${google_service_account.init[0].email}" + depends_on = [google_service_account.init] count = var.init ? 1 : 0 } diff --git a/infra/jobs.tf b/infra/jobs.tf index 74583cdc..bed42a35 100644 --- a/infra/jobs.tf +++ b/infra/jobs.tf @@ -144,32 +144,3 @@ resource "google_cloud_run_v2_job" "client" { ] } - -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 - ] -} diff --git a/infra/postdeployment.tf b/infra/postdeployment.tf index 0ca6b1dd..8c2d8dad 100644 --- a/infra/postdeployment.tf +++ b/infra/postdeployment.tf @@ -14,126 +14,140 @@ * limitations under the License. */ -resource "google_compute_network" "gce_init" { - count = var.init ? 1 : 0 - - 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 - depends_on = [module.project_services] +# used to collect access token, for authenticated POST commands +data "google_client_config" "current" { } -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 - - depends_on = [module.project_services] -} - -resource "google_compute_instance" "gce_init" { - 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 - - allow_stopping_for_update = true - - boot_disk { - initialize_params { - image = "debian-cloud/debian-11" +# Job that uses pre-built docker image to deploy a placeholder website. +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 + + # Variables consumed by /app/placeholder/placeholder-deploy.sh + env { + name = "PROJECT_ID" + value = var.project_id + } + env { + name = "SUFFIX" + value = var.random_suffix ? random_id.suffix.hex : "" + } + env { + name = "FIREBASE_URL" + value = local.firebase_url + } + } } } - 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 - } - } + depends_on = [ + module.project_services + ] - service_account { - email = google_service_account.compute[0].email - scopes = ["cloud-platform"] # TODO: Restrict?? + # work around, b/292021282 + lifecycle { + prevent_destroy = true } - - metadata_startup_script = <