From a9e047684d64beaa9500ee885597501ab927e4f0 Mon Sep 17 00:00:00 2001 From: chapati Date: Thu, 29 Aug 2024 14:22:47 +0200 Subject: [PATCH] Migrate to same setup as Oracle Relayer (#18) --- .trunk/trunk.yaml | 16 +- DEPLOY_FROM_SCRATCH.md | 162 ++++++++++ README.md | 302 ++++-------------- bin/check-gcloud-login.sh | 32 ++ bin/deploy-via-gcloud.sh | 32 ++ bin/get-logs-url.sh | 16 + bin/get-logs.sh | 31 ++ bin/get-project-vars.sh | 153 +++++++++ bin/set-up-terraform.sh | 73 +++++ .../test-deployed-function.sh | 0 deploy-via-gcloud.sh | 29 -- get-logs-url.sh | 10 - get-logs.sh | 21 -- infra/cloud_function.tf | 16 +- infra/cloudbuild.tf | 6 +- infra/local-dotenv-file.tf | 2 +- infra/main.tf | 65 +--- infra/monitoring.tf | 6 +- infra/secret-manager.tf | 8 +- infra/storage.tf | 8 +- infra/variables.tf | 39 +-- infra/versions.tf | 32 ++ package-lock.json | 226 ++++++++++++- package.json | 17 +- set-project-id.sh | 50 --- set-project-vars.sh | 79 ----- 26 files changed, 892 insertions(+), 539 deletions(-) create mode 100644 DEPLOY_FROM_SCRATCH.md create mode 100755 bin/check-gcloud-login.sh create mode 100755 bin/deploy-via-gcloud.sh create mode 100755 bin/get-logs-url.sh create mode 100755 bin/get-logs.sh create mode 100755 bin/get-project-vars.sh create mode 100755 bin/set-up-terraform.sh rename test-deployed-function.sh => bin/test-deployed-function.sh (100%) delete mode 100755 deploy-via-gcloud.sh delete mode 100755 get-logs-url.sh delete mode 100755 get-logs.sh create mode 100644 infra/versions.tf delete mode 100755 set-project-id.sh delete mode 100755 set-project-vars.sh diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml index 818db6d..f90093d 100644 --- a/.trunk/trunk.yaml +++ b/.trunk/trunk.yaml @@ -2,12 +2,12 @@ # To learn more about the format of this file, see https://docs.trunk.io/reference/trunk-yaml version: 0.1 cli: - version: 1.22.2 + version: 1.22.3 # Trunk provides extensibility via plugins. (https://docs.trunk.io/plugins) plugins: sources: - id: trunk - ref: v1.6.1 + ref: v1.6.2 uri: https://github.com/trunk-io/plugins # Many linters and tools depend on runtimes - configure them here. (https://docs.trunk.io/runtimes) runtimes: @@ -23,25 +23,25 @@ lint: - markdown-table-prettify enabled: - actionlint@1.7.1 - - checkov@3.2.198 + - checkov@3.2.236 - dotenv-linter@3.3.0 - dustilock@1.2.0 - - eslint@9.7.0 + - eslint@9.9.1 - git-diff-check - gitleaks@8.18.4 - markdown-link-check@3.12.2 - markdownlint@0.41.0 - - osv-scanner@1.8.2 + - osv-scanner@1.8.4 - oxipng@9.1.2 - pre-commit-hooks@4.6.0 - prettier@3.3.3 - shellcheck@0.10.0 - shfmt@3.6.0 - - sort-package-json@2.10.0 + - sort-package-json@2.10.1 - terraform@1.1.0 - terrascan@1.19.1 - - tflint@0.52.0 - - trufflehog@3.80.1 + - tflint@0.53.0 + - trufflehog@3.81.9 - yamllint@1.35.1 actions: disabled: diff --git a/DEPLOY_FROM_SCRATCH.md b/DEPLOY_FROM_SCRATCH.md new file mode 100644 index 0000000..8f30c7f --- /dev/null +++ b/DEPLOY_FROM_SCRATCH.md @@ -0,0 +1,162 @@ +# Deployment from Scratch + +How to deploy the entire governance watchdog infrastructure from scratch. + +- [Infra Deployment via Terraform](#infra-deployment-via-terraform) + - [Terraform State Management](#terraform-state-management) + - [Google Cloud Permission Requirements](#google-cloud-permission-requirements) + - [Using Service Account Impersonation (recommended)](#using-service-account-impersonation-recommended) + - [Using Your Own Gcloud User Account (not recommended)](#using-your-own-gcloud-user-account-not-recommended) + - [Deployment](#deployment) +- [Debugging Problems](#debugging-problems) + - [View Logs](#view-logs) +- [Teardown](#teardown) + +## Infra Deployment via Terraform + +### Terraform State Management + +- The Terraform State for this project lives in our shared Terraform Seed Project with the ID `mento-terraform-seed-ffac` +- Deploying the project for the first time should automatically create a subfolder in the [google storage bucket used for terraform state management in the seed project](https://console.cloud.google.com/storage/browser/mento-terraform-tfstate-6ed6;tab=objects?forceOnBucketsSortingFiltering=true&project=mento-terraform-seed-ffac&prefix=&forceOnObjectsSortingFiltering=false) + +### Google Cloud Permission Requirements + +#### Using Service Account Impersonation (recommended) + +The project is preconfigured to impersonate our shared terraform service account (see `./infra/versions.tf`). +The only permission you will need on your own gcloud user account is `roles/iam.serviceAccountTokenCreator` to allow you to impersonate our shared terraform service account. + +#### Using Your Own Gcloud User Account (not recommended) + +If for whatever reason service account impersonation doesn't work, you'll need at least the following permissions on your personal gcloud account to deploy this project with terraform: + +- `roles/resourcemanager.folderViewer` on the folder that you want to create the project in +- `roles/resourcemanager.organizationViewer` on the organization +- `roles/resourcemanager.projectCreator` on the organization +- `roles/billing.user` on the organization +- `roles/storage.admin` to allow creation of new storage buckets + +### Deployment + + + +1. Run `./bin/set-up-terraform.sh` to check required permissions and provision all required terraform providers and modules + +1. Create a `./infra/terraform.tfvars` file. This is like `.env` for Terraform: + + ```sh + touch ./infra/terraform.tfvars + # This file is `.gitignore`d to avoid accidentally leaking sensitive data + ``` + +1. Add Google Cloud Org ID and Billing Account to your local `terraform.tfvars` + + ```hcl + # Required for creating new GCP projects + # Get it via `gcloud organizations list` + org_id = "" + + # Required for creating new GCP projects + # Get it via `gcloud billing accounts list` (pick the GmbH account) + billing_account = "" + ``` + +1. [Create a Discord Webhook URL](https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks) for the channel you want to receive notifications in + +1. Add the Discord Webhook URL to your local `terraform.tfvars`: + + ```sh + # This will be stored in Google Secret Manager upon deployment via Terraform + echo "discord_webhook_url = \""" >> terraform.tfvars + ``` + +1. Create a Telegram group and invite a new bot into it + + - Open a new telegram chat with @BotFather + - Use the `/newbot` command to create a new bot + - Copy the API key printed out at the end of the prompt and store it in your `terraform.tfvars` + + ```hcl + telegram_bot_token = "" + ``` + + - Get the Chat ID by inviting @MissRose_bot to the group and then using the `/id` command + - Add the Chat ID to your `terraform.tfvars` + + ```hcl + telegram_chat_id = "" + ``` + + - Remove @MissRose_bot after you got the Chat ID + +1. Get (or generate if non-existing) a QuickNode API key to enable Terraform to provision QuickAlerts + + - Grab the API key from our QuickNode dashboard: + - Add it to `terraform.tfvars` + + ```hcl + quicknode_api_key = "" + ``` + +1. Get a VictorOps webhook URL by copying the Service API Endpoint URL from the [VictorOps Stackdriver Integration](https://portal.victorops.com/dash/mento-labs-gmbh#/advanced/stackdriver). The routing key can be founder under the [`Settings`](https://portal.victorops.com/dash/mento-labs-gmbh#/routekeys) tab + + ```hcl + # Required to send on-call alerts to VictorOps + victorops_webhook_url = "/" + ``` + +1. Generate an auth key to allow us to test the deployed function from our local machines + + - You can use your password manager to generate a long and secure (url-compatible) key + - Add it to `terraform.tfvars` + + ```hcl + x_auth_token = "" + ``` + +1. **Deploy the entire project via `terraform apply`** + + - You will see an overview of all resources to be created. Review them if you like and then type "Yes" to confirm. + - This command can take up to 10 minutes because it does a lot of work creating and configuring all defined Google Cloud Resources + - ❌ Given the complexity of setting up an entire Google Cloud Project incl. service accounts, permissions, etc., you might run + into deployment errors with some components. + + **Often a simple retry of `terraform apply` helps**. Sometimes a dependency of a resource has simply not finished creating when terraform already tried to deploy the next one, so waiting a few minutes for things to settle can help. + +1. Set your local `gcloud` project ID to our freshly created one and populate your local cache with frequently used project values: + + ```sh + npm run cache:clear + ``` + +1. Check that everything worked as expected + + ```sh + # 1. Call the deployed function via: + npm run test:prod + + # 2. Monitor the configured Discord channel for a message to appear + open https://discord.com/channels/966739027782955068/1262714272476037212 + + # 3. Monitor the configured Telegram channel for a message to appear + + # 4. Check the function logs via: + npm run logs # prints logs into your local terminal (with a few seconds of latency) + # OR + npm run logs:url # prints a URL to the cloud console logs in the browser + ``` + +## Debugging Problems + +### View Logs + +For most problems, you'll likely want to check the cloud function logs first. + +- `npm run logs` will print the latest 50 staging log entries into your local terminal for quick and easy access +- `npm run logs:url` will print the URL to the staging function logs in the Google Cloud Console for full access + +## Teardown + +1. Run `npm run destroy` to delete the entire production environment from google cloud + - You might run into permission issues here, especially around deleting the associated billing account resources + - I didn't have time to figure out the minimum set of permissions required to delete this project so the easiest would be to let an organization owner (i.e. Bogdan) run this with full permissions if you face any issues diff --git a/README.md b/README.md index 0775b97..d2067d5 100644 --- a/README.md +++ b/README.md @@ -2,26 +2,20 @@ -A monorepo for our governance watchdog, a system that monitors Mento Governance events on-chain and sends notifications about them to Discord and Telegram. Mento Devs can view the [full project spec in our Notion.](https://www.notion.so/mentolabs/Governance-Watchdog-d168a8110a53430a90e2f5ab65f103f5?pvs=4) +A system that monitors Mento Governance events on-chain and sends notifications about them to Discord and Telegram. Mento Devs can view the [full project spec in our Notion.](https://www.notion.so/mentolabs/Governance-Watchdog-d168a8110a53430a90e2f5ab65f103f5?pvs=4) -![Architecture Diagram](arch-diagram.png) - -- [Requirements for local development](#requirements-for-local-development) -- [Local Infra Setup (when project is deployed already)](#local-infra-setup-when-project-is-deployed-already) +- [Local Setup](#local-setup) - [Running and testing the Cloud Function locally](#running-and-testing-the-cloud-function-locally) - [Testing the Deployed Cloud Function](#testing-the-deployed-cloud-function) - [Updating the Cloud Function](#updating-the-cloud-function) -- [Infra Deployment via Terraform](#infra-deployment-via-terraform) - - [Google Cloud Permission Requirements](#google-cloud-permission-requirements) - - [Deployment from scratch](#deployment-from-scratch) - - [Migrate Terraform State to Google Cloud](#migrate-terraform-state-to-google-cloud) - [Debugging Problems](#debugging-problems) - [View Logs](#view-logs) -- [Teardown](#teardown) -## Requirements for local development +![Architecture Diagram](arch-diagram.png) + +## Local Setup 1. Install the `gcloud` CLI @@ -32,7 +26,7 @@ A monorepo for our governance watchdog, a system that monitors Mento Governance # For other systems, see https://cloud.google.com/sdk/docs/install ``` -1. Install trunk (one linter to rule them all) +1. Install `trunk` — one linter to rule them all ```sh # For macOS @@ -43,152 +37,117 @@ A monorepo for our governance watchdog, a system that monitors Mento Governance Optionally, you can also install the [Trunk VS Code Extension](https://marketplace.visualstudio.com/items?itemName=Trunk.io) -1. Install Terraform +1. Install `jq` — used in a few shell scripts ```sh - # On macOS - brew tap hashicorp/tap - brew install hashicorp/tap/terraform - - # For other systems, see https://developer.hashicorp.com/terraform/install - ``` - -1. Install `jq` (used in a few shell scripts) - - ```sh - # On macOS + # For macOS brew install jq # For other systems, see https://jqlang.github.io/jq/ ``` -1. Authenticate with Google Cloud default credentials in your local shell +1. Install `terraform` — to deploy and manage the infra for this project ```sh - gcloud auth application-default login - ``` - - The key differences between `gcloud auth login` and `gcloud auth application-default login` are: - - 1. `gcloud auth login` authenticates you for general gcloud CLI use and interactive scenarios. - 2. `gcloud auth application-default login` sets up Application Default Credentials (ADC) for use by local development environments and applications, such as running our function locally via `npm start` - 3. ADC credentials are specifically intended for [Google Auth Library](https://www.npmjs.com/package/google-auth-library) access, while regular login is for broader gcloud command usage. - -1. To fetch the secrets used by this function from Secret Manager, you'll need the `Secret Manager Secret Accessor` IAM role assigned to your Google Cloud Account - -1. A Discord channel with an active webhook to send notifications to - -1. A Telegram group to send notifications to - -1. A Telegram bot must be in the group to receive the notifications. + # For macOS + brew tap hashicorp/tap + brew install hashicorp/tap/terraform -## Local Infra Setup (when project is deployed already) + # For other systems, see https://developer.hashicorp.com/terraform/install + ``` -1. Set your local `gcloud` project to the watchdog project: +1. Run terraform setup script ```sh - ./set-project-id.sh + # Checks required permissions, provisions terraform providers and modules, syncs terraform state + ./bin/set-up-terraform.sh ``` -1. Inside the `./infra` folder, run `terraform init` to install all required terraform providers and sync the terraform state from the project's google cloud storage bucket: +1. Set your local `gcloud` project and cache project values used in shell scripts: ```sh - cd infra - terraform init + # Will set the correct gcloud project in your terminal and populate a local cache with values frequently used in shell scripts + npm run cache:clear ``` -1. While inside the `infra` folder, create `terraform.tfvars` file. This is like `.env` for Terraform: +1. Create a `./infra/terraform.tfvars` file. This is like `.env` for Terraform: ```sh - touch terraform.tfvars + touch ./infra/terraform.tfvars # This file is `.gitignore`d to avoid accidentally leaking sensitive data ``` -1. Add the following values to your `terraform.tfvars`, you can look up all values in the Google Cloud console (or ask another dev to share his local `terraform.tfvars` with you) +1. Add the following values to your `terraform.tfvars`. You can either follow the instructions in the comments to look up each value, or you can ask another dev to share his local `terraform.tfvars` with you - ```sh + ```hcl + # Required for creating new GCP projects # Get it via `gcloud organizations list` org_id = "" + # Required for creating new GCP projects # Get it via `gcloud billing accounts list` (pick the GmbH account) billing_account = "" - # Get it via `gcloud organizations get-iam-policy --format=json | jq -r '.bindings[] | select(.role | startswith("roles/resourcemanager.organizationAdmin")) | .members[] | select(startswith("group:")) | sub("^group:"; "")'` - group_org_admins = "" - - # Get it via `gcloud organizations get-iam-policy --format=json | jq -r '.bindings[] | select(.role | startswith("roles/billing.admin")) | .members[] | select(startswith("group:")) | sub("^group:"; "")'` - group_billing_admins = "" - ``` - -1. Add the Discord Webhook URL from Google Cloud Secret Manager to your local `terraform.tfvars`: - - ```sh + # The Discord Channel where we post notifications to + # Get it via `gcloud secrets versions access latest --secret discord-webhook-url` # You need the "Secret Manager Secret Accessor" IAM role for this command to succeed - echo "\ndiscord_webhook_url = \"$(gcloud secrets versions access latest --secret discord-webhook-url)\"" >> terraform.tfvars - ``` - -1. Add the Telegram Bot Token and Chat ID to your local `terraform.tfvars` - - ```sh - # Get the chat ID from cloud function's terraform state - echo "\ntelegram_chat_id = \"$(terraform state show "google_cloudfunctions2_function.watchdog_notifications" | grep TELEGRAM_CHAT_ID | awk -F '= ' '{print $2}' | tr -d '"')\"" >> terraform.tfvars + discord_webhook_url = "" - # Get the bot token from secret manager (you need the "Secret Manager Secret Accessor" IAM role for this command to succeed) - echo "\ntelegram_bot_token = \"$(gcloud secrets versions access latest --secret telegram-bot-token)\"" >> terraform.tfvars - ``` + # The Telegram Chat where we post notifications to + # Get it via `terraform state show "google_cloudfunctions2_function.watchdog_notifications" | grep TELEGRAM_CHAT_ID | awk -F '= ' '{print $2}' | tr -d '"'` + telegram_chat_id = "" -1. Add the secret auth token from Google Cloud Secret Manager to your local `terraform.tfvars`: + # The Telegram bot used to receive and post notifications + # Get it via `gcloud secrets versions access latest --secret telegram-bot-token` + telegram_bot_token = "" - ```sh - # You need the "Secret Manager Secret Accessor" IAM role for this command to succeed - echo "\nx_auth_token = \"$(gcloud secrets versions access latest --secret x-auth-token)\"" >> terraform.tfvars - ``` + # An auth token we use to be able to test deployed functions from our local machines + # Get it via `gcloud secrets versions access latest --secret x-auth-token` + x_auth_token = "" -1. [Get our QuickNode API key from the QuickNode dashboard](https://dashboard.quicknode.com/api-keys) and add it to your local `terraform.tfvars`: + # Required for Terraform to be able to create & destroy QuickAlerts + # Get it from the [QuickNode dashboard](https://dashboard.quicknode.com/api-keys) + quicknode_api_key = "" - ```sh - # ./infra/terraform.tfvars - quicknode_api_key = "" + # Required to send on-call alerts to VictorOps + # Get it from [our VictorOps](https://portal.victorops.com/dash/mento-labs-gmbh#/advanced/stackdriver) and clicking `Integrations` > `Stackdriver` and copying the URL. The routing key can be founder under the [`Settings`](https://portal.victorops.com/dash/mento-labs-gmbh#/routekeys) tab + victorops_webhook_url = "/" ``` - This is necessary for Terraform to be able to create & destroy QuickAlerts as part of `terraform apply` +1. Auto-generate a local `.env` file by running `npm run generate:env` — we'll need this to run the cloud function locally -1. Get the VictorOps Webhook URL to your local `terraform.tfvars`. You can get it by going to [our VictorOps](https://portal.victorops.com/dash/mento-labs-gmbh#/advanced/stackdriver) and clicking `Integrations` > `Stackdriver` and copying the URL. The routing key can be founder under the [`Settings`](https://portal.victorops.com/dash/mento-labs-gmbh#/routekeys) tab: +1. Verify that everything works ```sh - # ./infra/terraform.tfvars - victorops_webhook_url = "/" - ``` + # See if you can fetch logs of the watchdog cloud function + npm run logs -1. Auto-generate a local `.env` file by running `npm run generate:env` + # See if you can manually trigger the deployed watchdog function with some dummy data + # Make sure to delete the fake posts from the Telegram & Discord channels to not spam channel members too much + npm run test:prod + ``` ## Running and testing the Cloud Function locally -- Make sure you generated a local `.env` file via `npm run generate:env` earlier -- `npm install` (couldn't use `pnpm` because Google Cloud Build failed trying to install pnpm at the time of writing) -- `npm start` to start a local cloud function +- `npm install` +- `npm dev` to start a local cloud function with hot-reload via nodemon - `npm test` to call the local cloud function with a mocked payload, this will send a real Discord message into the channel belonging to the configured Discord Webhook: - ```sh - curl -H "Content-Type: application/json" -d @src/proposal-created.fixture.json localhost:8080 - ``` - ## Testing the Deployed Cloud Function You can test the deployed cloud function manually by using the `proposal-created.fixture.json` which contains a similar payload to what a QuickAlert would send to the cloud function: ```sh -./test-deployed-function.sh -# or `npm run test:prod` if you prefer npm to call this script +npm run test:prod ``` ## Updating the Cloud Function You have two options, using `terraform` or the `gcloud` cli. Both are perfectly fine to use. -1. Via `terraform` by running `npm run deploy:via:tf` +1. Via `terraform` by running `npm run deploy` - How? The npm task will: - - Call `terraform apply` which re-deploys the function with the latest code from your local machine + - Call `terraform apply` which re-deploys the function with the latest code from your local machine (and all other infrastructure that may have changed since the last terraform deploy) - Pros - Keeps the terraform state clean - Same command for all changes, regardless of infra or cloud function code @@ -196,141 +155,20 @@ You have two options, using `terraform` or the `gcloud` cli. Both are perfectly - Less familiar way of deploying cloud functions (if you're used to `gcloud functions deploy`) - Less log output - Slightly slower because `terraform apply` will always fetch the current state from the cloud storage bucket before deploying -2. Via `gcloud` by running `npm run deploy:via:gcloud` +2. Via `gcloud` by running `npm run deploy:function` - How? The npm task will: - Look up the service account used by the cloud function + - Impersonate our shared Terraform Service Account to avoid individual permission issues - Call `gcloud functions deploy` with the correct parameters - Pros - Familiar way of deploying cloud functions - More log output making deployment failures slightly faster to debug - Slightly faster because we're skipping the terraform state lookup - Cons - - Will lead to inconsistent terraform state (because terraform is tracking the function source code and its version) + - Will lead to slightly inconsistent terraform state (because terraform is tracking the function source code and its version) - Different commands to remember when updating infra components vs cloud function source code - Will only work for updating a pre-existing cloud function's code, will fail for a first-time deploy -## Infra Deployment via Terraform - -### Google Cloud Permission Requirements - -In order to create this project from scratch using the [terraform-google-bootstrap](https://github.com/terraform-google-modules/terraform-google-bootstrap) module, you will need the following permissions in our Google Cloud Organization: - -- `roles/resourcemanager.organizationAdmin` on the top-level GCP Organization -- `roles/orgpolicy.policyAdmin` on the top-level GCP Organization -- `roles/billing.admin` on the billing account connected to the project - -### Deployment from scratch - -1. Outcomment the `backend` section in `main.tf` (because this bucket doesn't exist yet, it will be created by the first `terraform apply` run) - - ```hcl - # backend "gcs" { - # bucket = "governance-watchdog-terraform-state-" - # } - ``` - -1. Run `terraform init` to install the required providers and init a temporary local backend in a `terraform.tfstate` file - - - -1. [Create a Discord Webhook URL](https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks) for the channel you want to receive notifications in - -1. Add the Discord Webhook URL to your local `terraform.tfvars`: - - ```sh - # This will be stored in Google Secret Manager upon deployment via Terraform - echo "discord_webhook_url = \""" >> terraform.tfvars - ``` - -1. Create a Telegram group and invite a new bot into it - - - Open a new telegram chat with @BotFather - - Use the `/newbot` command to create a new bot - - Copy the API key printed out at the end of the prompt and store it in your `terraform.tfvars` - - ```hcl - telegram_bot_token = "" - ``` - - - Get the Chat ID by inviting @MissRose_bot to the group and then using the `/id` command - - Add the Chat ID to your `terraform.tfvars` - - ```hcl - telegram_chat_id = "" - ``` - - - Remove @MissRose_bot after you got the Chat ID - -1. **Deploy the entire project via `terraform apply`** - - - You will see an overview of all resources to be created. Review them if you like and then type "Yes" to confirm. - - This command can take up to 10 minutes because it does a lot of work creating and configuring all defined Google Cloud Resources - - ❌ Given the complexity of setting up an entire Google Cloud Project incl. service accounts, permissions, etc., you might run - into deployment errors with some components. - - **Often a simple retry of `terraform apply` helps**. Sometimes a dependency of a resource has simply not finished creating when terraform already tried to deploy the next one, so waiting a few minutes for things to settle can help. - -1. Set your local `gcloud` project ID to our freshly created one: - - ```sh - ./set-project-id.sh - ``` - -1. Check that everything worked as expected - - ```sh - # 1. Call the deployed function via: - npm run test:prod # or call the script directly via ./test-deployed-function.sh - - # 2. Monitor the configured Discord channel for a message to appear - open https://discord.com/channels/966739027782955068/1262714272476037212 - - # 3. Check the function logs via: - npm run logs # prints logs into your local terminal (with a few seconds of latency) - # OR - npm run logs:url # prints a URL to the cloud console logs in the browser - ``` - -### Migrate Terraform State to Google Cloud - -For all team members to be able to manage the Google Cloud infrastructure, you need to migrate the terraform state from your local backend (`terraform.tfstate`) to a remote backend in a Google Cloud Storage Bucket: - -1. Copy the name of the created terraform state bucket to your clipboard: - - ```sh - terraform state show "module.bootstrap.google_storage_bucket.org_terraform_state" | grep name | awk -F '"' '{print $2}' | pbcopy - ``` - -1. Uncomment the original `backend` section in `main.tf` and replace the bucket name with the new one you just copied - - ```hcl - backend "gcs" { - bucket = "governance-watchdog-terraform-state-" - } - ``` - -1. Complete the state migration: - - ```sh - terraform init -migrate-state - - # This command will ask you _"Do you want to copy existing state to the new backend?"_ — Make sure to type **YES** here to not re-create everything from scratch again - ``` - -1. Commit & push your changes - - ```sh - git commit -m "build: updated terraform remote backend to new google cloud storage bucket" - git push - ``` - -1. Delete your local backend files, you don't need them anymore because our state now lives in the cloud and can be shared amongst team members: - - ```sh - rm terraform.tfstate - rm terraform.tfstate.backup - ``` - ## Debugging Problems ### View Logs @@ -339,23 +177,3 @@ For most problems, you'll likely want to check the cloud function logs first. - `npm run logs` will print the latest 50 log entries into your local terminal for quick and easy access - `npm run logs:url` will print the URL to the function logs in the Google Cloud Console for full access - -## Teardown - -Before destroying the project, you'll need to migrate the terraform state from the cloud bucket backend onto your local machine. -Because `terraform destroy` will also destroy the bucket that the terraform state is stored in so the moment it gets destroyed, -the terraform state will be gone and the destroy command will fail. - -1. Outcomment the `backend` section in `main.tf` again - - ```hcl - # backend "gcs" { - # bucket = "governance-watchdog-terraform-state-" - # } - ``` - -1. Run `terraform init -migrate-state` to move the state into a local `terraform.tfstate` file - -1. Now run `terraform destroy` to delete all cloud resources associated with this project - - You might run into permission issues here, especially around deleting the associated billing account resources - - I didn't have time to figure out the minimum set of permissions required to delete this project so the easiest would be to let an organization owner (i.e. Bogdan) run this with full permissions diff --git a/bin/check-gcloud-login.sh b/bin/check-gcloud-login.sh new file mode 100755 index 0000000..e88015b --- /dev/null +++ b/bin/check-gcloud-login.sh @@ -0,0 +1,32 @@ +#!/bin/bash +set -e # Fail on any error +set -o pipefail # Ensure piped commands propagate exit codes properly +set -u # Treat unset variables as an error when substituting + +# Checks for an active Google Cloud login and application-default credentials. +# If no active account or valid credentials are found, it prompts the user to log in. +check_gcloud_login() { + printf "\n" + echo "🌀 Checking gcloud login..." + # Check if there's an active account + if ! gcloud auth list --filter=status:ACTIVE --format="value(account)" | grep -q .; then + echo "No active Google Cloud account found. Initiating login..." + gcloud auth login + echo "✅ Successfully logged in to gcloud" + else + echo "✅ Already logged in to Google Cloud." + fi + printf "\n" + + echo "🌀 Checking gcloud application-default credentials..." + if ! gcloud auth application-default print-access-token &>/dev/null; then + echo "No valid application-default credentials found. Initiating login..." + gcloud auth application-default login + echo "✅ Successfully logged in to gcloud" + else + echo "✅ Already logged in with valid application-default credentials." + fi + printf "\n" +} + +check_gcloud_login diff --git a/bin/deploy-via-gcloud.sh b/bin/deploy-via-gcloud.sh new file mode 100755 index 0000000..3d3fce4 --- /dev/null +++ b/bin/deploy-via-gcloud.sh @@ -0,0 +1,32 @@ +#!/bin/bash +set -e # Fail on any error +set -o pipefail # Ensure piped commands propagate exit codes properly +set -u # Treat unset variables as an error when substituting + +# Deploys the Cloud Function using gcloud. +# Requires an environment arg (e.g., staging, production). +deploy_via_gcloud() { + printf "\n" + + # Load the project variables + script_dir=$(dirname "$0") + source "${script_dir}/get-project-vars.sh" + + # Deploy the Google Cloud Function + echo "Deploying to Google Cloud Functions..." + gcloud functions deploy "${function_name}" \ + --allow-unauthenticated \ + --entry-point "${function_entry_point}" \ + --gen2 \ + --impersonate-service-account "${terraform_service_account}" \ + --project "${project_id}" \ + --region "${region}" \ + --runtime nodejs20 \ + --service-account "${project_service_account}" \ + --source . \ + --trigger-http + + echo "✅ All Done!" +} + +deploy_via_gcloud diff --git a/bin/get-logs-url.sh b/bin/get-logs-url.sh new file mode 100755 index 0000000..5d5852a --- /dev/null +++ b/bin/get-logs-url.sh @@ -0,0 +1,16 @@ +#!/bin/bash +set -e # Fail on any error +set -o pipefail # Ensure piped commands propagate exit codes properly +set -u # Treat unset variables as an error when substituting + +# Prints the log explorer URL for the Cloud Function and displays it in the terminal. +get_function_logs_url() { + # Load the project variables + script_dir=$(dirname "$0") + source "${script_dir}/get-project-vars.sh" + + logs_url="https://console.cloud.google.com/functions/details/${region}/${function_name}?project=${project_id}&tab=logs " + printf '\n\033[1m%s\033[0m\n' "${logs_url}" +} + +get_function_logs_url "$@" diff --git a/bin/get-logs.sh b/bin/get-logs.sh new file mode 100755 index 0000000..05b4b3e --- /dev/null +++ b/bin/get-logs.sh @@ -0,0 +1,31 @@ +#!/bin/bash +set -e # Fail on any error +set -o pipefail # Ensure piped commands propagate exit codes properly +set -u # Treat unset variables as an error when substituting + +# Fetches the latest logs for the Cloud Function and displays them in the terminal. +get_function_logs() { + # Load the project variables + script_dir=$(dirname "$0") + source "${script_dir}/get-project-vars.sh" + + printf "\n" + echo "Fetching logs for function ${function_name} in region ${region}..." + printf "\n" + + # Fetch raw logs + raw_logs=$(gcloud functions logs read "${function_name}" \ + --region "${region}" \ + --format json \ + --limit 50 \ + --sort-by TIME_UTC) + + # Format logs + echo "${raw_logs}" | jq -r '.[] | if .level == "E" then + "\u001b[31m[\(.level)]\u001b[0m \u001b[33m\(.time_utc)\u001b[0m: \(.log)" +else + "[\(.level)] \u001b[33m\(.time_utc)\u001b[0m: \(.log)" +end' +} + +get_function_logs "$@" diff --git a/bin/get-project-vars.sh b/bin/get-project-vars.sh new file mode 100755 index 0000000..87eae25 --- /dev/null +++ b/bin/get-project-vars.sh @@ -0,0 +1,153 @@ +#!/bin/bash +set -e # Fail on any error +set -o pipefail # Ensure piped commands propagate exit codes properly +set -u # Treat unset variables as an error when substituting + +set_project_id() { + printf "Looking up project name in variables.tf..." + project_name=$(awk '/variable "project_name"/{f=1} f==1&&/default/{print $3; exit}' ./infra/variables.tf | tr -d '",') + printf ' \033[1m%s\033[0m\n' "${project_name}" + + printf "Fetching the project ID..." + project_id=$(gcloud projects list --filter="name:${project_name}" --format="value(projectId)") + printf ' \033[1m%s\033[0m\n' "${project_id}" + + # Set your local default project + printf "Setting your default project to \033[1m%s\033[0m...\n" "${project_id}" + { + output=$(gcloud config set project "${project_id}" 2>&1 >/dev/null) + status=$? + } + if [[ ${status} -ne 0 ]]; then + echo "Error: ${output}" + return "${status}" + fi + + # Set the quota project to the governance-watchdog project, some gcloud commands require this to be set + printf "Setting the quota project to \033[1m%s\033[0m...\n" "${project_id}" + { + output=$(gcloud auth application-default set-quota-project "${project_id}" 2>&1 >/dev/null) + status=$? + } + if [[ ${status} -ne 0 ]]; then + echo "Error: ${output}" + return "${status}" + fi + + # Update the project ID in your .env file so your cloud function points to the correct project when running locally + printf "Updating the project ID in your .env file..." + # Check if .env file exists + if [[ ! -f .env ]]; then + # If .env doesn't exist, create it with the initial value + echo "GCP_PROJECT_ID=${project_id}" >.env + else + # If .env exists, perform the sed replacement + sed -i '' "s/^GCP_PROJECT_ID=.*/GCP_PROJECT_ID=${project_id}/" .env + fi + printf "✅" +} + +cache_file=".project_vars_cache" + +# Function to load values from cache +load_cache() { + if [[ -f ${cache_file} ]]; then + # shellcheck disable=SC1090 + source "${cache_file}" + return 0 + else + return 1 + fi +} + +# Function to write values to cache +write_cache() { + printf "\nCaching project values in" + printf ' \033[1m%s\033[0m...' "${cache_file}" + { + echo "project_id=${project_id}" + echo "project_name=${project_name}" + echo "region=${region}" + echo "project_service_account=${project_service_account}" + echo "terraform_service_account=${terraform_service_account}" + echo "function_name=${function_name}" + echo "function_entry_point=${function_entry_point}" + } >>"${cache_file}" +} + +# Function to load & cache values +load_and_cache_values() { + printf "Loading project values...\n\n" + + project_name=$(awk '/variable "project_name"/{f=1} f==1&&/default/{print $3; exit}' ./infra/variables.tf | tr -d '",') + region=$(awk '/variable "region"/{f=1} f==1&&/default/{print $3; exit}' ./infra/variables.tf | tr -d '",') + project_service_account=$(terraform -chdir=infra state show "module.governance_watchdog.module.project-factory.google_service_account.default_service_account[0]" | grep email | awk '{print $3}' | tr -d '"') + terraform_service_account=$(awk '/variable "terraform_service_account"/{f=1} f==1&&/default/{print $3; exit}' ./infra/variables.tf | tr -d '",') + function_name=$(awk '/variable "function_name"/{f=1} f==1&&/default/{print $3; exit}' ./infra/variables.tf | tr -d '",') + function_entry_point=$(awk '/variable "function_entry_point"/{f=1} f==1&&/default/{print $3; exit}' ./infra/variables.tf | tr -d '",') + + print_values + write_cache + + printf "✅\n\n" +} + +print_values() { + printf " - Project ID: \033[1m%s\033[0m\n" "${project_id}" + printf " - Project Name: \033[1m%s\033[0m\n" "${project_name}" + printf " - Region: \033[1m%s\033[0m\n" "${region}" + printf " - Project Service Account: \033[1m%s\033[0m\n" "${project_service_account}" + printf " - Terraform Service Account: \033[1m%s\033[0m\n" "${terraform_service_account}" + printf " - Function Name: \033[1m%s\033[0m\n" "${function_name}" + printf " - Function Entry Point: \033[1m%s\033[0m\n" "${function_entry_point}" +} + +# Function to invalidate cache +invalidate_cache() { + printf "Clearing local cache file %s..." "${cache_file}" + rm -f "${cache_file}" + printf " ✅\n" + + printf "Loading current local gcloud project ID:" + current_local_project_id=$(gcloud config get project) + printf ' \033[1m%s\033[0m\n' "${current_local_project_id}" + + printf "Comparing with project ID from terraform state:" + current_tf_state_project_id=$(terraform -chdir=infra state show module.governance_watchdog.module.project-factory.google_project.main 2>/dev/null | grep project_id | awk '{print $3}' | tr -d '"' || echo "Not found") + printf ' \033[1m%s\033[0m\n' "${current_tf_state_project_id}" + + if [[ ${current_local_project_id} != "${current_tf_state_project_id}" ]]; then + printf '️\n🚨 Your local gcloud is set to the wrong project: \033[1m%s\033[0m 🚨\n' "${current_local_project_id}" + printf "\nTrying to set the correct project ID...\n\n" + set_project_id + printf "\n\n" + else + project_id="${current_local_project_id}" + fi + + load_and_cache_values +} + +# Main script logic +main() { + if [[ ${1-} == "--invalidate-cache" ]]; then + invalidate_cache + return 0 + fi + + set +e + load_cache + cache_loaded=$? + set -e + + if [[ ${cache_loaded} -eq 0 ]]; then + printf "Using cached values from %s:\n" "${cache_file}" + print_values + else + printf "⚠️ No cache found. Setting project ID and fetching values...\n\n" + set_project_id + load_and_cache_values + fi +} + +main "$@" diff --git a/bin/set-up-terraform.sh b/bin/set-up-terraform.sh new file mode 100755 index 0000000..d172e1a --- /dev/null +++ b/bin/set-up-terraform.sh @@ -0,0 +1,73 @@ +#!/bin/bash +set -e # Fail on any error +set -o pipefail # Ensure piped commands propagate exit codes properly +set -u # Treat unset variables as an error when substituting + +# Checks if the user has the "Service Account Token Creator" role in the Terraform Seed Project +# This role is necessary to access the Terraform state bucket in Google Cloud +check_gcloud_iam_permissions() { + printf "Looking up Terraform Seed Project ID..." + terraform_seed_project_id=$(awk '/variable "terraform_seed_project_id"/{f=1} f==1&&/default/{print $3; exit}' ./infra/variables.tf | tr -d '",') + if [[ -z ${terraform_seed_project_id} ]]; then + echo "❌ Error: Variable \$terraform_seed_project_id is empty. Please ensure it's set in ./infra/variables.tf" >&2 + exit 1 + fi + printf ' \033[1m%s\033[0m\n' "${terraform_seed_project_id}" + + printf "Looking up Terraform Service Account email..." + terraform_service_account=$(awk '/variable "terraform_service_account"/{f=1} f==1&&/default/{print $3; exit}' ./infra/variables.tf | tr -d '",') + if [[ -z ${terraform_service_account} ]]; then + echo "❌ Error: Variable \$terraform_seed_project_id is empty. Please ensure it's set in ./infra/variables.tf" >&2 + exit 1 + fi + printf ' \033[1m%s\033[0m\n\n' "${terraform_service_account}" + + # Check if the user has access to the Terraform state via the Service Account Token Creator role + echo "🌀 Checking if you have the 'Service Account Token Creator' role in the terraform seed project..." + user_account_to_check="$(gcloud config get-value account)" + local check_result + check_result=$(gcloud projects get-iam-policy "${terraform_seed_project_id}" --format=json | + jq -r \ + --arg MEMBER "user:${user_account_to_check}" \ + --arg SA "${terraform_service_account}" \ + '.bindings[] | select(.members[] | contains($MEMBER)) | select(.role == "roles/iam.serviceAccountTokenCreator" or .role == "roles/iam.serviceAccountUser") | .role') + + if echo "${check_result}" | grep -q "roles/iam.serviceAccountTokenCreator"; then + echo "✅ Permission check passed: ${user_account_to_check} has the Service Account Token Creator role in the terraform seed project." + printf "\n" + else + # If not, try to give the user the Service Account Token Creator role + echo "⚠️ Permission check failed: ${user_account_to_check} does not have the Service Account Token Creator role in the terraform seed project." + printf "\n" + echo "Trying to give permission "Service Account Token Creator" role to ${user_account_to_check}" + if gcloud projects add-iam-policy-binding "${terraform_seed_project_id}" \ + --member="user:${user_account_to_check}" \ + --role="roles/iam.serviceAccountTokenCreator"; then + echo "✅ Successfully added the Service Account Token Creator role to ${user_account_to_check}" + else + echo "❌ Error: Failed to add the Service Account Token Creator role to ${user_account_to_check}" + echo "You may have to ask a project owner of '${terraform_seed_project_id}' to add the role manually via the following command." + echo "gcloud projects add-iam-policy-binding \"${terraform_seed_project_id}\" --member=\"user:${user_account_to_check}\" --role=\"roles/iam.serviceAccountTokenCreator\"" + exit 1 + fi + printf "\n" + fi +} + +set_up_terraform() { + script_dir=$(dirname "$0") + source "${script_dir}/check-gcloud-login.sh" + + if ! command -v terraform &>/dev/null; then + echo "❌ Error: Terraform is not installed or not in your PATH. Please install terraform: https://developer.hashicorp.com/terraform/install" >&2 + exit 1 + fi + + check_gcloud_iam_permissions + + cd infra + terraform init + printf "\n" +} + +set_up_terraform diff --git a/test-deployed-function.sh b/bin/test-deployed-function.sh similarity index 100% rename from test-deployed-function.sh rename to bin/test-deployed-function.sh diff --git a/deploy-via-gcloud.sh b/deploy-via-gcloud.sh deleted file mode 100755 index 8208f8e..0000000 --- a/deploy-via-gcloud.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/bin/bash -set -e # Fail on any error -set -o pipefail # Ensure piped commands propagate exit codes properly -set -u # Treat unset variables as an error when substituting - -# Load the project variables -source ./set-project-vars.sh - -printf "Looking up entry point..." -entry_point=$(gcloud functions describe "${function_name}" --region="${region}" --format json | jq .buildConfig.entryPoint) -printf ' \033[1m%s\033[0m\n' "${entry_point}" - -printf "Looking up service account for function..." -service_account_email=$(gcloud functions describe "${function_name}" --region="${region}" --format="value(serviceConfig.serviceAccountEmail)") -printf ' \033[1m%s\033[0m\n' "${service_account_email}" - -echo "Deploying to Google Cloud Functions..." -gcloud functions deploy "${function_name}" \ - --allow-unauthenticated \ - --entry-point "${entry_point}" \ - --gen2 \ - --project "${project_id}" \ - --region "${region}" \ - --runtime nodejs20 \ - --service-account "${service_account_email}" \ - --source . \ - --trigger-http - -echo "✅ All Done!" diff --git a/get-logs-url.sh b/get-logs-url.sh deleted file mode 100755 index f034a3f..0000000 --- a/get-logs-url.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash -set -e # Fail on any error -set -o pipefail # Ensure piped commands propagate exit codes properly -set -u # Treat unset variables as an error when substituting - -# Load the project variables -source ./set-project-vars.sh - -logs_url="https://console.cloud.google.com/functions/details/${region}/${function_name}?project=${project_id}&tab=logs " -printf '\n\033[1m%s\033[0m\n' "${logs_url}" diff --git a/get-logs.sh b/get-logs.sh deleted file mode 100755 index 7958ad7..0000000 --- a/get-logs.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/bash -set -e # Fail on any error -set -o pipefail # Ensure piped commands propagate exit codes properly -set -u # Treat unset variables as an error when substituting - -# Load the project variables -source ./set-project-vars.sh - -# Fetch raw logs -raw_logs=$(gcloud functions logs read "${function_name}" \ - --region "${region}" \ - --format json \ - --limit 50 \ - --sort-by TIME_UTC) - -# Format logs -echo "${raw_logs}" | jq -r '.[] | if .level == "E" then - "\u001b[31m[\(.level)]\u001b[0m \u001b[33m\(.time_utc)\u001b[0m: \(.log)" -else - "[\(.level)] \u001b[33m\(.time_utc)\u001b[0m: \(.log)" -end' diff --git a/infra/cloud_function.tf b/infra/cloud_function.tf index 95181b3..a23137d 100644 --- a/infra/cloud_function.tf +++ b/infra/cloud_function.tf @@ -1,5 +1,5 @@ resource "google_cloudfunctions2_function" "watchdog_notifications" { - project = module.bootstrap.seed_project_id + project = module.governance_watchdog.project_id location = var.region name = var.function_name description = "A cloud function that receives blockchain event data from QuickAlerts and sends notifications to a Discord channel" @@ -7,7 +7,7 @@ resource "google_cloudfunctions2_function" "watchdog_notifications" { build_config { runtime = "nodejs20" entry_point = var.function_entry_point - service_account = "projects/${module.bootstrap.seed_project_id}/serviceAccounts/${module.bootstrap.terraform_sa_email}" + service_account = module.governance_watchdog.service_account_name source { storage_source { @@ -19,7 +19,7 @@ resource "google_cloudfunctions2_function" "watchdog_notifications" { service_config { available_memory = "512M" - service_account_email = module.bootstrap.terraform_sa_email + service_account_email = module.governance_watchdog.service_account_email timeout_seconds = 60 # 🔒 Security Note: Checkov recommends to only allow this function to be called from a cloud load balancer. @@ -30,7 +30,7 @@ resource "google_cloudfunctions2_function" "watchdog_notifications" { environment_variables = { # Necessary for the function to be able to find the secrets in Secret Manager - GCP_PROJECT_ID = module.bootstrap.seed_project_id + GCP_PROJECT_ID = module.governance_watchdog.project_id DISCORD_WEBHOOK_URL_SECRET_ID = google_secret_manager_secret.discord_webhook_url.secret_id TELEGRAM_BOT_TOKEN_SECRET_ID = google_secret_manager_secret.telegram_bot_token.secret_id TELEGRAM_CHAT_ID = var.telegram_chat_id @@ -45,8 +45,8 @@ resource "google_cloudfunctions2_function" "watchdog_notifications" { # Allows the QuickAlerts service (and everyone else...) to call the cloud function resource "google_cloud_run_v2_service_iam_member" "cloud_function_invoker" { - project = module.bootstrap.seed_project_id - location = var.region + project = module.governance_watchdog.project_id + location = google_cloudfunctions2_function.watchdog_notifications.location name = google_cloudfunctions2_function.watchdog_notifications.name role = "roles/run.invoker" # We could probably somehow whitelist the QuickAlerts URL or their IP range here instead of allowing everyone to call it, @@ -57,9 +57,9 @@ resource "google_cloud_run_v2_service_iam_member" "cloud_function_invoker" { # Allows the cloud function to access secrets (i.e. the Discord webhook URL) stored in Secret Manager resource "google_project_iam_member" "secret_accessor" { - project = module.bootstrap.seed_project_id + project = module.governance_watchdog.project_id role = "roles/secretmanager.secretAccessor" - member = "serviceAccount:${module.bootstrap.terraform_sa_email}" + member = "serviceAccount:${module.governance_watchdog.service_account_email}" } output "function_uri" { diff --git a/infra/cloudbuild.tf b/infra/cloudbuild.tf index f8583cd..55b4cda 100644 --- a/infra/cloudbuild.tf +++ b/infra/cloudbuild.tf @@ -1,13 +1,13 @@ # Allows cloud build to do builds and write build logs. resource "google_project_iam_member" "cloudbuild_builder" { - project = module.bootstrap.seed_project_id + project = module.governance_watchdog.project_id role = "roles/cloudbuild.builds.builder" - member = "serviceAccount:${module.bootstrap.terraform_sa_email}" + member = "serviceAccount:${module.governance_watchdog.service_account_email}" } # Allows cloud build to access the function source code in the storage bucket resource "google_storage_bucket_iam_member" "cloud_build_storage_access" { bucket = google_storage_bucket.watchdog_notifications_function.name role = "roles/storage.objectViewer" - member = "serviceAccount:${module.bootstrap.terraform_sa_email}" + member = "serviceAccount:${module.governance_watchdog.service_account_email}" } diff --git a/infra/local-dotenv-file.tf b/infra/local-dotenv-file.tf index a68213a..cf52ecf 100644 --- a/infra/local-dotenv-file.tf +++ b/infra/local-dotenv-file.tf @@ -1,7 +1,7 @@ resource "local_file" "env_file" { filename = "${path.module}/../.env" content = <<-EOT - GCP_PROJECT_ID=${module.bootstrap.seed_project_id} + GCP_PROJECT_ID=${module.governance_watchdog.project_id} DISCORD_WEBHOOK_URL_SECRET_ID=${var.discord_webhook_url_secret_id} TELEGRAM_BOT_TOKEN_SECRET_ID=${var.telegram_bot_token_secret_id} TELEGRAM_CHAT_ID=${var.telegram_chat_id} diff --git a/infra/main.tf b/infra/main.tf index 83cb35b..a192756 100644 --- a/infra/main.tf +++ b/infra/main.tf @@ -1,54 +1,23 @@ -terraform { - required_version = ">= 1.8" - - required_providers { - google = { - source = "hashicorp/google" - version = ">= 5.36.0" - } - archive = { - source = "hashicorp/archive" - version = ">= 2.4.2" - } - local = { - source = "hashicorp/local" - version = ">= 2.5.1" - } - quicknode = { - source = "jmtx1020/quicknode" - version = "0.0.2" - } - } - - backend "gcs" { - bucket = "governance-watchdog-terraform-state-0e83" - } -} - -module "bootstrap" { - source = "git::https://github.com/terraform-google-modules/terraform-google-bootstrap.git?ref=177e6be173eb8451155a133f7c6a591215130aab" # commit hash of v8.0.0 - org_id = var.org_id - # Can be at most 30 characters long, 4 of which are an auto-generated random suffix - project_id = var.project_name - default_region = var.region - billing_account = var.billing_account - group_org_admins = var.group_org_admins - group_billing_admins = var.group_billing_admins - state_bucket_name = "${var.project_name}-terraform-state" - force_destroy = true - +module "governance_watchdog" { activate_apis = [ - # Required by the bootstrap module https://github.com/terraform-google-modules/terraform-google-bootstrap?tab=readme-ov-file#apis - "cloudresourcemanager.googleapis.com", - "cloudbilling.googleapis.com", - "iam.googleapis.com", - "storage-api.googleapis.com", - "serviceusage.googleapis.com", - - # Required for our Cloud Function - "cloudbuild.googleapis.com", "cloudfunctions.googleapis.com", + "cloudbuild.googleapis.com", + "cloudresourcemanager.googleapis.com", + "logging.googleapis.com", + "monitoring.googleapis.com", "run.googleapis.com", "secretmanager.googleapis.com", + "storage-api.googleapis.com", ] + billing_account = var.billing_account + create_project_sa = true + default_service_account = "disable" + name = var.project_name + org_id = var.org_id + random_project_id = true + source = "git::https://github.com/terraform-google-modules/terraform-google-project-factory.git?ref=9ac04a6868cadea19a5c016d4d0a4ae35d378b05" # commit hash of v15.0.1 +} + +output "project_id" { + value = module.governance_watchdog.project_id } diff --git a/infra/monitoring.tf b/infra/monitoring.tf index 1e10f56..0423750 100644 --- a/infra/monitoring.tf +++ b/infra/monitoring.tf @@ -1,6 +1,6 @@ # Creates a metric that counts the number of log entries containing 'HealthCheck' in the watchdog cloud function. resource "google_logging_metric" "health_check_metric" { - project = module.bootstrap.seed_project_id + project = module.governance_watchdog.project_id name = "health_check_logs_count" description = "Number of log entries containing 'health check' in the watchdog cloud function" filter = < --format=json | jq -r '.bindings[] | select(.role | startswith("roles/resourcemanager.organizationAdmin")) | .members[] | select(startswith("group:")) | sub("^group:"; "")'` -variable "group_org_admins" { - type = string +variable "function_name" { + type = string + default = "watchdog-notifications" } -# You can find the billing admins group via: -# `gcloud organizations get-iam-policy --format=json | jq -r '.bindings[] | select(.role | startswith("roles/billing.admin")) | .members[] | select(startswith("group:")) | sub("^group:"; "")'` -variable "group_billing_admins" { - type = string +variable "function_entry_point" { + type = string + default = "watchdogNotifier" } # You can look this up via: @@ -89,16 +87,6 @@ variable "x_auth_token" { sensitive = true } -variable "function_name" { - type = string - default = "watchdog-notifications" -} - -variable "function_entry_point" { - type = string - default = "watchdogNotifier" -} - # Webhook URL to send monitoring alerts from within GCP Monitoring # You can find this URL in Victorops by going to "Integrations" -> "Stackdriver". # The routing key can be found under "Settings" -> "Routing Keys" @@ -106,3 +94,18 @@ variable "victorops_webhook_url" { type = string sensitive = true } + +# Used to impersonate our Terraform service account in the Google provider +variable "terraform_service_account" { + type = string + description = "Service account of our Terraform GCP Project which can be impersonated to create and destroy resources in this project" + default = "org-terraform@mento-terraform-seed-ffac.iam.gserviceaccount.com" +} + +# For consistency we also keep this variable in here, although it's not used in the Terraform code (only in the shell scripts) +# trunk-ignore(tflint/terraform_unused_declarations) +variable "terraform_seed_project_id" { + type = string + description = "The GCP Project ID of the Terraform Seed Project housing the terraform state of all projects" + default = "mento-terraform-seed-ffac" +} diff --git a/infra/versions.tf b/infra/versions.tf new file mode 100644 index 0000000..0100a02 --- /dev/null +++ b/infra/versions.tf @@ -0,0 +1,32 @@ +terraform { + required_version = ">= 1.8" + + required_providers { + google = { + source = "hashicorp/google" + version = ">= 5.36.0" + } + archive = { + source = "hashicorp/archive" + version = ">= 2.4.2" + } + local = { + source = "hashicorp/local" + version = ">= 2.5.1" + } + quicknode = { + source = "jmtx1020/quicknode" + version = "0.0.2" + } + } + + backend "gcs" { + bucket = "mento-terraform-tfstate-6ed6" + prefix = "governance-watchdog" + impersonate_service_account = "org-terraform@mento-terraform-seed-ffac.iam.gserviceaccount.com" + } +} + +provider "google" { + impersonate_service_account = var.terraform_service_account +} diff --git a/package-lock.json b/package-lock.json index 98646cc..374dc8e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "@trunkio/launcher": "^1.3.1", "@types/eslint__js": "^8.42.3", "eslint": "^9.7.0", + "nodemon": "^3.1.4", "rimraf": "^6.0.1", "typescript": "^5.5.3", "typescript-eslint": "^8.0.0-alpha.51" @@ -1434,6 +1435,19 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -1506,6 +1520,18 @@ "node": "*" } }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/body-parser": { "version": "1.20.2", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", @@ -1607,6 +1633,42 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/chownr": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", @@ -2405,6 +2467,20 @@ "node": ">=8" } }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -2830,6 +2906,12 @@ "node": ">= 4" } }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -2888,6 +2970,18 @@ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/is-callable": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", @@ -3216,9 +3310,9 @@ } }, "node_modules/micromatch": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", - "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "dependencies": { "braces": "^3.0.3", @@ -3362,6 +3456,78 @@ } } }, + "node_modules/nodemon": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.4.tgz", + "integrity": "sha512-wjPBbFhtpJwmIeY2yP7QF+UKzPfltVGtfce1g/bB15/8vCGZj8uxD62b/b9M9/WVgme0NZudpownKN+c0plXlQ==", + "dev": true, + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/nodemon/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/nodemon/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/nodemon/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/normalize-package-data": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", @@ -3381,6 +3547,15 @@ "semver": "bin/semver" } }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-hash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", @@ -3666,6 +3841,12 @@ "node": ">= 0.10" } }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -3829,6 +4010,18 @@ "node": ">= 6" } }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -4083,6 +4276,18 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -4355,6 +4560,15 @@ "node": ">=0.6" } }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -4450,6 +4664,12 @@ } } }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true + }, "node_modules/undici": { "version": "6.13.0", "resolved": "https://registry.npmjs.org/undici/-/undici-6.13.0.tgz", diff --git a/package.json b/package.json index f498244..82c68cb 100644 --- a/package.json +++ b/package.json @@ -7,21 +7,21 @@ "main": "dist/index.js", "scripts": { "build": "rimraf dist && tsc", - "cache:clear": "./set-project-vars.sh --no-cache", - "deploy": "npm run deploy:via:tf", - "deploy:via:gcloud": "./deploy-via-gcloud.sh", - "deploy:via:terraform": "npm run deploy:via:tf", - "deploy:via:tf": "terraform -chdir=infra apply", + "cache:clear": "./bin/get-project-vars.sh --invalidate-cache", + "deploy": "terraform -chdir=infra apply", + "deploy:function": "./bin/deploy-via-gcloud.sh", + "destroy": "terraform -chdir=infra destroy", + "dev": "nodemon --watch 'src/**/*.ts' --exec 'npm run build && npm run start'", "gcp-build": "npm run build", "generate:env": "terraform -chdir=infra apply -target=local_file.env_file", - "logs": "./get-logs.sh", - "logs:url": "./get-logs-url.sh", + "logs": "./bin/get-logs.sh", + "logs:url": "./bin/get-logs-url.sh", "prestart": "npm run build", "start": "NODE_ENV=development functions-framework --target=watchdogNotifier", "test": "npm run test:local", "test:healthcheck": "curl -H \"Content-Type: application/json\" -d @src/health-check.fixture.json localhost:8080", "test:local": "curl -H \"Content-Type: application/json\" -d @src/proposal-created.fixture.json localhost:8080", - "test:prod": "./test-deployed-function.sh", + "test:prod": "./bin/test-deployed-function.sh", "todo": "git ls-files -c --exclude-standard | grep -v \"package.json\" | xargs grep -n -i --color \"TODO:\\|FIXME:\"" }, "dependencies": { @@ -37,6 +37,7 @@ "@trunkio/launcher": "^1.3.1", "@types/eslint__js": "^8.42.3", "eslint": "^9.7.0", + "nodemon": "^3.1.4", "rimraf": "^6.0.1", "typescript": "^5.5.3", "typescript-eslint": "^8.0.0-alpha.51" diff --git a/set-project-id.sh b/set-project-id.sh deleted file mode 100755 index a107559..0000000 --- a/set-project-id.sh +++ /dev/null @@ -1,50 +0,0 @@ -#!/bin/bash -set -e # Fail on any error -set -o pipefail # Ensure piped commands propagate exit codes properly -set -u # Treat unset variables as an error when substituting - -printf "Looking up project name in variables.tf..." -project_name=$(awk '/variable "project_name"/{f=1} f==1&&/default/{print $3; exit}' ./infra/variables.tf | tr -d '",') -printf ' \033[1m%s\033[0m\n' "${project_name}" - -printf "Fetching the project ID..." -project_id=$(gcloud projects list --filter="name:${project_name}" --format="value(projectId)") -printf ' \033[1m%s\033[0m\n' "${project_id}" - -# Set your local default project -printf "Setting your default project to %s..." "${project_id}" -{ - output=$(gcloud config set project "${project_id}" 2>&1 >/dev/null) - status=$? -} -if [[ ${status} -ne 0 ]]; then - echo "Error: ${output}" - exit "${status}" -fi -printf "✅\n" - -# Set the quota project to the governance-watchdog project, some gcloud commands require this to be set -printf "Setting the quota project to %s..." "${project_id}" -{ - output=$(gcloud auth application-default set-quota-project "${project_id}" 2>&1 >/dev/null) - status=$? -} -if [[ ${status} -ne 0 ]]; then - echo "Error: ${output}" - exit "${status}" -fi -printf "✅\n" - -# Update the project ID in your .env file so your cloud function points to the correct project when running locally -printf "Updating the project ID in your .env file..." -# Check if .env file exists -if [[ ! -f .env ]]; then - # If .env doesn't exist, create it with the initial value - echo "GCP_PROJECT_ID=${project_id}" >.env -else - # If .env exists, perform the sed replacement - sed -i '' "s/^GCP_PROJECT_ID=.*/GCP_PROJECT_ID=${project_id}/" .env -fi -printf "✅\n\n" - -echo "✅ All Done!" diff --git a/set-project-vars.sh b/set-project-vars.sh deleted file mode 100755 index 1fc1709..0000000 --- a/set-project-vars.sh +++ /dev/null @@ -1,79 +0,0 @@ -#!/bin/bash -set -e # Fail on any error -set -o pipefail # Ensure piped commands propagate exit codes properly -set -u # Treat unset variables as an error when substituting - -cache_file=".project_vars_cache" -current_local_project_id=$(gcloud config get project) -current_tf_state_project_id=$(terraform -chdir=infra state show module.bootstrap.module.seed_project.module.project-factory.google_project.main | grep project_id | awk '{print $3}' | tr -d '"') - -if [[ ${current_local_project_id} != "${current_tf_state_project_id}" ]]; then - printf '️\n🚨 Your local gcloud is set to the wrong project: \033[1m%s\033[0m 🚨\n' "${current_local_project_id}" - printf "\nRunning ./set-project-id.sh in an attempt to fix this...\n\n" - source ./set-project-id.sh - printf "\n\n" -fi - -cache_vars() { - if [[ $* != *"--no-cache"* ]]; then - printf "No cache file found at %s.\n\n" "${cache_file}" - fi - - printf "Loading and caching project values...\n\n" - - printf " - Project ID:" - project_id=${current_tf_state_project_id} - printf ' \033[1m%s\033[0m\n' "${project_id}" - - printf " - Project Name:" - project_name=$(awk '/variable "project_name"/{f=1} f==1&&/default/{print $3; exit}' ./infra/variables.tf | tr -d '",') - printf ' \033[1m%s\033[0m\n' "${project_name}" - - printf " - Region:" - region=$(awk '/variable "region"/{f=1} f==1&&/default/{print $3; exit}' ./infra/variables.tf | tr -d '",') - printf ' \033[1m%s\033[0m\n' "${region}" - - printf " - Service Account:" - service_account_email=$(terraform -chdir=infra state show "module.bootstrap.google_service_account.org_terraform[0]" | grep email | awk '{print $3}' | tr -d '"') - printf ' \033[1m%s\033[0m\n' "${service_account_email}" - - printf " - Function Name:" - function_name=$(awk '/variable "function_name"/{f=1} f==1&&/default/{print $3; exit}' ./infra/variables.tf | tr -d '",') - printf ' \033[1m%s\033[0m\n' "${function_name}" - - printf " - Function Entry Point:" - function_entry_point=$(awk '/variable "function_entry_point"/{f=1} f==1&&/default/{print $3; exit}' ./infra/variables.tf | tr -d '",') - printf ' \033[1m%s\033[0m\n' "${function_entry_point}" - - printf "\nCaching values in" - printf ' \033[1m%s\033[0m...' "${cache_file}" - - { - echo "project_id=${project_id}" - echo "project_name=${project_name}" - echo "region=${region}" - echo "service_account_email=${service_account_email}" - echo "function_name=${function_name}" - echo "function_entry_point=${function_entry_point}" - } >>"${cache_file}" - printf "✅\n\n" -} - -if [[ $* == *"--no-cache"* ]]; then - echo "Invalidating cache..." - rm -f "${cache_file}" - cache_vars --no-cache -elif [[ ! -f ${cache_file} ]]; then - cache_vars -else - # shellcheck disable=SC1090 - source "${cache_file}" - printf "Using cached values:\n" - printf " - Project ID: \033[1m%s\033[0m\n" "${project_id}" - printf " - Project Name: \033[1m%s\033[0m\n" "${project_name}" - printf " - Region: \033[1m%s\033[0m\n" "${region}" - printf " - Service Account: \033[1m%s\033[0m\n" "${service_account_email}" - printf " - Function Name: \033[1m%s\033[0m\n" "${function_name}" - printf " - Function Entry Point: \033[1m%s\033[0m\n" "${function_entry_point}" - printf "\n" -fi