#!/usr/bin/env bash # Trustico® CaaS for Google Cloud : Optional Extra. The load balancer script for the classic Compute store. # # Purpose # The job installs each new SSL Certificate in the classic Compute store as a new resource. A load balancer keeps using the resource it was given until it is told to use the new one. This script tells it. Every day, after the job, it puts the newest SSL Certificate on your load balancer. On a day with nothing new it does nothing. It never deletes anything. # This file installs the script. Run it once in Cloud Shell and answer its questions. It creates the script in your project, sets it to run every day, and runs it once so that you can see the result. Run it again whenever your answers change, or when Trustico® publishes a new version of the script : it applies your answers again, and leaves what already matches as it is. # # The Questions # Your project. The region of your job. The proxy of your load balancer : a load balancer holds its SSL Certificate in a part called the target HTTPS proxy, and the installer lists the proxies in your project so that you can pick yours. The series : the name you gave the SSL Certificate in the job configuration file. The service account of the job. The hour of the day the script runs, 05:00 UTC unless you choose another, so that it runs after the job. # # Running the Installer # Open Cloud Shell in the Google Cloud console and run these two commands. # curl -O https://cdn.trustico.com/caas/gcloud/install-classic-proxy-script.sh # bash install-classic-proxy-script.sh # Answer the questions. Type yes when it shows you your answers. The first time takes a few minutes while Google builds the script. # # The Result # It prints each thing it does, then the result of the first run. "replaced" means the newest SSL Certificate is now on your load balancer in place of the old one. "unchanged" means it was there already. "appended" means the load balancer had none of this series before and has the newest now. If something is wrong, it says what. # # Afterwards # Every day the job runs first, then the script. Its runs are listed in the console under Cloud Run, Services, trustico-caas-classic-proxy, Logs. Old SSL Certificate resources stay in the classic store until you delete them. To remove the script, run these two commands in Cloud Shell, with your region : # gcloud scheduler jobs delete trustico-caas-classic-proxy-daily --location=YOUR-REGION # gcloud run services delete trustico-caas-classic-proxy --region=YOUR-REGION # # If you prefer, give the answers on the command line, in this order : project, region, proxy, series, service account, and the hour if you want one. set -euo pipefail SERVICE="trustico-caas-classic-proxy" SCHEDULE="trustico-caas-classic-proxy-daily" FILES="https://cdn.trustico.com/caas/gcloud/classic-proxy-certificate" fail() { echo "$1" >&2; exit 1; } # ask VARIABLE "question" "default" : one question, Enter keeps the default when there is one. ask() { local var="$1" question="$2" default="${3:-}" answer if [[ -n "$default" ]]; then read -r -p " $question [$default] : " answer answer="${answer:-$default}" else read -r -p " $question : " answer fi printf -v "$var" '%s' "$answer" } # choose VARIABLE "what" item... : one item is offered as the default ; several are numbered to pick from, or another can be typed. choose() { local var="$1" what="$2" shift 2 local items=("$@") answer i if (( ${#items[@]} == 0 )); then ask "$var" "$what" return fi if (( ${#items[@]} == 1 )); then ask "$var" "$what" "${items[0]}" return fi echo " $what :" for i in "${!items[@]}"; do echo " $((i + 1))) ${items[$i]}" done while true; do read -r -p " the number of your choice, or type another : " answer if [[ "$answer" =~ ^[0-9]+$ ]]; then if (( 10#$answer >= 1 && 10#$answer <= ${#items[@]} )); then printf -v "$var" '%s' "${items[$((10#$answer - 1))]}" return fi echo " there is no choice $answer : type a number from 1 to ${#items[@]}" continue fi if [[ -n "$answer" ]]; then printf -v "$var" '%s' "$answer" return fi done } if (( $# >= 5 )); then PROJECT="$1"; REGION="$2"; PROXY="$3"; SERIES="$4"; ACCOUNT="$5"; HOUR="${6:-5}" elif (( $# == 0 )); then echo "=== YOUR VALUES" ask PROJECT "Your project ID" "$(gcloud config get-value project 2>/dev/null || true)" # The region of the job : offered from the jobs the project has, typed when that cannot be read. mapfile -t REGIONS < <(gcloud run jobs list --project="$PROJECT" --format=json 2>/dev/null | python3 -c 'import json, sys; print("\n".join(sorted({j["metadata"]["labels"].get("cloud.googleapis.com/location", "") for j in json.load(sys.stdin)} - {""})))' 2>/dev/null || true) choose REGION "The region of your job" "${REGIONS[@]}" mapfile -t PROXIES < <(gcloud compute target-https-proxies list --global --project="$PROJECT" --format="value(name)" 2>/dev/null | sort || true) choose PROXY "The target HTTPS proxy of your load balancer" "${PROXIES[@]}" # A series is what is left of a classic resource name once its -01, -02 is removed. mapfile -t SERIESES < <(gcloud compute ssl-certificates list --global --project="$PROJECT" --format="value(name)" 2>/dev/null | grep -E -- '-[0-9]+$' | sed -E 's/-[0-9]+$//' | sort -u || true) choose SERIES "The series, the name you gave the SSL Certificate in the job configuration file" "${SERIESES[@]}" mapfile -t ACCOUNTS < <(gcloud iam service-accounts list --project="$PROJECT" --format="value(email)" 2>/dev/null | sort || true) choose ACCOUNT "The service account of the job" "${ACCOUNTS[@]}" ask HOUR "The hour of the day at which the script runs, 0 to 23 in UTC" "5" echo echo " project $PROJECT, region $REGION, proxy $PROXY, series $SERIES, service account $ACCOUNT, every day at $HOUR:00 UTC" read -r -p " Create it now ? Type yes to continue : " GO [[ "$GO" == "yes" ]] || { echo "NOTHING CREATED"; exit 0; } else echo "USAGE : bash $0 (and answer the questions), or bash $0 YOUR-PROJECT-ID YOUR-REGION YOUR-PROXY YOUR-SERIES YOUR-SERVICE-ACCOUNT [HOUR]" exit 2 fi echo "=== GUARDS" # The project number names the account Google builds the script with, below. NUMBER="$(gcloud projects describe "$PROJECT" --format="value(projectNumber)" 2>/dev/null || true)" [[ -n "$NUMBER" ]] || fail "PROJECT NOT FOUND, OR NO ACCESS TO IT : $PROJECT" [[ "$HOUR" =~ ^0?([0-9]|1[0-9]|2[0-3])$ ]] || fail "THE HOUR MUST BE A NUMBER FROM 0 TO 23 : $HOUR" HOUR=$((10#$HOUR)) gcloud iam service-accounts describe "$ACCOUNT" --project="$PROJECT" >/dev/null 2>&1 || fail "SERVICE ACCOUNT NOT FOUND : $ACCOUNT" gcloud compute target-https-proxies describe "$PROXY" --global --project="$PROJECT" --format="value(name)" >/dev/null 2>&1 || fail "TARGET HTTPS PROXY NOT FOUND : $PROXY" NEWEST="$(gcloud compute ssl-certificates list --global --project="$PROJECT" --filter="name~^${SERIES}-[0-9]+$" --format="value(name)" | sort | tail -1)" [[ -n "$NEWEST" ]] || fail "NO SSL CERTIFICATE RESOURCE OF THE SERIES $SERIES EXISTS YET : THE JOB CREATES THE FIRST ONE ON ITS FIRST RUN" echo " the newest resource of $SERIES is $NEWEST" echo "=== APIS" # Google builds the script with Cloud Build and keeps its image in Artifact Registry ; both are enabled here so that the deploy below never stops to ask. gcloud services enable run.googleapis.com cloudbuild.googleapis.com artifactregistry.googleapis.com logging.googleapis.com cloudscheduler.googleapis.com compute.googleapis.com --project="$PROJECT" >/dev/null echo " enabled" # Google builds the script as the Compute Engine default service account of the project. In an organisation created after May 3, 2024 that account starts with no role at all and the build is refused, so it is given the Cloud Run Builder role here, the role Google's own instructions for a deploy from source grant it. Where it already has more, this changes nothing. BUILDER="${NUMBER}-compute@developer.gserviceaccount.com" # --condition=None adds the role without a condition ; without it gcloud stops at a menu of conditions whenever the project already holds a conditional grant, such as an expiring one. gcloud projects add-iam-policy-binding "$PROJECT" --member="serviceAccount:$BUILDER" --role="roles/run.builder" --condition=None >/dev/null echo " $BUILDER may build it" # Cloud Scheduler is not offered in every region Cloud Run is ; the schedule lives in the region of the job, so that region must have it. gcloud scheduler locations describe "$REGION" --project="$PROJECT" >/dev/null 2>&1 || fail "CLOUD SCHEDULER IS NOT AVAILABLE IN $REGION : the schedule cannot live in the region of your job (gcloud scheduler locations list shows the regions it supports)" echo "=== THE SCRIPT" WORK="$(mktemp -d)" trap 'rm -rf "$WORK"' EXIT curl -fsS -o "$WORK/index.js" "$FILES/index.js" || fail "COULD NOT DOWNLOAD $FILES/index.js" curl -fsS -o "$WORK/package.json" "$FILES/package.json" || fail "COULD NOT DOWNLOAD $FILES/package.json" echo " building and deploying $SERVICE (a few minutes the first time)" # A role granted a moment ago takes a couple of minutes to apply, Google says, and until it does the build is refused. So a deploy that fails is tried again after a minute, up to three times, before the installer gives up. DEPLOYED="" for attempt in 1 2 3; do if gcloud run deploy "$SERVICE" --project="$PROJECT" --region="$REGION" --source="$WORK" --function=classicProxyCertificate --base-image=nodejs22 --no-allow-unauthenticated --service-account="$ACCOUNT" --set-env-vars="PROXY=$PROXY,SERIES=$SERIES" --quiet; then DEPLOYED="yes" break fi if (( attempt < 3 )); then echo " the deploy did not succeed ; a permission granted a moment ago can take a couple of minutes to apply ; waiting a minute and trying again" sleep 60 fi done [[ -n "$DEPLOYED" ]] || fail "THE DEPLOY FAILED THREE TIMES : the error above says why ; fix it and run this installer again" URL="$(gcloud run services describe "$SERVICE" --project="$PROJECT" --region="$REGION" --format="value(status.url)")" [[ -n "$URL" ]] || fail "THE SCRIPT HAS NO ADDRESS AFTER THE DEPLOY : $SERVICE" echo " deployed : $SERVICE at $URL" echo "=== PERMISSION" gcloud run services add-iam-policy-binding "$SERVICE" --project="$PROJECT" --region="$REGION" --member="serviceAccount:$ACCOUNT" --role="roles/run.invoker" >/dev/null echo " $ACCOUNT may start it" echo "=== THE DAILY SCHEDULE" if gcloud scheduler jobs describe "$SCHEDULE" --project="$PROJECT" --location="$REGION" >/dev/null 2>&1; then gcloud scheduler jobs update http "$SCHEDULE" --project="$PROJECT" --location="$REGION" --schedule="0 $HOUR * * *" --time-zone="Etc/UTC" --uri="$URL" --http-method=POST --oidc-service-account-email="$ACCOUNT" --oidc-token-audience="$URL" >/dev/null echo " exists : $SCHEDULE, set to every day at $(printf '%02d' "$HOUR"):00 UTC" else gcloud scheduler jobs create http "$SCHEDULE" --project="$PROJECT" --location="$REGION" --schedule="0 $HOUR * * *" --time-zone="Etc/UTC" --uri="$URL" --http-method=POST --oidc-service-account-email="$ACCOUNT" --oidc-token-audience="$URL" >/dev/null echo " created : $SCHEDULE, every day at $(printf '%02d' "$HOUR"):00 UTC" fi echo "=== THE FIRST RUN" # The schedule is started once by hand, so that the first run takes the very path every later run takes : the service account, its token, and the permission granted above. A permission granted seconds ago can take a minute or more to apply, and until it does the call is refused with 403 ; the schedule is then started again. Only results written after this moment count. STARTED="$(date -u +%Y-%m-%dT%H:%M:%SZ)" RESULT="" for attempt in 1 2 3 4 5 6; do gcloud scheduler jobs run "$SCHEDULE" --project="$PROJECT" --location="$REGION" >/dev/null echo " started through the schedule, reading the result" for _ in $(seq 1 8); do sleep 5 RESULT="$(gcloud logging read "resource.type=\"cloud_run_revision\" AND resource.labels.service_name=\"$SERVICE\" AND jsonPayload.message:\"CLASSIC PROXY SSL CERTIFICATE\" AND timestamp>=\"$STARTED\"" --project="$PROJECT" --limit=1 --format=json 2>/dev/null | python3 -c 'import json, sys; e = json.load(sys.stdin); print(json.dumps(e[0]["jsonPayload"], indent=2) if e else "")' 2>/dev/null || true)" [[ -n "$RESULT" ]] && break done [[ -n "$RESULT" ]] && break LAST_CODE="$(gcloud logging read "resource.type=\"cloud_run_revision\" AND resource.labels.service_name=\"$SERVICE\" AND logName:\"requests\" AND timestamp>=\"$STARTED\"" --project="$PROJECT" --limit=1 --format="value(httpRequest.status)" 2>/dev/null || true)" if [[ "$LAST_CODE" == "403" ]]; then echo " the permission has not applied yet (Google refused the call) ; waiting twenty seconds and starting it again" sleep 20 else echo " no result yet ; waiting twenty seconds and starting it again" sleep 20 fi done [[ -n "$RESULT" ]] || fail "NO RESULT AFTER SEVERAL ATTEMPTS : read the Logs of $SERVICE under Cloud Run, Services, and the row of $SCHEDULE under Cloud Scheduler" echo "$RESULT" | sed 's/^/ /' [[ "$RESULT" != *'"CLASSIC PROXY SSL CERTIFICATE FAILED"'* ]] || fail "THE FIRST RUN FAILED : the error above says why ; fix it and run this installer again" echo "DONE : $SERVICE runs every day at $(printf '%02d' "$HOUR"):00 UTC and puts the newest resource of $SERIES on $PROXY ; its runs are under Cloud Run, Services, $SERVICE, Logs"