diff --git a/.github/workflows/cloud-smoke-cleanup.yml b/.github/workflows/cloud-smoke-cleanup.yml new file mode 100644 index 0000000..a69a006 --- /dev/null +++ b/.github/workflows/cloud-smoke-cleanup.yml @@ -0,0 +1,65 @@ +name: Cloud smoke cleanup + +on: + workflow_run: + workflows: + - Cloud smoke tests + types: + - completed + +permissions: + contents: read + id-token: write + +jobs: + cleanup: + name: Destroy ${{ matrix.name }} + if: github.event.workflow_run.head_repository.full_name == github.repository && contains(fromJSON('["failure","cancelled","timed_out"]'), github.event.workflow_run.conclusion) + runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + include: + - name: DigitalOcean ISLE + provider: digitalocean + template: isle + - name: Linode WordPress + provider: linode + template: wp + - name: GCP WordPress + provider: gcp + template: wp + concurrency: + group: cloud-compose-smoke-${{ matrix.provider }}-${{ matrix.template }} + cancel-in-progress: false + env: + CLOUD_COMPOSE_SMOKE_RUN_ID: ${{ github.event.workflow_run.id }} + DIGITALOCEAN_TOKEN: ${{ secrets.DIGITALOCEAN_TOKEN }} + GCLOUD_OIDC_POOL: ${{ vars.GCLOUD_OIDC_POOL || secrets.GCLOUD_OIDC_POOL }} + GCLOUD_PROJECT: ${{ vars.GCLOUD_PROJECT || secrets.GCLOUD_PROJECT }} + GCLOUD_REGION: ${{ vars.GCLOUD_REGION || secrets.GCLOUD_REGION }} + GSA: ${{ vars.GSA || secrets.GSA }} + LINODE_TOKEN: ${{ secrets.LINODE_TOKEN }} + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Authenticate to Google Cloud + if: matrix.provider == 'gcp' && env.GCLOUD_OIDC_POOL != '' && env.GSA != '' && env.GCLOUD_PROJECT != '' + uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3 + with: + workload_identity_provider: ${{ env.GCLOUD_OIDC_POOL }} + service_account: ${{ env.GSA }} + project_id: ${{ env.GCLOUD_PROJECT }} + + - name: Install gcloud + if: matrix.provider == 'gcp' && env.GCLOUD_OIDC_POOL != '' && env.GSA != '' && env.GCLOUD_PROJECT != '' + uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db # v3 + + - name: Sweep smoke resources + if: matrix.provider != 'gcp' || (env.GCLOUD_OIDC_POOL != '' && env.GSA != '' && env.GCLOUD_PROJECT != '') + run: ci/cloud-smoke.sh sweep-${{ matrix.provider }}-${{ matrix.template }} + + - name: Skip GCP cleanup + if: matrix.provider == 'gcp' && (env.GCLOUD_OIDC_POOL == '' || env.GSA == '' || env.GCLOUD_PROJECT == '') + run: echo "Skipping GCP cleanup because GCP smoke secrets are not configured." diff --git a/.github/workflows/cloud-smoke.yml b/.github/workflows/cloud-smoke.yml new file mode 100644 index 0000000..da1e5c4 --- /dev/null +++ b/.github/workflows/cloud-smoke.yml @@ -0,0 +1,162 @@ +name: Cloud smoke tests + +on: + pull_request: + branches: + - main + +permissions: + contents: read + id-token: write + +concurrency: + group: cloud-compose-smoke + cancel-in-progress: true + +jobs: + terraform-lint: + name: Terraform lint + runs-on: ubuntu-24.04 + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Install Terraform + uses: hashicorp/setup-terraform@dfe3c3f87815947d99a8997f908cb6525fc44e9e # v4 + with: + terraform_version: 1.14.6 + terraform_wrapper: false + + - name: Run Terraform lint + run: make terraform-lint-check + + smoke: + name: ${{ matrix.name }} + needs: terraform-lint + runs-on: ubuntu-24.04 + timeout-minutes: 150 + if: github.event.pull_request.head.repo.full_name == github.repository + strategy: + fail-fast: false + matrix: + include: + - name: DigitalOcean ISLE + provider: digitalocean + template: isle + - name: Linode WordPress + provider: linode + template: wp + - name: GCP WordPress + provider: gcp + template: wp + concurrency: + group: cloud-compose-smoke-${{ matrix.provider }}-${{ matrix.template }} + cancel-in-progress: false + env: + CLOUD_COMPOSE_SMOKE_AUTO_APPROVE: "true" + CLOUD_COMPOSE_SMOKE_DESTROY_TIMEOUT: "1800" + CLOUD_COMPOSE_SMOKE_SWEEP_ORPHANS: "true" + CLOUD_COMPOSE_SMOKE_RUN_ID: ${{ github.run_id }} + CLOUD_COMPOSE_SOURCE_REF: ${{ github.event.pull_request.head.sha }} + DIGITALOCEAN_TOKEN: ${{ secrets.DIGITALOCEAN_TOKEN }} + GCLOUD_OIDC_POOL: ${{ vars.GCLOUD_OIDC_POOL || secrets.GCLOUD_OIDC_POOL }} + GCLOUD_PROJECT: ${{ vars.GCLOUD_PROJECT || secrets.GCLOUD_PROJECT }} + GCLOUD_REGION: ${{ vars.GCLOUD_REGION || secrets.GCLOUD_REGION }} + GSA: ${{ vars.GSA || secrets.GSA }} + LINODE_TOKEN: ${{ secrets.LINODE_TOKEN }} + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Install Terraform + uses: hashicorp/setup-terraform@dfe3c3f87815947d99a8997f908cb6525fc44e9e # v4 + with: + terraform_version: 1.14.6 + terraform_wrapper: false + + - name: Check GCP smoke secrets + if: matrix.provider == 'gcp' + run: | + test -n "$GCLOUD_OIDC_POOL" || { echo "GCLOUD_OIDC_POOL Actions variable or secret is required for GCP smoke tests"; exit 1; } + test -n "$GSA" || { echo "GSA Actions variable or secret is required for GCP smoke tests"; exit 1; } + test -n "$GCLOUD_PROJECT" || { echo "GCLOUD_PROJECT Actions variable or secret is required for GCP smoke tests"; exit 1; } + + - name: Authenticate to Google Cloud + if: matrix.provider == 'gcp' + uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3 + with: + workload_identity_provider: ${{ env.GCLOUD_OIDC_POOL }} + service_account: ${{ env.GSA }} + project_id: ${{ env.GCLOUD_PROJECT }} + + - name: Install gcloud + if: matrix.provider == 'gcp' + uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db # v3 + + - name: Install sitectl + run: ci/install-sitectl-apt.sh + + - name: Run smoke test + run: make smoke-test PROVIDER=${{ matrix.provider }} TEMPLATE=${{ matrix.template }} + + - name: Destroy smoke resources + if: always() && (matrix.provider != 'gcp' || (env.GCLOUD_OIDC_POOL != '' && env.GSA != '' && env.GCLOUD_PROJECT != '')) + run: ci/cloud-smoke.sh destroy-${{ matrix.provider }}-${{ matrix.template }} + + - name: Skip GCP destroy + if: always() && matrix.provider == 'gcp' && (env.GCLOUD_OIDC_POOL == '' || env.GSA == '' || env.GCLOUD_PROJECT == '') + run: echo "Skipping GCP destroy because GCP smoke secrets are not configured and no GCP resources were applied." + + cleanup: + name: Destroy ${{ matrix.name }} + needs: smoke + if: always() && (failure() || cancelled()) + runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + include: + - name: DigitalOcean ISLE + provider: digitalocean + template: isle + - name: Linode WordPress + provider: linode + template: wp + - name: GCP WordPress + provider: gcp + template: wp + concurrency: + group: cloud-compose-smoke-${{ matrix.provider }}-${{ matrix.template }} + cancel-in-progress: false + env: + CLOUD_COMPOSE_SMOKE_RUN_ID: ${{ github.run_id }} + DIGITALOCEAN_TOKEN: ${{ secrets.DIGITALOCEAN_TOKEN }} + GCLOUD_OIDC_POOL: ${{ vars.GCLOUD_OIDC_POOL || secrets.GCLOUD_OIDC_POOL }} + GCLOUD_PROJECT: ${{ vars.GCLOUD_PROJECT || secrets.GCLOUD_PROJECT }} + GCLOUD_REGION: ${{ vars.GCLOUD_REGION || secrets.GCLOUD_REGION }} + GSA: ${{ vars.GSA || secrets.GSA }} + LINODE_TOKEN: ${{ secrets.LINODE_TOKEN }} + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Authenticate to Google Cloud + if: matrix.provider == 'gcp' && env.GCLOUD_OIDC_POOL != '' && env.GSA != '' && env.GCLOUD_PROJECT != '' + uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3 + with: + workload_identity_provider: ${{ env.GCLOUD_OIDC_POOL }} + service_account: ${{ env.GSA }} + project_id: ${{ env.GCLOUD_PROJECT }} + + - name: Install gcloud + if: matrix.provider == 'gcp' && env.GCLOUD_OIDC_POOL != '' && env.GSA != '' && env.GCLOUD_PROJECT != '' + uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db # v3 + + - name: Sweep smoke resources + if: matrix.provider != 'gcp' || (env.GCLOUD_OIDC_POOL != '' && env.GSA != '' && env.GCLOUD_PROJECT != '') + run: ci/cloud-smoke.sh sweep-${{ matrix.provider }}-${{ matrix.template }} + + - name: Skip GCP cleanup + if: matrix.provider == 'gcp' && (env.GCLOUD_OIDC_POOL == '' || env.GSA == '' || env.GCLOUD_PROJECT == '') + run: echo "Skipping GCP cleanup because GCP smoke secrets are not configured." diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..208e5cb --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,40 @@ +name: "Build docs" + +on: + push: + branches: + - main + paths: + - "docs/**" + - ".github/workflows/docs.yml" + - "Makefile" + +permissions: + contents: read + pages: write + id-token: write + +jobs: + deploy: + runs-on: ubuntu-24.04 + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Build docs + run: make docs-build + + - name: Setup Pages + uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6 + + - name: Upload artifact + uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5 + with: + path: ./docs/site + + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5 diff --git a/.gitignore b/.gitignore index 23031b9..6ac0c2b 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,7 @@ override.tf override.tf.json *_override.tf *_override.tf.json +site/ +docs/site/ +docs/.cache/ +.cloud-compose-smoke/ diff --git a/.terraform.lock.hcl b/.terraform.lock.hcl index d09b678..f56a508 100644 --- a/.terraform.lock.hcl +++ b/.terraform.lock.hcl @@ -1,6 +1,30 @@ # This file is maintained automatically by "terraform init". # Manual edits may be lost in future updates. +provider "registry.terraform.io/digitalocean/digitalocean" { + version = "2.93.0" + constraints = "~> 2.0" + hashes = [ + "h1:furxWCm/4BbtUX/XHa3TsP0eI+YjQekvcUTyx4BLqnM=", + "zh:2509716282fb0d04027279b2d6ffb449d5f4b4a5f380a2ad7b7b6221d2f2fdc4", + "zh:3a6407123d127b6948040583c29371b6ea92120d2a239d8bcd27673725673013", + "zh:71443c90f85572fce0c563e3587d956afb8be9f0a0090f1c40b33b1005718824", + "zh:790c8a3694baba6e28b13d45d86523995ffda4693e0c8ad1584e34deee914fe2", + "zh:7fe00fd5d4d322923550651b6ebca9dea14b3b7a68c09ba4c20f9b416a34cb40", + "zh:8bf4d5ccb634bacba929fc632d554eddb349b890d26e5f4dbcb53481ca34dbff", + "zh:8f545ec1622fa4d42a7112de8cbd0c42f8967e8dafd5c514d77b7ec25914951d", + "zh:98300c632e9eabdcecb9f77586a4a48e19dab62b7365410f4ab857ab4007e159", + "zh:9db648994459fe62a9e91f9d875dd9220db5b063a00f4eec22dbdf2247a67730", + "zh:9f21713916718613da2a42c8b39c503e46399b2d06c3b492979bde939d05d2bb", + "zh:a4b4afa0ef6f672d50ac072cae1a39ec6d97716e4ed118900b5db1a553a3c1cd", + "zh:bebed73ab49be830ef03346cb91e1da83fff06b555695f69fc3de13c51a456ec", + "zh:d207d54f3ab86a0f71c194b8b34a49a219f97dd40ec6db0ed13ed70df9f26aab", + "zh:ee4caa17a2e62bfa9c2669ddd909026a3ded3294c4f2bb325cf563f5766e1981", + "zh:efb59a5436f103a4930a7cab2e77ba75d31fd08f2f024c9a4177c70a89cd3264", + "zh:ffaf97ab916b12c2181bdf5f7232aaac88cce59bb8edbad07a527207da232f90", + ] +} + provider "registry.terraform.io/hashicorp/cloudinit" { version = "2.4.0" constraints = "~> 2.3" @@ -72,3 +96,24 @@ provider "registry.terraform.io/hashicorp/time" { "zh:c239c619101a8c95e1f14061eb973c57a8d15fa0e68878ced5bbd76858ee5b79", ] } + +provider "registry.terraform.io/linode/linode" { + version = "4.0.0" + constraints = "~> 4.0" + hashes = [ + "h1:ZUykBGGXIJsXzafpxxKN+oRJh/n/d+jFiiTZlYVDrvY=", + "zh:01e0045344d13ca61c8b065df73b007bf45f665eb419d045a31c8d9ba48ede86", + "zh:3e22a4672940ec1a8c59bd6c8d594a6fb4bdfc50efa993ac2b9edd21a9459b03", + "zh:467c82a79a04cc2f279b4b8206db691035dbcb3ba88f8a29a63e060046a7747b", + "zh:4d7f3a1ec9dfa15ea2bac0f9670885812d09e45737d55c7daea049770143bc1d", + "zh:4d9b946d66940231488990943f9564725891310f8500c149af380eba614f5ab4", + "zh:75b48ac11262ac401c2fc3eb5fb73b01ad1607a1c2a0ddcb42687d1941158e6d", + "zh:864de67a080eeb4b14902056dc96bd29f1dc935442440cdb774ad2d46a872ca9", + "zh:a3fd99fb4f9b2e631c63b0ab9ff4d28e32aca4ff0a14b9271fdc634798ecc808", + "zh:b0c63ea32e65a76e987a086dc4ee6d8f445876a59d7a5a94aa2f225f5fd8729f", + "zh:b3d6f4461724983798af50bc91003b0e7892a5b2ab172d7fd0a8d57578c6a7bf", + "zh:b5f2643c36657241b6ef4a987727e99352ed2dcf63d54cba6d5292ee0edd59f4", + "zh:bf148ad0a4cdd049de12bdea2dc2bb20f2a0e09574a1917f7e45c1f54aa8dc0b", + "zh:d1448b79e1c9c9d14adc03c97730f68fec042011012ec6727ff7b17d7e130df4", + ] +} diff --git a/Makefile b/Makefile index a7a927f..8e51f0d 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,97 @@ -.PHONY: docs lint +.PHONY: lint lint-check terraform-fmt terraform-fmt-check terraform-validate terraform-lint-check shell-lint terraform-docs smoke-test-clouds smoke-test smoke-test-digitalocean-isle smoke-test-linode-wp smoke-test-gcp-wp destroy-smoke destroy-smoke-digitalocean-isle destroy-smoke-linode-wp destroy-smoke-gcp-wp docs docs-docker-build docs-build docs-serve docs-preview docs-clean -lint: - @find . -type f -name "*.tf" -exec terraform fmt {} + - @find . -type f -name "*.sh" -exec shellcheck {} + +DOCS_IMAGE ?= cloud-compose-docs +DOCS_PORT ?= 8888 +DOCS_DOCKER_USER ?= $(shell id -u):$(shell id -g) -docs: +lint: terraform-fmt shell-lint terraform-validate + +lint-check: terraform-fmt-check shell-lint terraform-validate + +terraform-fmt: + terraform fmt -recursive + +terraform-fmt-check: + terraform fmt -check -recursive + +terraform-validate: + bash ci/terraform-validate.sh + +terraform-lint-check: terraform-fmt-check terraform-validate + +shell-lint: + @find . \ + -path "./.terraform" -prune -o \ + -path "./docs/site" -prune -o \ + -type f -name "*.sh" -print0 | xargs -0 shellcheck + +terraform-docs: terraform-docs markdown table --sort-by required --output-file README.md . +smoke-test-clouds: + ci/cloud-smoke.sh all + +smoke-test: + @test -n "$(PROVIDER)" || { echo "PROVIDER is required"; exit 2; } + @test -n "$(TEMPLATE)" || { echo "TEMPLATE is required"; exit 2; } + ci/cloud-smoke.sh $(PROVIDER)-$(TEMPLATE) + +smoke-test-digitalocean-isle: + $(MAKE) smoke-test PROVIDER=digitalocean TEMPLATE=isle + +smoke-test-linode-wp: + $(MAKE) smoke-test PROVIDER=linode TEMPLATE=wp + +smoke-test-gcp-wp: + $(MAKE) smoke-test PROVIDER=gcp TEMPLATE=wp + +destroy-smoke: + @test -n "$(PROVIDER)" || { echo "PROVIDER is required"; exit 2; } + @test -n "$(TEMPLATE)" || { echo "TEMPLATE is required"; exit 2; } + ci/cloud-smoke.sh destroy-$(PROVIDER)-$(TEMPLATE) + +destroy-smoke-digitalocean-isle: + $(MAKE) destroy-smoke PROVIDER=digitalocean TEMPLATE=isle + +destroy-smoke-linode-wp: + $(MAKE) destroy-smoke PROVIDER=linode TEMPLATE=wp + +destroy-smoke-gcp-wp: + $(MAKE) destroy-smoke PROVIDER=gcp TEMPLATE=wp + +docs: docs-build + +docs-docker-build: + docker build -f docs/Dockerfile -t $(DOCS_IMAGE) . + +docs-build: docs-docker-build + rm -rf site docs/site + docker run --rm \ + -u "$(DOCS_DOCKER_USER)" \ + $(if $(SITE_URL),-e SITE_URL=$(SITE_URL)) \ + -v "$(CURDIR):/work" \ + -w /work \ + $(DOCS_IMAGE) \ + build --clean --config-file docs/mkdocs.yml + +docs-serve: docs-docker-build + docker run --rm -it \ + -u "$(DOCS_DOCKER_USER)" \ + -p $(DOCS_PORT):8080 \ + -v "$(CURDIR):/work" \ + -w /work \ + $(DOCS_IMAGE) \ + serve --config-file docs/mkdocs.yml --dev-addr 0.0.0.0:8080 + +docs-preview: + $(MAKE) docs-build SITE_URL=http://localhost:$(DOCS_PORT) + docker run --rm -it \ + -p $(DOCS_PORT):8080 \ + -v "$(CURDIR)/docs/site:/site" \ + -w /site \ + --entrypoint python3 \ + $(DOCS_IMAGE) \ + -m http.server 8080 + +docs-clean: + rm -rf site docs/site diff --git a/README.md b/README.md index e79a468..124b48b 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,7 @@ # cloud-compose -Deploy a docker compose project to a Google Cloud Compute Instance. - -Optional VM APIs: - -- [Rollout API](docs/rollout.md) exposes authenticated deployment rollout triggers for the compose project. -- [Managed Runtime](docs/managed-runtime.md) keeps `sitectl`, sitectl plugins, and LibOps-side support services up to date on the VM. +Deploy Docker Compose projects to cloud VMs. The root module targets Google +Cloud; provider modules under `modules/` cover DigitalOcean and Linode. ## Requirements @@ -13,130 +9,51 @@ Optional VM APIs: | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.2.4 | -| [cloudinit](#requirement\_cloudinit) | ~> 2.3 | -| [google](#requirement\_google) | ~> 7.0 | -| [time](#requirement\_time) | ~> 0.14 | ## Providers -| Name | Version | -|------|---------| -| [cloudinit](#provider\_cloudinit) | 2.4.0 | -| [google](#provider\_google) | 7.34.0 | -| [time](#provider\_time) | 0.14.0 | +No providers. ## Modules | Name | Source | Version | |------|--------|---------| -| [ppb](#module\_ppb) | git::https://github.com/libops/terraform-cloudrun-v2 | 0.5.3 | +| [digitalocean](#module\_digitalocean) | ./modules/digitalocean | n/a | +| [gcp](#module\_gcp) | ./modules/gcp | n/a | +| [linode](#module\_linode) | ./modules/linode | n/a | ## Resources -| Name | Type | -|------|------| -| [google_artifact_registry_repository_iam_member.private-policy-cloud-compose](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/artifact_registry_repository_iam_member) | resource | -| [google_compute_disk.boot](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/compute_disk) | resource | -| [google_compute_disk.data](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/compute_disk) | resource | -| [google_compute_disk.docker-volumes](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/compute_disk) | resource | -| [google_compute_disk.overlay_disk](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/compute_disk) | resource | -| [google_compute_disk_resource_policy_attachment.daily_snapshot](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/compute_disk_resource_policy_attachment) | resource | -| [google_compute_disk_resource_policy_attachment.weekly_snapshot](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/compute_disk_resource_policy_attachment) | resource | -| [google_compute_firewall.allow_rollout_ipv4](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/compute_firewall) | resource | -| [google_compute_firewall.allow_ssh_ipv4](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/compute_firewall) | resource | -| [google_compute_firewall.allow_ssh_ipv6](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/compute_firewall) | resource | -| [google_compute_instance.cloud-compose](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/compute_instance) | resource | -| [google_compute_reservation.production](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/compute_reservation) | resource | -| [google_compute_resource_policy.daily_snapshot](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/compute_resource_policy) | resource | -| [google_compute_resource_policy.weekly_snapshot](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/compute_resource_policy) | resource | -| [google_project_iam_member.gce-start](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/project_iam_member) | resource | -| [google_project_iam_member.gce-suspend](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/project_iam_member) | resource | -| [google_project_iam_member.log](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/project_iam_member) | resource | -| [google_project_iam_member.stackdriver](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/project_iam_member) | resource | -| [google_service_account.app](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/service_account) | resource | -| [google_service_account.cloud-compose](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/service_account) | resource | -| [google_service_account.internal-services](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/service_account) | resource | -| [google_service_account.ppb](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/service_account) | resource | -| [google_service_account_iam_member.app-keys](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/service_account_iam_member) | resource | -| [google_service_account_iam_member.gsa-user](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/service_account_iam_member) | resource | -| [google_service_account_iam_member.internal-services-keys](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/service_account_iam_member) | resource | -| [google_service_account_iam_member.self_jwt_signer_policy](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/service_account_iam_member) | resource | -| [google_service_account_iam_member.token-creator](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/service_account_iam_member) | resource | -| [time_static.snapshot_time_static](https://registry.terraform.io/providers/hashicorp/time/latest/docs/resources/static) | resource | -| [cloudinit_config.ci](https://registry.terraform.io/providers/hashicorp/cloudinit/latest/docs/data-sources/config) | data source | -| [google_compute_snapshot.latest_prod](https://registry.terraform.io/providers/hashicorp/google/latest/docs/data-sources/compute_snapshot) | data source | -| [google_project_iam_custom_role.gce-start](https://registry.terraform.io/providers/hashicorp/google/latest/docs/data-sources/project_iam_custom_role) | data source | -| [google_project_iam_custom_role.gce-suspend](https://registry.terraform.io/providers/hashicorp/google/latest/docs/data-sources/project_iam_custom_role) | data source | +No resources. ## Inputs | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| -| [docker\_compose\_repo](#input\_docker\_compose\_repo) | git repo to checkout that contains a docker compose project | `string` | n/a | yes | -| [name](#input\_name) | The site name (will be the name of the GCP instance) | `string` | n/a | yes | -| [project\_id](#input\_project\_id) | The GCP project ID | `string` | n/a | yes | -| [project\_number](#input\_project\_number) | The GCP project number | `string` | n/a | yes | -| [allowed\_ips](#input\_allowed\_ips) | CIDR IP Addresses allowed to turn on this site's GCP instance | `list(string)` | `[]` | no | -| [allowed\_ssh\_ipv4](#input\_allowed\_ssh\_ipv4) | CIDR IPv4 Addresses allowed to to SSH into this site's GCP instance | `list(string)` | `[]` | no | -| [allowed\_ssh\_ipv6](#input\_allowed\_ssh\_ipv6) | CIDR IPv6 Addresses allowed to SSH into this site's GCP instance | `list(string)` | `[]` | no | -| [artifact\_registry\_location](#input\_artifact\_registry\_location) | Artifact Registry location for var.artifact\_registry\_repository. | `string` | `"us"` | no | -| [artifact\_registry\_repository](#input\_artifact\_registry\_repository) | Optional Artifact Registry repository name to grant the VM service account reader access to. Leave empty to skip creating the IAM binding. | `string` | `""` | no | -| [disk\_size\_gb](#input\_disk\_size\_gb) | Data disk size in GB | `number` | `50` | no | -| [disk\_type](#input\_disk\_type) | The disk type for disks attached to the machine | `string` | `"hyperdisk-balanced"` | no | -| [docker\_compose\_branch](#input\_docker\_compose\_branch) | git branch to checkout for var.docker\_compose\_repo | `string` | `"main"` | no | -| [docker\_compose\_down](#input\_docker\_compose\_down) | Command to stop the docker compose project | `list(string)` |
[
"sitectl compose --context \"${SITECTL_CONTEXT_NAME}\" down"
]
| no | -| [docker\_compose\_init](#input\_docker\_compose\_init) | After cloning the docker compose git repo, any initialization that needs to happen before the docker compose project can start. One command per list value | `list(string)` |
[
"sitectl config set-context \"${SITECTL_CONTEXT_NAME}\" --type local --project-dir \"${DOCKER_COMPOSE_DIR}\" --site \"${GCP_INSTANCE_NAME}\" --plugin \"${SITECTL_PLUGIN}\" --environment \"${SITECTL_ENVIRONMENT}\" --project-name \"${GCP_INSTANCE_NAME}\" --compose-project-name \"${COMPOSE_PROJECT_NAME}\" --docker-socket /var/run/docker.sock --env-file .env --default"
]
| no | -| [docker\_compose\_rollout](#input\_docker\_compose\_rollout) | Command to roll out a new git ref for the docker compose project. The optional rollout service sets GIT\_REF/GIT\_BRANCH from the trigger request. | `list(string)` |
[
"TARGET_REF=\"${GIT_REF:-${GIT_BRANCH:-${DOCKER_COMPOSE_BRANCH:-main}}}\"",
"if [ -x ./scripts/rollout.sh ]; then ./scripts/rollout.sh; else sitectl deploy --context \"${SITECTL_CONTEXT_NAME}\" --branch \"$TARGET_REF\"; fi",
"sitectl healthcheck --context \"${SITECTL_CONTEXT_NAME}\" --persist --timeout \"${SITECTL_HEALTHCHECK_TIMEOUT}\" --interval \"${SITECTL_HEALTHCHECK_INTERVAL}\"",
"if [ \"${SITECTL_ENVIRONMENT}\" != \"production\" ]; then sitectl verify --context \"${SITECTL_CONTEXT_NAME}\" ${SITECTL_VERIFY_ARGS:-}; fi"
]
| no | -| [docker\_compose\_up](#input\_docker\_compose\_up) | Command to start the docker compose project | `list(string)` |
[
"sitectl deploy --context \"${SITECTL_CONTEXT_NAME}\" --skip-git",
"sitectl healthcheck --context \"${SITECTL_CONTEXT_NAME}\" --persist --timeout \"${SITECTL_HEALTHCHECK_TIMEOUT}\" --interval \"${SITECTL_HEALTHCHECK_INTERVAL}\"",
"if [ \"${SITECTL_ENVIRONMENT}\" != \"production\" ]; then sitectl verify --context \"${SITECTL_CONTEXT_NAME}\" ${SITECTL_VERIFY_ARGS:-}; fi",
"sitectl compose --context \"${SITECTL_CONTEXT_NAME}\" logs -f"
]
| no | -| [frontend](#input\_frontend) | Optional frontend container to deploy as a sidecar next to ppb. When set,
ppb continues to power on and ping the VM referenced by machineMetadata,
but proxies incoming requests to this container on localhost instead of
to the VM. Use this to serve a frontend from Cloud Run while keeping
backend services on the VM. |
object({
image = string
port = optional(number, 8080)
cpu = optional(string, "1000m")
memory = optional(string, "1Gi")
})
| `null` | no | -| [ingress\_port](#input\_ingress\_port) | TCP port on the VM that the Cloud Run ingress should connect to. | `number` | `80` | no | -| [initcmd](#input\_initcmd) | Commands to run before /home/cloud-compose/run.sh | `list(string)` | `[]` | no | -| [libops\_cadvisor\_image](#input\_libops\_cadvisor\_image) | Container image used for the internal cAdvisor service. | `string` | `"ghcr.io/google/cadvisor:v0.57.0@sha256:e75bdb03b74b0b6995f208f166fead2e6e555dde73e44200113bb26f41b1981d"` | no | -| [libops\_cap\_image](#input\_libops\_cap\_image) | Container image used for the internal CAP metrics service. | `string` | `"ghcr.io/libops/cap:main"` | no | -| [libops\_internal\_services\_auto\_update](#input\_libops\_internal\_services\_auto\_update) | Whether the managed runtime updater should pull and restart the internal LibOps compose project. | `bool` | `true` | no | -| [libops\_lightsout\_image](#input\_libops\_lightsout\_image) | Container image used for the internal lightsout service. | `string` | `"ghcr.io/libops/lightsout:main"` | no | -| [libops\_managed\_artifacts](#input\_libops\_managed\_artifacts) | Additional LibOps-managed files or binaries to download, verify, install, and optionally restart with the managed runtime updater. |
list(object({
name = string
url = string
sha256 = string
path = string
mode = optional(string, "0755")
owner = optional(string, "root")
group = optional(string, "root")
restart = optional(string, "")
}))
| `[]` | no | -| [libops\_managed\_runtime\_enabled](#input\_libops\_managed\_runtime\_enabled) | Install and periodically update LibOps-managed host tools and internal VM services. | `bool` | `true` | no | -| [machine\_type](#input\_machine\_type) | VM machine type (General-purpose series that support Hyperdisk Balanced | `string` | `"n4-standard-2"` | no | -| [os](#input\_os) | The host OS to install on the GCP instance | `string` | `"cos-125-19216-220-185"` | no | -| [overlay\_source\_instance](#input\_overlay\_source\_instance) | Name of production instance to get latest snapshot from (e.g., 'ojs-production'). Terraform will automatically use the most recent snapshot from this instance's data disk. Leave empty for production environments. | `string` | `""` | no | -| [production](#input\_production) | Whether this VM is the production environment. Production VMs reserve one matching machine so stop/start and recreate operations keep capacity. | `bool` | `false` | no | -| [region](#input\_region) | GCP region for resources | `string` | `"us-east5"` | no | -| [rollout\_allowed\_ipv4](#input\_rollout\_allowed\_ipv4) | CIDR IPv4 ranges allowed to reach the rollout service port. | `list(string)` |
[
"10.0.0.0/8"
]
| no | -| [rollout\_custom\_claims](#input\_rollout\_custom\_claims) | Optional JSON object of additional JWT claims required by the rollout service. | `string` | `""` | no | -| [rollout\_enabled](#input\_rollout\_enabled) | Install and run the optional generic rollout HTTP service on the VM. | `bool` | `false` | no | -| [rollout\_jwks\_uri](#input\_rollout\_jwks\_uri) | JWKS URI used by the rollout service to validate bearer JWTs. | `string` | `""` | no | -| [rollout\_jwt\_audience](#input\_rollout\_jwt\_audience) | JWT audience required by the rollout service. | `string` | `""` | no | -| [rollout\_port](#input\_rollout\_port) | TCP port exposed by the optional rollout service. | `number` | `8081` | no | -| [rollout\_release\_sha256](#input\_rollout\_release\_sha256) | Lowercase SHA256 checksum for var.rollout\_release\_url. | `string` | `""` | no | -| [rollout\_release\_url](#input\_rollout\_release\_url) | HTTPS URL for the pinned rollout Linux binary. | `string` | `""` | no | -| [rootfs](#input\_rootfs) | Path to additional rootfs files to copy into the VM. Files will be merged with the base rootfs. Example: '/path/to/custom/rootfs' | `string` | `""` | no | -| [run\_snapshots](#input\_run\_snapshots) | Enable daily snapshots of the data disk (recommended for production). Last seven days of snapshots are available. Also weekly snapshots for past year. | `bool` | `false` | no | -| [runcmd](#input\_runcmd) | Additional commands to run during cloud-init. Commands are executed after the main initialization. | `list(string)` | `[]` | no | -| [service\_account\_email](#input\_service\_account\_email) | Existing service account email for the VM. When empty, this module creates one. | `string` | `""` | no | -| [sitectl\_context\_name](#input\_sitectl\_context\_name) | Sitectl context name to create on the VM. Defaults to var.name. | `string` | `""` | no | -| [sitectl\_environment](#input\_sitectl\_environment) | Sitectl environment label. Production runs healthcheck only by default; non-production also runs sitectl verify. | `string` | `"production"` | no | -| [sitectl\_healthcheck\_interval](#input\_sitectl\_healthcheck\_interval) | Interval passed to sitectl healthcheck --interval in default lifecycle commands. | `string` | `"15s"` | no | -| [sitectl\_healthcheck\_timeout](#input\_sitectl\_healthcheck\_timeout) | Timeout passed to sitectl healthcheck --timeout in default lifecycle commands. | `string` | `"10m"` | no | -| [sitectl\_packages](#input\_sitectl\_packages) | LibOps GitHub release package names to install and keep updated on the VM. Include plugin packages such as sitectl-isle or sitectl-wp as needed. | `list(string)` |
[
"sitectl"
]
| no | -| [sitectl\_plugin](#input\_sitectl\_plugin) | Sitectl plugin id to associate with the VM context. | `string` | `"core"` | no | -| [sitectl\_verify\_args](#input\_sitectl\_verify\_args) | Additional arguments appended to sitectl verify by the default non-production lifecycle commands. | `list(string)` | `[]` | no | -| [sitectl\_version](#input\_sitectl\_version) | Sitectl release tag to install for sitectl packages, or latest to follow https://github.com/libops/sitectl/releases/latest. | `string` | `"latest"` | no | -| [users](#input\_users) | Map of usernames to lists of SSH public keys. Users will be created with docker group membership. Example: { "alice" = ["ssh-rsa AAAA..."], "bob" = ["ssh-ed25519 AAAA...", "ssh-rsa BBBB..."] } | `map(list(string))` | `{}` | no | -| [volume\_names](#input\_volume\_names) | List of docker volumes to overlay from production snapshot (e.g., ['compose\_ojs-public']). Production data is mounted read-only as lower layer, staging writes go to upper layer. | `list(string)` | `[]` | no | -| [zone](#input\_zone) | GCP zone for resources | `string` | `"us-east5-b"` | no | +| [name](#input\_name) | Deployment name. | `string` | n/a | yes | +| [cloud\_provider](#input\_cloud\_provider) | Cloud provider to deploy to. Supported values are gcp, digitalocean, and linode. | `string` | `"gcp"` | no | +| [digitalocean](#input\_digitalocean) | DigitalOcean infrastructure settings. |
object({
region = optional(string, "tor1")
tags = optional(list(string), ["cloud-compose"])

droplet = optional(object({
size = optional(string, "s-2vcpu-4gb")
image = optional(string, "ubuntu-24-04-x64")
ssh_keys = optional(list(string), [])
vpc_uuid = optional(string, null)
monitoring = optional(bool, true)
ipv6 = optional(bool, true)
backups = optional(bool, false)
}), {})

ssh = optional(object({
cloud_compose_keys = optional(list(string), [])
users = optional(map(list(string)), {})
}), {})

volumes = optional(object({
data_size_gb = optional(number, 50)
docker_volumes_size_gb = optional(number, 100)
}), {})

firewall = optional(object({
enabled = optional(bool, true)
ssh_source_addresses = optional(list(string), ["0.0.0.0/0", "::/0"])
web_source_addresses = optional(list(string), ["0.0.0.0/0", "::/0"])
}), {})
})
| `{}` | no | +| [gcp](#input\_gcp) | Google Cloud infrastructure settings. |
object({
project_id = optional(string, "")
project_number = optional(string, "")
region = optional(string, "us-east5")
zone = optional(string, "us-east5-b")

identity = optional(object({
vm_service_account_email = optional(string, "")
app_service_account_email = optional(string, "")
}), {})

instance = optional(object({
machine_type = optional(string, "n4-standard-2")
os = optional(string, "cos-125-19216-220-185")
production = optional(bool, false)
}), {})

disks = optional(object({
type = optional(string, "hyperdisk-balanced")
docker_volumes_size_gb = optional(number, 50)
}), {})

network = optional(object({
create = optional(bool, true)
name = optional(string, "")
subnetwork = optional(string, "")
ip_cidr_range = optional(string, "10.42.0.0/24")
power_button_allowed_ips = optional(list(string), [])
ssh_ipv4 = optional(list(string), [])
ssh_ipv6 = optional(list(string), [])
}), {})

snapshots = optional(object({
enabled = optional(bool, false)
}), {})

overlay = optional(object({
source_instance = optional(string, "")
volume_names = optional(list(string), [])
}), {})

cloud_init = optional(object({
initcmd = optional(list(string), [])
runcmd = optional(list(string), [])
}), {})

artifact_registry = optional(object({
repository = optional(string, "")
location = optional(string, "us")
}), {})

power_management = optional(object({
enabled = optional(bool, true)
frontend = optional(object({
image = string
port = optional(number, 8080)
cpu = optional(string, "1000m")
memory = optional(string, "1Gi")
}), null)
}), {})

rollout = optional(object({
enabled = optional(bool, false)
release_url = optional(string, "")
release_sha256 = optional(string, "")
port = optional(number, 8081)
jwks_uri = optional(string, "")
jwt_audience = optional(string, "")
custom_claims = optional(string, "")
allowed_ipv4 = optional(list(string), ["10.0.0.0/8"])
}), {})
})
| `{}` | no | +| [linode](#input\_linode) | Linode infrastructure settings. |
object({
region = optional(string, "us-east")
tags = optional(list(string), ["cloud-compose"])

instance = optional(object({
type = optional(string, "g6-standard-2")
image = optional(string, "linode/ubuntu22.04")
authorized_keys = optional(list(string), [])
authorized_users = optional(list(string), [])
root_pass = optional(string, null)
private_ip = optional(bool, true)
backups_enabled = optional(bool, false)
watchdog_enabled = optional(bool, true)
}), {})

ssh = optional(object({
cloud_compose_keys = optional(list(string), [])
users = optional(map(list(string)), {})
}), {})

volumes = optional(object({
data_size_gb = optional(number, 50)
docker_volumes_size_gb = optional(number, 100)
}), {})

firewall = optional(object({
enabled = optional(bool, true)
ssh_source_ipv4 = optional(list(string), ["0.0.0.0/0"])
ssh_source_ipv6 = optional(list(string), ["::/0"])
web_source_ipv4 = optional(list(string), ["0.0.0.0/0"])
web_source_ipv6 = optional(list(string), ["::/0"])
}), {})
})
| `{}` | no | +| [runtime](#input\_runtime) | Provider-neutral compose/runtime settings. |
object({
rootfs = optional(string, "")
rootfs_archive_url = optional(string, "")
rootfs_archive_sha256 = optional(string, "")
users = optional(map(list(string)), {})

compose = optional(object({
primary = optional(string, "")
ingress_port = optional(number, 80)
ingress = optional(object({
letsencrypt = optional(bool, false)
bot_mitigation = optional(bool, false)
mode = optional(string, "")
domain = optional(string, "")
acme_email = optional(string, "")
trusted_ips = optional(list(string), [])
max_upload_size = optional(string, "")
upload_timeout = optional(string, "")
}), {})
repo = optional(string, "")
branch = optional(string, "main")
projects = optional(map(object({
docker_compose_repo = string
docker_compose_branch = optional(string)
project_dir = optional(string)
compose_project_name = optional(string)
ingress_port = optional(number)
ingress = optional(object({
letsencrypt = optional(bool)
bot_mitigation = optional(bool)
mode = optional(string)
domain = optional(string)
acme_email = optional(string)
trusted_ips = optional(list(string))
max_upload_size = optional(string)
upload_timeout = optional(string)
}), {})
sitectl_context_name = optional(string)
sitectl_plugin = optional(string)
sitectl_environment = optional(string)
sitectl_packages = optional(list(string), [])
sitectl_verify_args = optional(list(string), [])
docker_compose_init = optional(list(string))
docker_compose_up = optional(list(string))
docker_compose_down = optional(list(string))
docker_compose_rollout = optional(list(string))
})), {})
init = optional(list(string), [
"sitectl config set-context \"$${SITECTL_CONTEXT_NAME}\" --type local --project-dir \"$${DOCKER_COMPOSE_DIR}\" --site \"$${CLOUD_COMPOSE_INSTANCE_NAME}\" --plugin \"$${SITECTL_PLUGIN}\" --environment \"$${SITECTL_ENVIRONMENT}\" --project-name \"$${CLOUD_COMPOSE_INSTANCE_NAME}\" --compose-project-name \"$${COMPOSE_PROJECT_NAME}\" --docker-socket /var/run/docker.sock --env-file .env --default"
])
up = optional(list(string), [
"sitectl compose --context \"$${SITECTL_CONTEXT_NAME}\" up -d --remove-orphans",
"sitectl healthcheck --context \"$${SITECTL_CONTEXT_NAME}\" --persist --timeout \"$${SITECTL_HEALTHCHECK_TIMEOUT}\" --interval \"$${SITECTL_HEALTHCHECK_INTERVAL}\"",
"if [ \"$${SITECTL_ENVIRONMENT}\" != \"production\" ]; then sitectl verify --context \"$${SITECTL_CONTEXT_NAME}\" $${SITECTL_VERIFY_ARGS:-}; fi"
])
down = optional(list(string), [
"sitectl compose --context \"$${SITECTL_CONTEXT_NAME}\" down"
])
rollout = optional(list(string), [
"TARGET_REF=\"$${GIT_REF:-$${GIT_BRANCH:-$${DOCKER_COMPOSE_BRANCH:-main}}}\"",
"if [ -x ./scripts/rollout.sh ]; then ./scripts/rollout.sh; else sitectl deploy --context \"$${SITECTL_CONTEXT_NAME}\" --branch \"$TARGET_REF\"; fi",
"sitectl healthcheck --context \"$${SITECTL_CONTEXT_NAME}\" --persist --timeout \"$${SITECTL_HEALTHCHECK_TIMEOUT}\" --interval \"$${SITECTL_HEALTHCHECK_INTERVAL}\"",
"if [ \"$${SITECTL_ENVIRONMENT}\" != \"production\" ]; then sitectl verify --context \"$${SITECTL_CONTEXT_NAME}\" $${SITECTL_VERIFY_ARGS:-}; fi"
])
}), {})

sitectl = optional(object({
packages = optional(list(string), ["sitectl"])
version = optional(string, "latest")
context_name = optional(string, "")
plugin = optional(string, "core")
environment = optional(string, "production")
healthcheck_timeout = optional(string, "20m")
healthcheck_interval = optional(string, "15s")
verify_args = optional(list(string), [])
}), {})

docker = optional(object({
# renovate: datasource=github-releases depName=docker-compose packageName=docker/compose versioning=semver
compose_version = optional(string, "v5.2.0")
# renovate: datasource=github-releases depName=docker-buildx packageName=docker/buildx versioning=semver
buildx_version = optional(string, "v0.35.0")
}), {})

managed_runtime = optional(object({
enabled = optional(bool, true)
internal_services_enabled = optional(bool, true)
internal_services_auto_update = optional(bool, true)
lightsout_image = optional(string, "ghcr.io/libops/lightsout:main")
cap_image = optional(string, "ghcr.io/libops/cap:main")
cadvisor_image = optional(string, "ghcr.io/google/cadvisor:v0.57.0@sha256:e75bdb03b74b0b6995f208f166fead2e6e555dde73e44200113bb26f41b1981d")
artifacts = optional(list(object({
name = string
url = string
sha256 = string
path = string
mode = optional(string, "0755")
owner = optional(string, "root")
group = optional(string, "root")
restart = optional(string, "")
})), [])
}), {})

vault = optional(object({
addr = optional(string, "")
namespace = optional(string, "")
role = optional(string, "")
agent_enabled = optional(bool, false)
auth_method = optional(string, "gcp-iam")
gcp_auth_mount_path = optional(string, "auth/gcp")
agent_token_path = optional(string, "/mnt/disks/data/vault/token")
agent_additional_config = optional(string, "")
agent_templates = optional(list(object({
destination = string
contents = string
perms = optional(string, "0640")
command = optional(string, "")
})), [])
}), {})

extra_env = optional(map(string), {})
})
| `{}` | no | +| [template](#input\_template) | Optional compose template preset. Supported values are archivesspace, ojs, isle, drupal, wp, omeka-s, and omeka-classic. Explicit runtime settings override preset defaults. | `string` | `""` | no | ## Outputs | Name | Description | |------|-------------| -| [appGsa](#output\_appGsa) | The Google Service Account the app can leverage to auth to other Google services | -| [backend](#output\_backend) | Backend service ID for attaching the Cloud Run ingress to an external HTTPS load balancer. | -| [external\_ip](#output\_external\_ip) | The Google Compute instance external IPv4 address. | -| [instance](#output\_instance) | The Google Compute instance ID, name, zone, data disk, GSA for the instance. | -| [instance\_id](#output\_instance\_id) | The Google Compute instance ID. | -| [internal\_ip](#output\_internal\_ip) | The Google Compute instance internal IPv4 address. | -| [rollout](#output\_rollout) | Optional rollout API endpoint details. The URL is the VPC-internal endpoint. | -| [serviceGsa](#output\_serviceGsa) | The Google Service Account internal services that manage the VM runs as | +| [appGsa](#output\_appGsa) | The Google Service Account the app can use for app-scoped auth. | +| [backend](#output\_backend) | Backend service ID for attaching Cloud Run ingress to an external HTTPS load balancer. | +| [cloud\_provider](#output\_cloud\_provider) | Selected cloud provider. | +| [compose\_projects](#output\_compose\_projects) | Normalized compose project manifest. | +| [external\_ip](#output\_external\_ip) | Selected provider VM public IPv4 address. | +| [instance](#output\_instance) | Selected provider VM instance details. | +| [instance\_id](#output\_instance\_id) | Selected provider VM instance ID. | +| [internal\_ip](#output\_internal\_ip) | Selected provider VM private IPv4 address. | +| [primary\_compose\_project](#output\_primary\_compose\_project) | Normalized primary compose project. | +| [rollout](#output\_rollout) | Optional rollout API endpoint details. | +| [serviceGsa](#output\_serviceGsa) | The Google Service Account internal services run as. | +| [template](#output\_template) | Selected compose template preset. | | [urls](#output\_urls) | Cloud Run ingress URLs by region. | +| [volumes](#output\_volumes) | Selected provider persistent volume details where available. | diff --git a/ci/cloud-smoke.sh b/ci/cloud-smoke.sh new file mode 100755 index 0000000..41952cd --- /dev/null +++ b/ci/cloud-smoke.sh @@ -0,0 +1,885 @@ +#!/usr/bin/env bash + +set -euo pipefail + +repo_root="$(cd "$(dirname "$0")/.." && pwd)" + +usage() { + cat <<'EOF' +Usage: + ci/cloud-smoke.sh all + ci/cloud-smoke.sh -