├── .trivyignore ├── modules ├── controller │ ├── docs │ │ ├── footer.md │ │ └── header.md │ ├── provider.tf │ ├── outputs.tf │ ├── .terraform.lock.hcl │ ├── variables.tf │ ├── tests │ │ └── controller.tftest.hcl │ ├── README.md │ └── main.tf ├── registry │ ├── docs │ │ ├── footer.md │ │ └── header.md │ ├── provider.tf │ ├── outputs.tf │ ├── .terraform.lock.hcl │ ├── variables.tf │ ├── tests │ │ └── registry.tftest.hcl │ ├── main.tf │ └── README.md └── subcluster │ ├── docs │ ├── footer.md │ └── header.md │ ├── provider.tf │ ├── output.tf │ ├── .terraform.lock.hcl │ ├── data_plane.tf │ ├── tests │ ├── external_etcd.tftest.hcl │ ├── ha_deployment.tftest.hcl │ └── base_deploy.tftest.hcl │ ├── variables.tf │ ├── README.md │ └── control_plane.tf ├── .github ├── CODEOWNERS ├── workflows │ ├── pull-request.yaml │ ├── cla_check.yaml │ ├── trivy-update.yaml │ ├── security-scan.yaml │ └── build-and-test.yaml ├── renovate-config.js ├── pull_request_template.md ├── renovate.json └── actions │ └── setup-trivy │ └── action.yaml ├── scripts ├── generate-docs.sh └── compare_kev_vulnerabilities.sh ├── trivy.yaml ├── provider.tf ├── outputs.tf ├── .terraform.lock.hcl ├── docs ├── header.md └── footer.md ├── .terraform-docs.yml ├── .gitignore ├── tests ├── base_deployment.tftest.hcl └── registry.tftest.hcl ├── SECURITY.md ├── variables.tf ├── main.tf ├── README.md └── LICENSE /.trivyignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /modules/controller/docs/footer.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /modules/registry/docs/footer.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /modules/subcluster/docs/footer.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # The Anbox team owns all for now 2 | * @canonical/anbox 3 | -------------------------------------------------------------------------------- /scripts/generate-docs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | terraform-docs -c .terraform-docs.yml . --output-file README.md 4 | -------------------------------------------------------------------------------- /trivy.yaml: -------------------------------------------------------------------------------- 1 | format: table 2 | exit-code: 1 3 | severity: 4 | - MEDIUM 5 | - HIGH 6 | - CRITICAL 7 | scan: 8 | scanners: 9 | - vuln 10 | - secret 11 | - misconfig 12 | -------------------------------------------------------------------------------- /provider.tf: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2025 Canonical Ltd. All rights reserved. 3 | // 4 | 5 | terraform { 6 | required_providers { 7 | juju = { 8 | version = "~> 0.19.0" 9 | source = "juju/juju" 10 | } 11 | } 12 | required_version = "~> 1.6" 13 | } 14 | 15 | provider "juju" {} 16 | -------------------------------------------------------------------------------- /outputs.tf: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2025 Canonical Ltd. All rights reserved. 3 | # 4 | 5 | output "anbox_models" { 6 | value = compact(concat(values(module.subcluster)[*].model_name, [module.controller.model_name, one(module.registry[*].model_name)])) 7 | description = "Name of the models created for Anbox Cloud" 8 | } 9 | -------------------------------------------------------------------------------- /.github/workflows/pull-request.yaml: -------------------------------------------------------------------------------- 1 | name: Pull Request 2 | on: 3 | pull_request: 4 | paths-ignore: 5 | - ".gitignore" 6 | 7 | concurrency: 8 | group: ${{ github.workflow }}-${{ github.ref }} 9 | cancel-in-progress: true 10 | 11 | jobs: 12 | test: 13 | uses: ./.github/workflows/build-and-test.yaml 14 | -------------------------------------------------------------------------------- /.github/renovate-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | branchPrefix: "renovate/", 3 | dryRun: null, 4 | username: "renovate-release", 5 | gitAuthor: "Renovate Bot ", 6 | onboarding: true, 7 | platform: "github", 8 | includeForks: true, 9 | repositories: ["canonical/anbox-cloud-terraform"], 10 | } 11 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Done 2 | 3 | [Summary of work items] 4 | 5 | ## QA 6 | 7 | [Steps for testing the changes] 8 | 9 | ## JIRA / Launchpad bug 10 | 11 | Fixes # 12 | 13 | ## Documentation 14 | 15 | Does this impact the team's internal or public documentation? If yes, has the relevant documentation been updated? 16 | -------------------------------------------------------------------------------- /.github/workflows/cla_check.yaml: -------------------------------------------------------------------------------- 1 | name: CLA check 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | 7 | jobs: 8 | cla-check: 9 | runs-on: ubuntu-24.04 10 | steps: 11 | - name: Check if Canonical's Contributor License Agreement has been signed 12 | uses: canonical/has-signed-canonical-cla@5d1443b94417bd150ad234a82fe21f7340a25e4d # v2 13 | -------------------------------------------------------------------------------- /modules/registry/docs/header.md: -------------------------------------------------------------------------------- 1 | # Anbox Registry 2 | 3 | This is a terraform module to deploy anbox registry model using juju and terraform. 4 | The module uses `terraform-provider-juju` to deploy the anbox charm to a 5 | juju model. 6 | 7 | ### Some features of the deployment 8 | 9 | * The module deploys an Anbox Registry for anbox cloud. The model currently 10 | includes: 11 | - AAR (Anbox Application Registry) 12 | 13 | -------------------------------------------------------------------------------- /modules/controller/provider.tf: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2025 Canonical Ltd. All rights reserved. 3 | // 4 | 5 | terraform { 6 | required_providers { 7 | juju = { 8 | version = "~> 0.19.0" 9 | source = "juju/juju" 10 | } 11 | } 12 | required_version = "~> 1.6" 13 | } 14 | 15 | locals { 16 | base = "ubuntu@22.04" 17 | _channel_split = split("/", var.channel) 18 | risk = element(local._channel_split, length(local._channel_split) - 1) 19 | } 20 | 21 | -------------------------------------------------------------------------------- /modules/registry/provider.tf: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2025 Canonical Ltd. All rights reserved. 3 | // 4 | 5 | terraform { 6 | required_providers { 7 | juju = { 8 | version = "~> 0.19.0" 9 | source = "juju/juju" 10 | } 11 | } 12 | required_version = "~> 1.6" 13 | } 14 | 15 | locals { 16 | base = "ubuntu@22.04" 17 | _channel_split = split("/", var.channel) 18 | risk = element(local._channel_split, length(local._channel_split) - 1) 19 | } 20 | 21 | -------------------------------------------------------------------------------- /modules/subcluster/provider.tf: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2025 Canonical Ltd. All rights reserved. 3 | // 4 | 5 | terraform { 6 | required_providers { 7 | juju = { 8 | version = "~> 0.19.0" 9 | source = "juju/juju" 10 | } 11 | } 12 | required_version = "~> 1.6" 13 | } 14 | 15 | locals { 16 | base = "ubuntu@22.04" 17 | _channel_split = split("/", var.channel) 18 | risk = element(local._channel_split, length(local._channel_split) - 1) 19 | } 20 | 21 | -------------------------------------------------------------------------------- /modules/controller/docs/header.md: -------------------------------------------------------------------------------- 1 | # Anbox Cloud Terraform 2 | 3 | This is a terraform module to deploy anbox controller model using juju and terraform. 4 | The module uses `terraform-provider-juju` to deploy the anbox charm to a 5 | juju model. 6 | 7 | ### Some features of the deployment 8 | 9 | * The module deploys a control plane for anbox cloud. The control plane currently 10 | includes: 11 | - NATS 12 | - Anbox Cloud Gateway 13 | - Certificate Authority (CA: self-signed-certificates) 14 | - Anbox Cloud Dashboard 15 | 16 | -------------------------------------------------------------------------------- /modules/subcluster/output.tf: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2025 Canonical Ltd. All rights reserved. 3 | # 4 | 5 | output "ams_offer_url" { 6 | value = juju_offer.ams_offer.url 7 | description = "Juju offer url for connecting to the AMS charm." 8 | } 9 | 10 | output "model_name" { 11 | value = juju_model.subcluster.name 12 | description = "Model name for the deployed subcluster." 13 | } 14 | 15 | output "agent_app_name" { 16 | value = juju_application.agent.name 17 | description = "Anbox Stream Agent application name deployed in the subcluster model." 18 | } 19 | -------------------------------------------------------------------------------- /modules/controller/outputs.tf: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2025 Canonical Ltd. All rights reserved. 3 | # 4 | 5 | output "nats_offer_url" { 6 | value = juju_offer.nats_offer.url 7 | description = "Juju offer url for connecting to the NATS charm." 8 | } 9 | 10 | output "model_name" { 11 | value = juju_model.controller.name 12 | description = "Model name for the deployed controller." 13 | } 14 | 15 | output "dashboard_app_name" { 16 | value = juju_application.dashboard.name 17 | description = "Anbox Cloud Dashboard application name deployed in the controller model." 18 | } 19 | -------------------------------------------------------------------------------- /modules/registry/outputs.tf: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2025 Canonical Ltd. All rights reserved. 3 | # 4 | 5 | output "client_offer_url" { 6 | value = juju_offer.client_offer.url 7 | description = "Juju Offer URL for connecting to AAR in `client` mode." 8 | } 9 | 10 | output "publisher_offer_url" { 11 | value = juju_offer.publisher_offer.url 12 | description = "Juju Offer URL for connecting to AAR in `publisher` mode." 13 | } 14 | 15 | output "model_name" { 16 | value = juju_model.registry.name 17 | description = "Model name created for Anbox Application Registry" 18 | } 19 | -------------------------------------------------------------------------------- /scripts/compare_kev_vulnerabilities.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copyright 2025 Canonical Ltd. All rights reserved. 4 | 5 | trivy fs "$GITHUB_WORKSPACE" --format json --output trivy-full-report.json 6 | 7 | kev_cves="$(jq -r '.vulnerabilities[].cveID' kev.json | sort -u)" 8 | 9 | found_cves="$(jq -r '.Results[] | select(.Vulnerabilities != null) | .Vulnerabilities[].VulnerabilityID' trivy-full-report.json | sort -u)" 10 | 11 | matches="$(echo "$found_cves" | grep -F -f <(echo "$kev_cves") || true)" 12 | 13 | if [ -n "$matches" ]; then 14 | echo "KEV listed vulnerabilities found." 15 | echo "$matches" 16 | exit 1 17 | fi 18 | 19 | echo "No KEV listed vulnerabilities found." -------------------------------------------------------------------------------- /.github/workflows/trivy-update.yaml: -------------------------------------------------------------------------------- 1 | name: Update Trivy cache 2 | on: 3 | workflow_dispatch: 4 | # Run daily after midnight UTC 5 | schedule: 6 | - cron: '0 1 * * *' 7 | 8 | jobs: 9 | trivy-update: 10 | strategy: 11 | fail-fast: false 12 | matrix: 13 | arch: [arm64, amd64] 14 | runs-on: [self-hosted, linux, "${{ matrix.arch == 'amd64' && 'X64' || 'ARM64' }}", jammy, large] 15 | steps: 16 | - name: Check out repository 17 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 18 | with: 19 | fetch-depth: 0 20 | - name: Setup Trivy to warm up the cache 21 | uses: ./.github/actions/setup-trivy 22 | with: 23 | arch: ${{ matrix.arch }} 24 | -------------------------------------------------------------------------------- /modules/registry/.terraform.lock.hcl: -------------------------------------------------------------------------------- 1 | # This file is maintained automatically by "terraform init". 2 | # Manual edits may be lost in future updates. 3 | 4 | provider "registry.terraform.io/juju/juju" { 5 | version = "0.19.0" 6 | constraints = "~> 0.19.0" 7 | hashes = [ 8 | "h1:QzSjUIELE2QEsZCYrOSMrCopgasqwJpmzjgsc++igrM=", 9 | "zh:1d5c0b2671052bbc4600dae6b07bd5d0ca0ca492e2b8c408e3358518cca419dd", 10 | "zh:26b8f4e409d22ab21ed4a9d192a8d40f3887bf0ac1865b427102327ccec30502", 11 | "zh:3e52068e40067ab8f68ee12a56e733fe73180050c0e9644da87ded6a1eab0d1a", 12 | "zh:577ac4ca9e6bb6d79c209d85c3ea5f4271349443200acdd6852ff8748a13e99a", 13 | "zh:753ad16d007180a77a147bd377de2fb334f409123f6fee36d4c50c7fe8b76a29", 14 | "zh:d62a0e40e9010712c993c39ade6d051d33d6f3e584bfefc2f7041abefcadbcef", 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /modules/controller/.terraform.lock.hcl: -------------------------------------------------------------------------------- 1 | # This file is maintained automatically by "terraform init". 2 | # Manual edits may be lost in future updates. 3 | 4 | provider "registry.terraform.io/juju/juju" { 5 | version = "0.19.0" 6 | constraints = "~> 0.19.0" 7 | hashes = [ 8 | "h1:QzSjUIELE2QEsZCYrOSMrCopgasqwJpmzjgsc++igrM=", 9 | "zh:1d5c0b2671052bbc4600dae6b07bd5d0ca0ca492e2b8c408e3358518cca419dd", 10 | "zh:26b8f4e409d22ab21ed4a9d192a8d40f3887bf0ac1865b427102327ccec30502", 11 | "zh:3e52068e40067ab8f68ee12a56e733fe73180050c0e9644da87ded6a1eab0d1a", 12 | "zh:577ac4ca9e6bb6d79c209d85c3ea5f4271349443200acdd6852ff8748a13e99a", 13 | "zh:753ad16d007180a77a147bd377de2fb334f409123f6fee36d4c50c7fe8b76a29", 14 | "zh:d62a0e40e9010712c993c39ade6d051d33d6f3e584bfefc2f7041abefcadbcef", 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /modules/subcluster/.terraform.lock.hcl: -------------------------------------------------------------------------------- 1 | # This file is maintained automatically by "terraform init". 2 | # Manual edits may be lost in future updates. 3 | 4 | provider "registry.terraform.io/juju/juju" { 5 | version = "0.19.0" 6 | constraints = "~> 0.19.0" 7 | hashes = [ 8 | "h1:QzSjUIELE2QEsZCYrOSMrCopgasqwJpmzjgsc++igrM=", 9 | "zh:1d5c0b2671052bbc4600dae6b07bd5d0ca0ca492e2b8c408e3358518cca419dd", 10 | "zh:26b8f4e409d22ab21ed4a9d192a8d40f3887bf0ac1865b427102327ccec30502", 11 | "zh:3e52068e40067ab8f68ee12a56e733fe73180050c0e9644da87ded6a1eab0d1a", 12 | "zh:577ac4ca9e6bb6d79c209d85c3ea5f4271349443200acdd6852ff8748a13e99a", 13 | "zh:753ad16d007180a77a147bd377de2fb334f409123f6fee36d4c50c7fe8b76a29", 14 | "zh:d62a0e40e9010712c993c39ade6d051d33d6f3e584bfefc2f7041abefcadbcef", 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /modules/subcluster/docs/header.md: -------------------------------------------------------------------------------- 1 | # Anbox Cloud Subcluster 2 | 3 | This is a terraform module to deploy anbox cloud subcluster using juju and terraform. 4 | The module uses `terraform-provider-juju` to deploy the anbox charm to a 5 | juju model. 6 | 7 | ### Some features of the deployment 8 | 9 | * The module logically divides resources into a control plane and a data plane. 10 | * The control plane currently includes: 11 | - AMS 12 | - ETCD (optional) 13 | - Self-signed-certificates (optional) 14 | - Anbox Stream Agent 15 | - Coturn 16 | * The data plan includes: 17 | - LXD 18 | * This module can deploy a number of LXD machines to act as nodes to AMS using the 19 | input variable `var.lxd_nodes`. 20 | * Each LXD node is accompanied by a subordinate charm `ams-node-controller` to 21 | setup network rules properly on the lxd node. 22 | 23 | -------------------------------------------------------------------------------- /.terraform.lock.hcl: -------------------------------------------------------------------------------- 1 | # This file is maintained automatically by "terraform init". 2 | # Manual edits may be lost in future updates. 3 | 4 | provider "registry.terraform.io/juju/juju" { 5 | version = "0.19.0" 6 | constraints = "~> 0.19.0" 7 | hashes = [ 8 | "h1:QzSjUIELE2QEsZCYrOSMrCopgasqwJpmzjgsc++igrM=", 9 | "h1:sWo4pFb0CYEqLFX2OF/ObAB8vZFZUgL/lceamTLyHsc=", 10 | "zh:1d5c0b2671052bbc4600dae6b07bd5d0ca0ca492e2b8c408e3358518cca419dd", 11 | "zh:26b8f4e409d22ab21ed4a9d192a8d40f3887bf0ac1865b427102327ccec30502", 12 | "zh:3e52068e40067ab8f68ee12a56e733fe73180050c0e9644da87ded6a1eab0d1a", 13 | "zh:577ac4ca9e6bb6d79c209d85c3ea5f4271349443200acdd6852ff8748a13e99a", 14 | "zh:753ad16d007180a77a147bd377de2fb334f409123f6fee36d4c50c7fe8b76a29", 15 | "zh:d62a0e40e9010712c993c39ade6d051d33d6f3e584bfefc2f7041abefcadbcef", 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /docs/header.md: -------------------------------------------------------------------------------- 1 | # Anbox Cloud Terraform 2 | 3 | > [!WARNING] 4 | > This terraform plan is a work in progress and makes use of [terraform-provider-juju](https://github.com/juju/terraform-provider-juju) 5 | > which is in active development too. Please expect breaking changes (if required) in the future for the plan and the module. 6 | 7 | 8 | This is a terraform plan to deploy anbox cloud using juju and terraform. 9 | The module uses `terraform-provider-juju` to deploy the anbox bundles to a 10 | bootstrapped juju cluster. 11 | 12 | This plan uses terraform modules to deploy an [anbox subcluster](./modules/subcluster/README.md) 13 | and a [control plane](./modules/controller/README.md) for Anbox Cloud into two separate juju models. 14 | The subclusters can be scaled up and down according to the requirements. The two terraform modules expose 15 | the required attributes as outputs to be used to connect apps across the two juju models using 16 | cross model relations. 17 | 18 | -------------------------------------------------------------------------------- /.terraform-docs.yml: -------------------------------------------------------------------------------- 1 | formatter: "md" # this is required 2 | 3 | version: "" 4 | 5 | header-from: docs/header.md 6 | footer-from: docs/footer.md 7 | 8 | recursive: 9 | enabled: true 10 | path: modules 11 | 12 | sections: 13 | hide: [] 14 | show: [] 15 | 16 | hide-all: false # deprecated in v0.13.0, removed in v0.15.0 17 | show-all: true # deprecated in v0.13.0, removed in v0.15.0 18 | 19 | content: "" 20 | 21 | output: 22 | file: README.md 23 | mode: inject 24 | template: |- 25 | 26 | {{ .Content }} 27 | 28 | 29 | output-values: 30 | enabled: false 31 | from: "" 32 | 33 | sort: 34 | enabled: true 35 | by: name 36 | 37 | settings: 38 | anchor: true 39 | color: true 40 | default: true 41 | description: false 42 | escape: true 43 | hide-empty: false 44 | html: true 45 | indent: 2 46 | lockfile: true 47 | read-comments: true 48 | required: true 49 | sensitive: true 50 | type: true 51 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base", 5 | ":disableDependencyDashboard", 6 | ":automergeDigest", 7 | ":automergePatch", 8 | ":automergeMinor", 9 | ":rebaseStalePrs", 10 | ":semanticCommits", 11 | ":semanticCommitScope(deps)", 12 | "docker:pinDigests", 13 | "helpers:pinGitHubActionDigests", 14 | "regexManagers:dockerfileVersions" 15 | ], 16 | "automergeType": "branch", 17 | "packageRules": [ 18 | { 19 | "groupName": "github actions", 20 | "matchManagers": ["github-actions"], 21 | "automerge": true, 22 | "schedule": ["on monday"] 23 | }, 24 | { 25 | "groupName": "renovate packages", 26 | "matchSourceUrlPrefixes": ["https://github.com/renovatebot/"], 27 | "matchUpdateTypes": ["major", "minor", "patch", "pin", "digest"], 28 | "automerge": true, 29 | "schedule": ["on monday"] 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Local .terraform directories 2 | **/.terraform/* 3 | *tfplan* 4 | 5 | # .tfstate files 6 | *.tfstate 7 | *.tfstate.* 8 | 9 | # Crash log files 10 | crash.log 11 | crash.*.log 12 | 13 | # Exclude all .tfvars files, which are likely to contain sensitive data, such as 14 | # password, private keys, and other secrets. These should not be part of version 15 | # control as they are data points which are potentially sensitive and subject 16 | # to change depending on the environment. 17 | *.tfvars 18 | *.tfvars.json 19 | 20 | # Ignore override files as they are usually used to override resources locally and so 21 | # are not checked in 22 | override.tf 23 | override.tf.json 24 | *_override.tf 25 | *_override.tf.json 26 | 27 | # Include override files you do wish to add to version control using negated pattern 28 | # !example_override.tf 29 | 30 | # Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan 31 | # example: *tfplan* 32 | 33 | # Ignore CLI configuration files 34 | .terraformrc 35 | terraform.rc 36 | -------------------------------------------------------------------------------- /modules/registry/variables.tf: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2025 Canonical Ltd. All rights reserved. 3 | // 4 | 5 | variable "channel" { 6 | description = "Channel for the deployed charm" 7 | type = string 8 | default = "latest/stable" 9 | } 10 | 11 | variable "constraints" { 12 | description = "List of constraints that need to be applied to applications. Each constraint must be of format `=`" 13 | type = list(string) 14 | default = [] 15 | } 16 | 17 | variable "enable_ha" { 18 | description = "Number of lxd nodes to deploy per subcluster" 19 | type = bool 20 | default = false 21 | } 22 | 23 | variable "ssh_public_key" { 24 | description = "SSH key to be imported in the juju models. No key is imported by default." 25 | type = string 26 | default = "" 27 | } 28 | 29 | variable "ubuntu_pro_token" { 30 | description = "Ubuntu Advantage token that is received with your license of Anbox Cloud." 31 | type = string 32 | default = "" 33 | } 34 | -------------------------------------------------------------------------------- /modules/controller/variables.tf: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2025 Canonical Ltd. All rights reserved. 3 | // 4 | 5 | variable "channel" { 6 | description = "Channel for the deployed charm" 7 | type = string 8 | default = "latest/stable" 9 | } 10 | 11 | variable "constraints" { 12 | description = "List of constraints that need to be applied to applications. Each constraint must be of format `=`" 13 | type = list(string) 14 | default = [] 15 | } 16 | 17 | variable "enable_ha" { 18 | description = "Number of lxd nodes to deploy per subcluster" 19 | type = bool 20 | default = false 21 | } 22 | 23 | variable "enable_cos" { 24 | description = "Enable cos integration by deploying grafana-agent charm." 25 | type = bool 26 | default = false 27 | } 28 | 29 | variable "ssh_public_key" { 30 | description = "SSH key to be imported in the juju models. No key is imported by default." 31 | type = string 32 | default = "" 33 | } 34 | 35 | variable "ubuntu_pro_token" { 36 | description = "Ubuntu Advantage token that is received with your license of Anbox Cloud." 37 | type = string 38 | default = "" 39 | } 40 | -------------------------------------------------------------------------------- /tests/base_deployment.tftest.hcl: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2025 Canonical Ltd. All rights reserved. 3 | // 4 | 5 | run "test_required_variables" { 6 | command = plan 7 | variables { 8 | anbox_channel = "" 9 | subclusters = [] 10 | } 11 | expect_failures = [var.anbox_channel, var.subclusters] 12 | } 13 | 14 | run "test_models_per_subcluster" { 15 | command = plan 16 | variables { 17 | anbox_channel = "1.27/stable" 18 | subclusters = [{ 19 | name = "a" 20 | lxd_node_count = 1 21 | }, { 22 | name = "b" 23 | lxd_node_count = 1 24 | }, { 25 | name = "c" 26 | lxd_node_count = 1 27 | }] 28 | } 29 | 30 | assert { 31 | condition = length(module.subcluster) == 3 32 | error_message = "A subcluster should be created per label." 33 | } 34 | 35 | assert { 36 | condition = length(juju_integration.agent_nats_cmr) == 3 37 | error_message = "Every agent in subcluster should be connected to controller NATS." 38 | } 39 | 40 | assert { 41 | condition = length(juju_integration.dashboard_ams_cmr) == 3 42 | error_message = "Every AMS in subcluster should be connected to controller Dashboard." 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/registry.tftest.hcl: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2025 Canonical Ltd. All rights reserved. 3 | // 4 | 5 | run "test_registry" { 6 | command = plan 7 | variables { 8 | anbox_channel = "1.27/stable" 9 | subclusters = [{ 10 | name = "a" 11 | lxd_node_count = 1 12 | registry = { 13 | mode = "client" 14 | } 15 | }] 16 | deploy_registry = true 17 | } 18 | 19 | assert { 20 | condition = length(module.subcluster) == 1 21 | error_message = "A subcluster should be created per label." 22 | } 23 | 24 | assert { 25 | condition = length(module.registry) == 1 26 | error_message = "Registry should be deployed" 27 | } 28 | 29 | assert { 30 | condition = local.subcluster_config_map["a"].registry_config.mode == "client" 31 | error_message = "Registry should be deployed" 32 | } 33 | 34 | assert { 35 | condition = length(juju_integration.agent_nats_cmr) == 1 36 | error_message = "Every agent in subcluster should be connected to controller NATS." 37 | } 38 | 39 | assert { 40 | condition = length(juju_integration.dashboard_ams_cmr) == 1 41 | error_message = "Every AMS in subcluster should be connected to controller Dashboard." 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /modules/registry/tests/registry.tftest.hcl: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2025 Canonical Ltd. All rights reserved. 3 | // 4 | 5 | variables { 6 | ubuntu_pro_token = "token" 7 | channel = "1.27/stable" 8 | constraints = [""] 9 | ssh_public_key = "ssh-rsa key a@b" 10 | } 11 | 12 | run "test_base_controller_resources" { 13 | command = plan 14 | assert { 15 | condition = length(juju_model.registry) > 0 16 | error_message = "Model not created in controller." 17 | } 18 | assert { 19 | condition = length(juju_application.aar) > 0 20 | error_message = "AAR should be deployed." 21 | } 22 | assert { 23 | condition = length(juju_ssh_key.this) > 0 24 | error_message = "SSH Key not imported in the model." 25 | } 26 | } 27 | 28 | run "test_ha_deployment" { 29 | command = plan 30 | variables { 31 | enable_ha = true 32 | } 33 | assert { 34 | condition = length(juju_model.registry) > 0 35 | error_message = "Model not created in controller." 36 | } 37 | assert { 38 | condition = length(juju_application.aar) > 0 39 | error_message = "AAR should be deployed." 40 | } 41 | assert { 42 | condition = juju_application.aar.units == 3 43 | error_message = "AAR should be have 3 units in HA mode." 44 | } 45 | } 46 | 47 | -------------------------------------------------------------------------------- /modules/subcluster/data_plane.tf: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2025 Canonical Ltd. All rights reserved. 3 | // 4 | 5 | resource "juju_application" "lxd" { 6 | name = "lxd" 7 | 8 | model = juju_model.subcluster.name 9 | constraints = join(" ", concat(var.constraints, ["root-disk=10240M"])) 10 | 11 | charm { 12 | name = "ams-lxd" 13 | channel = var.channel 14 | base = local.base 15 | } 16 | 17 | machines = juju_machine.lxd_node[*].machine_id 18 | // FIXME: Currently the provider has some issues with reconciling state using 19 | // the response from the JUJU APIs. This is done just to ignore the changes in 20 | // string values returned. 21 | lifecycle { 22 | ignore_changes = [constraints] 23 | } 24 | } 25 | 26 | resource "juju_integration" "ams_lxd" { 27 | model = juju_model.subcluster.name 28 | 29 | application { 30 | name = juju_application.ams.name 31 | endpoint = "lxd" 32 | } 33 | 34 | application { 35 | name = juju_application.lxd.name 36 | endpoint = "api" 37 | } 38 | } 39 | 40 | resource "juju_machine" "lxd_node" { 41 | model = juju_model.subcluster.name 42 | count = var.lxd_nodes 43 | base = local.base 44 | name = "lxd-${count.index}" 45 | constraints = join(" ", var.constraints) 46 | } 47 | -------------------------------------------------------------------------------- /modules/subcluster/tests/external_etcd.tftest.hcl: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2025 Canonical Ltd. All rights reserved. 3 | // 4 | 5 | run "test_external_etcd_enabled" { 6 | command = plan 7 | variables { 8 | model_suffix = "test-model" 9 | external_etcd = true 10 | } 11 | 12 | assert { 13 | condition = length(juju_machine.db_node) == 1 14 | error_message = "Separate machine not created for etcd." 15 | } 16 | assert { 17 | condition = length(juju_application.etcd) == 1 18 | error_message = "ETCD not deployed." 19 | } 20 | assert { 21 | condition = length(juju_integration.ams_db) == 1 22 | error_message = "AMS not related to ETCD." 23 | } 24 | assert { 25 | condition = length(juju_application.etcd_ca) == 1 26 | error_message = "Separate CA for etcd not deployed." 27 | } 28 | assert { 29 | condition = length(juju_integration.etcd_ca) == 1 30 | error_message = "ETCD not related to CA." 31 | } 32 | assert { 33 | condition = juju_application.etcd[0].charm[0].name == "etcd" 34 | error_message = "`etcd` charm should be used to deploy ETCD." 35 | } 36 | assert { 37 | condition = juju_application.etcd[0].config == tomap({ channel = "3.4/stable" }) 38 | error_message = "`etcd` charm should be deployed from `3.4/stable` channel by default." 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Anbox Cloud security policy 2 | 3 | Learn about our [release and support policy](https://documentation.ubuntu.com/anbox-cloud/en/latest/reference/release-notes/release-notes/#release-and-support-policy) for the nature of our releases and versions. 4 | 5 | ## Reporting a vulnerability 6 | 7 | If you discover a security vulnerability, follow the steps outlined below to report it: 8 | 9 | 1. Do not publicly disclose the vulnerability before discussing it with us. 10 | 2. Report a bug at https://bugs.launchpad.net/anbox-cloud 11 | 12 | **Important**: Remember to set the information type to *Private Security*. You will see a field with the text *This bug contains information that is:* 13 | 3. Provide detailed information about the vulnerability, including: 14 | - A description of the vulnerability 15 | - Steps to reproduce the issue 16 | - Potential impact and affected versions 17 | - Suggested mitigation, if possible 18 | 19 | The [Ubuntu Security disclosure and embargo policy](https://ubuntu.com/security/disclosure-policy) contains more information about what you can expect when you contact us and what we expect from you. 20 | 21 | The Anbox Cloud team will be notified of the issue and review the vulnerability. We may reach out to you for further information or clarification if needed. 22 | If the issue is confirmed as a valid security vulnerability, we will assign a CVE and coordinate the release of the fix. We also document them as [security notices](https://documentation.ubuntu.com/anbox-cloud/en/latest/reference/security-notices/). 23 | -------------------------------------------------------------------------------- /modules/registry/main.tf: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2025 Canonical Ltd. All rights reserved. 3 | // 4 | 5 | locals { 6 | controller_model_name = "anbox-registry" 7 | num_units = var.enable_ha ? 3 : 1 8 | } 9 | 10 | resource "juju_model" "registry" { 11 | name = local.controller_model_name 12 | 13 | constraints = join(" ", var.constraints) 14 | 15 | config = { 16 | logging-config = "=INFO" 17 | update-status-hook-interval = "5m" 18 | } 19 | } 20 | 21 | resource "juju_ssh_key" "this" { 22 | count = length(var.ssh_public_key) > 0 ? 1 : 0 23 | model = juju_model.registry.name 24 | payload = trim(var.ssh_public_key, "\n") 25 | } 26 | 27 | resource "juju_application" "aar" { 28 | name = "aar" 29 | 30 | model = juju_model.registry.name 31 | constraints = join(" ", var.constraints) 32 | 33 | charm { 34 | name = "aar" 35 | channel = var.channel 36 | base = local.base 37 | } 38 | 39 | units = local.num_units 40 | 41 | config = { 42 | snap_risk_level = local.risk 43 | ua_token = var.ubuntu_pro_token 44 | } 45 | 46 | // FIXME: Currently the provider has some issues with reconciling state using 47 | // the response from the JUJU APIs. This is done just to ignore the changes in 48 | // string values returned. 49 | lifecycle { 50 | ignore_changes = [constraints] 51 | } 52 | } 53 | 54 | resource "juju_offer" "client_offer" { 55 | model = juju_model.registry.name 56 | application_name = juju_application.aar.name 57 | endpoint = "client" 58 | name = "aar-client" 59 | } 60 | 61 | resource "juju_offer" "publisher_offer" { 62 | model = juju_model.registry.name 63 | application_name = juju_application.aar.name 64 | name = "aar-publisher" 65 | endpoint = "publisher" 66 | } 67 | -------------------------------------------------------------------------------- /.github/workflows/security-scan.yaml: -------------------------------------------------------------------------------- 1 | name: Run security scan 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | 8 | concurrency: 9 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 10 | cancel-in-progress: true 11 | 12 | jobs: 13 | build: 14 | name: Run triviy security scan 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 18 | - name: Setup Trivy 19 | uses: ./.github/actions/setup-trivy 20 | - name: Run Trivy vulnerability scanner 21 | run: | 22 | trivy repository "$GITHUB_WORKSPACE" \ 23 | -c trivy.yaml \ 24 | --ignorefile .trivyignore \ 25 | --show-suppressed \ 26 | --cache-dir="$GITHUB_WORKSPACE"/.cache/trivy 27 | 28 | - name: Setup Terraform 29 | run: 30 | sudo snap install terraform --channel latest/stable --classic 31 | - name: Setup operator environment 32 | uses: charmed-kubernetes/actions-operator@1c7c9a30d7d233e26e7a4fc1505cc44bbd937229 33 | with: 34 | provider: lxd 35 | - name: Create terraform Plan 36 | run: | 37 | cat < default.auto.tfvars 38 | anbox_channel = "1.27/stable" 39 | subclusters = [ 40 | { 41 | name = "a" 42 | lxd_node_count = 1 43 | registry = { 44 | mode = "client" 45 | } 46 | } 47 | ] 48 | deploy_registry=true 49 | EOF 50 | terraform init && terraform plan -out tfplan -var-file=default.auto.tfvars 51 | - name: Run Trivy Terraform Plan Scanner 52 | run: | 53 | trivy config tfplan 54 | - name: Compare Trivy results with KEV list 55 | run: bash ./scripts/compare_kev_vulnerabilities.sh 56 | -------------------------------------------------------------------------------- /modules/subcluster/variables.tf: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2025 Canonical Ltd. All rights reserved. 3 | // 4 | 5 | variable "model_suffix" { 6 | type = string 7 | description = "Suffix to attach for model" 8 | } 9 | 10 | variable "channel" { 11 | description = "Channel for the deployed charm" 12 | type = string 13 | default = "latest/stable" 14 | } 15 | 16 | variable "external_etcd" { 17 | description = "Channel for the deployed charm" 18 | type = bool 19 | default = false 20 | } 21 | 22 | variable "lxd_nodes" { 23 | description = "Channel for the deployed charm" 24 | type = number 25 | default = 1 26 | } 27 | 28 | variable "constraints" { 29 | description = "List of constraints that need to be applied to applications. Each constraint must be of format `=`" 30 | type = list(string) 31 | default = [] 32 | } 33 | 34 | variable "enable_ha" { 35 | description = "Number of lxd nodes to deploy per subcluster" 36 | type = bool 37 | default = false 38 | } 39 | 40 | variable "registry_config" { 41 | description = "Object to represent connection details for connecting to anbox registry" 42 | type = object({ 43 | mode = string 44 | offer_url = string 45 | }) 46 | default = null 47 | } 48 | 49 | variable "enable_cos" { 50 | description = "Enable cos integration by deploying grafana-agent charm." 51 | type = bool 52 | default = false 53 | } 54 | 55 | variable "ssh_public_key" { 56 | description = "SSH key to be imported in the juju models. No key is imported by default." 57 | type = string 58 | default = "" 59 | } 60 | 61 | variable "ubuntu_pro_token" { 62 | description = "Ubuntu Advantage token that is received with your license of Anbox Cloud." 63 | type = string 64 | default = "" 65 | } 66 | 67 | -------------------------------------------------------------------------------- /docs/footer.md: -------------------------------------------------------------------------------- 1 | ## Usage 2 | The module can deploy a number of anbox subclusters per juju region using the 3 | variable `var.subclusters_per_region`. To execute the terraform plan: 4 | 5 | > Note: You need to have juju controller bootstrapped and a juju client 6 | > configured on your local system to be able to use the plan. 7 | 8 | * Create a file called `anbox.tfvars` and set the values for the variables e.g 9 | 10 | ```tfvars 11 | ubuntu_pro_token = "" 12 | anbox_channel = "1.27/stable" 13 | subclusters = [ 14 | { 15 | name = "a" 16 | lxd_node_count = 1 17 | registry = { 18 | mode = "client" 19 | } 20 | } 21 | ] 22 | ssh_key_path = "~/.ssh/id_rsa.pub" 23 | deploy_registry = true 24 | enable_ha = false 25 | enable_cos = false 26 | constraints = [ "arch=arm64" ] 27 | ``` 28 | 29 | * Initialise the terraform directory 30 | 31 | ```shell 32 | terraform init 33 | ``` 34 | 35 | * Create a terraform plan using 36 | 37 | ```shell 38 | terraform plan -out=tfplan -var-file=anbox.tfvars 39 | ``` 40 | 41 | * Apply the terraform plan using 42 | 43 | ```shell 44 | terraform apply tfplan 45 | ``` 46 | 47 | ## Known Issues 48 | - COS Support: This plan does not create integrations for the grafana agent charm. This is because the Juju Terraform provider [does not](https://github.com/juju/terraform-provider-juju/issues/119) support cross-controller model relations. 49 | - `var.constraints` may not work properly and needs to be specified to keep terraform consistent even when default constraints are being filled by Juju. [#344](https://github.com/juju/terraform-provider-juju/issues/344), [#632](https://github.com/juju/terraform-provider-juju/issues/632) 50 | - The plan might see failures from juju when running terraform with default parallelism. It is recommended to run terraform with `-parallelism=1` for most consistent results. 51 | 52 | ## Contributing 53 | ### Generate Docs 54 | This repository uses [terraform docs](https://terraform-docs.io/) to generate 55 | the docs. To generate docs run: 56 | 57 | ```shell 58 | ./scripts/generate-docs.sh 59 | ``` 60 | 61 | -------------------------------------------------------------------------------- /variables.tf: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2025 Canonical Ltd. All rights reserved. 3 | // 4 | 5 | variable "constraints" { 6 | description = "List of constraints that need to be applied to applications. Each constraint must be of format `=`" 7 | type = list(string) 8 | default = [] 9 | } 10 | 11 | variable "anbox_channel" { 12 | description = "Channel to deploy anbox cloud charms from." 13 | type = string 14 | 15 | validation { 16 | condition = can(regex("\\d+\\.\\d+\\/\\w+", var.anbox_channel)) 17 | error_message = "Channel should be of the format `\\d+.\\d+/\\w+`" 18 | } 19 | } 20 | 21 | variable "subclusters" { 22 | type = list(object({ 23 | name = string 24 | lxd_node_count = number 25 | registry = optional(object({ 26 | mode = optional(string) 27 | })) 28 | })) 29 | default = [] 30 | description = "List of subclusters to deploy." 31 | validation { 32 | condition = length(var.subclusters) > 0 33 | error_message = "Minimum 1 subcluster is required." 34 | } 35 | validation { 36 | condition = alltrue([for c in var.subclusters : c.registry == null ? true : length(c.registry.mode) > 0 ? true : false]) 37 | error_message = "Registry mode must be set if registry is enabled" 38 | } 39 | } 40 | 41 | variable "ubuntu_pro_token" { 42 | description = "Ubuntu Advantage token that is received with your license of Anbox Cloud." 43 | type = string 44 | default = "" 45 | } 46 | 47 | variable "enable_cos" { 48 | description = "Enable cos integration by deploying grafana-agent charm." 49 | type = bool 50 | default = false 51 | } 52 | 53 | variable "enable_ha" { 54 | description = "Enable HA mode for anbox cloud" 55 | type = bool 56 | default = false 57 | } 58 | 59 | variable "deploy_registry" { 60 | description = "Deploy the Anbox Application Registry" 61 | type = bool 62 | default = false 63 | } 64 | 65 | variable "ssh_key_path" { 66 | description = "Path to the SSH key to be imported in the juju models. No key is imported by default." 67 | type = string 68 | default = "" 69 | } 70 | 71 | -------------------------------------------------------------------------------- /modules/subcluster/tests/ha_deployment.tftest.hcl: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2025 Canonical Ltd. All rights reserved. 3 | // 4 | 5 | run "test_ha_deployment" { 6 | command = plan 7 | variables { 8 | model_suffix = "-a" 9 | enable_ha = true 10 | } 11 | assert { 12 | condition = length(juju_model.subcluster) > 0 13 | error_message = "A separate model should be created for subcluster" 14 | } 15 | assert { 16 | condition = length(juju_application.ams) > 0 17 | error_message = "AMS should be deployed by default." 18 | } 19 | assert { 20 | condition = juju_application.ams.units == 3 21 | error_message = "HA for AMS must deploy 3 units" 22 | } 23 | assert { 24 | condition = length(juju_application.agent) > 0 25 | error_message = "Anbox Stream Agent should be deployed by default." 26 | } 27 | assert { 28 | condition = juju_application.agent.units == 3 29 | error_message = "HA for agent must deploy 3 units" 30 | } 31 | assert { 32 | condition = length(juju_application.ca) > 0 33 | error_message = "CA should be deployed by default." 34 | } 35 | assert { 36 | condition = juju_application.ca.units == 3 37 | error_message = "HA for CA must deploy 3 units" 38 | } 39 | assert { 40 | condition = length(juju_application.coturn) > 0 41 | error_message = "Coturn should not be deployed by default." 42 | } 43 | assert { 44 | condition = juju_application.coturn.units == 3 45 | error_message = "HA for coturn must deploy 3 units" 46 | } 47 | assert { 48 | condition = length(juju_integration.ams_agent_streaming) > 0 49 | error_message = "AMS should be related to agent to share api token." 50 | } 51 | assert { 52 | condition = length(juju_integration.agent_ams) > 0 53 | error_message = "AMS should be related to agent." 54 | } 55 | assert { 56 | condition = length(juju_integration.agent_ca) > 0 57 | error_message = "Agent should be related to CA" 58 | } 59 | assert { 60 | condition = length(juju_integration.coturn_agent) > 0 61 | error_message = "Coturn should be related to Agent" 62 | } 63 | assert { 64 | condition = length(juju_integration.ams_lxd) > 0 65 | error_message = "AMS should be related to LXD" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /main.tf: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2025 Canonical Ltd. All rights reserved. 3 | // 4 | 5 | locals { 6 | subcluster_config_map = { for obj in var.subclusters : obj.name => { 7 | node_count = obj.lxd_node_count 8 | registry_config = var.deploy_registry && obj.registry != null ? { mode = obj.registry.mode, offer_url = obj.registry.mode == "client" ? one(module.registry[*].client_offer_url) : one(module.registry[*].publisher_offer_url) } : null 9 | } } 10 | } 11 | 12 | module "subcluster" { 13 | for_each = local.subcluster_config_map 14 | source = "./modules/subcluster" 15 | model_suffix = each.key 16 | channel = var.anbox_channel 17 | external_etcd = true 18 | constraints = var.constraints 19 | enable_ha = var.enable_ha 20 | registry_config = each.value.registry_config 21 | enable_cos = var.enable_cos 22 | ssh_public_key = length(var.ssh_key_path) > 0 ? file(var.ssh_key_path) : "" 23 | ubuntu_pro_token = var.ubuntu_pro_token 24 | 25 | // We let the `lxd_node_count` value override the HA configuration for number 26 | // of LXD nodes. 27 | lxd_nodes = var.enable_ha ? (each.value.node_count >= 3 ? each.value.node_count : 3) : each.value.node_count 28 | } 29 | 30 | module "controller" { 31 | source = "./modules/controller" 32 | channel = var.anbox_channel 33 | constraints = var.constraints 34 | enable_ha = var.enable_ha 35 | enable_cos = var.enable_cos 36 | ssh_public_key = length(var.ssh_key_path) > 0 ? file(var.ssh_key_path) : "" 37 | ubuntu_pro_token = var.ubuntu_pro_token 38 | } 39 | 40 | module "registry" { 41 | count = var.deploy_registry ? 1 : 0 42 | source = "./modules/registry" 43 | channel = var.anbox_channel 44 | constraints = var.constraints 45 | enable_ha = var.enable_ha 46 | ssh_public_key = length(var.ssh_key_path) > 0 ? file(var.ssh_key_path) : "" 47 | ubuntu_pro_token = var.ubuntu_pro_token 48 | } 49 | 50 | resource "juju_integration" "agent_nats_cmr" { 51 | for_each = module.subcluster 52 | model = each.value.model_name 53 | 54 | application { 55 | name = each.value.agent_app_name 56 | endpoint = "nats" 57 | } 58 | 59 | application { 60 | offer_url = module.controller.nats_offer_url 61 | } 62 | } 63 | 64 | resource "juju_integration" "dashboard_ams_cmr" { 65 | for_each = module.subcluster 66 | model = module.controller.model_name 67 | 68 | application { 69 | name = module.controller.dashboard_app_name 70 | endpoint = "ams" 71 | } 72 | 73 | application { 74 | offer_url = each.value.ams_offer_url 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /modules/registry/README.md: -------------------------------------------------------------------------------- 1 | 2 | # Anbox Registry 3 | 4 | This is a terraform module to deploy anbox registry model using juju and terraform. 5 | The module uses `terraform-provider-juju` to deploy the anbox charm to a 6 | juju model. 7 | 8 | ### Some features of the deployment 9 | 10 | * The module deploys an Anbox Registry for anbox cloud. The model currently 11 | includes: 12 | - AAR (Anbox Application Registry) 13 | 14 | ## Requirements 15 | 16 | | Name | Version | 17 | |------|---------| 18 | | [terraform](#requirement\_terraform) | ~> 1.6 | 19 | | [juju](#requirement\_juju) | ~> 0.19.0 | 20 | 21 | ## Providers 22 | 23 | | Name | Version | 24 | |------|---------| 25 | | [juju](#provider\_juju) | 0.19.0 | 26 | 27 | ## Modules 28 | 29 | No modules. 30 | 31 | ## Resources 32 | 33 | | Name | Type | 34 | |------|------| 35 | | [juju_application.aar](https://registry.terraform.io/providers/juju/juju/latest/docs/resources/application) | resource | 36 | | [juju_model.registry](https://registry.terraform.io/providers/juju/juju/latest/docs/resources/model) | resource | 37 | | [juju_offer.client_offer](https://registry.terraform.io/providers/juju/juju/latest/docs/resources/offer) | resource | 38 | | [juju_offer.publisher_offer](https://registry.terraform.io/providers/juju/juju/latest/docs/resources/offer) | resource | 39 | | [juju_ssh_key.this](https://registry.terraform.io/providers/juju/juju/latest/docs/resources/ssh_key) | resource | 40 | 41 | ## Inputs 42 | 43 | | Name | Description | Type | Default | Required | 44 | |------|-------------|------|---------|:--------:| 45 | | [channel](#input\_channel) | Channel for the deployed charm | `string` | `"latest/stable"` | no | 46 | | [constraints](#input\_constraints) | List of constraints that need to be applied to applications. Each constraint must be of format `=` | `list(string)` | `[]` | no | 47 | | [enable\_ha](#input\_enable\_ha) | Number of lxd nodes to deploy per subcluster | `bool` | `false` | no | 48 | | [ssh\_public\_key](#input\_ssh\_public\_key) | SSH key to be imported in the juju models. No key is imported by default. | `string` | `""` | no | 49 | 50 | ## Outputs 51 | 52 | | Name | Description | 53 | |------|-------------| 54 | | [client\_offer\_url](#output\_client\_offer\_url) | Juju Offer URL for connecting to AAR in `client` mode. | 55 | | [model\_name](#output\_model\_name) | Model name created for Anbox Application Registry | 56 | | [publisher\_offer\_url](#output\_publisher\_offer\_url) | Juju Offer URL for connecting to AAR in `publisher` mode. | 57 | -------------------------------------------------------------------------------- /.github/workflows/build-and-test.yaml: -------------------------------------------------------------------------------- 1 | name: Build/Test 2 | 3 | on: 4 | workflow_call: 5 | 6 | jobs: 7 | static-checks: 8 | name: Static Checks 9 | runs-on: [self-hosted, linux, ARM64, jammy, medium] 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 13 | - name: Setup Terraform 14 | run: 15 | sudo snap install terraform --channel latest/stable --classic 16 | - name: Setup TFLint 17 | run: 18 | # This is the revision corresponding to the ARM architecture 19 | sudo snap install tflint --revision 94 20 | - name: Run terraform fmt check 21 | run: terraform fmt -check -diff -recursive . 22 | - name: Run terraform init 23 | run: terraform init 24 | - name: Run terraform validate 25 | run: terraform validate 26 | # Print TFLint version 27 | - name: Run tflint checks 28 | run: | 29 | tflint --version 30 | # Install plugins 31 | tflint --init 32 | # Run tflint command in each directory recursively # use --force if you want to continue with workflow although errors are there 33 | tflint -f compact --recursive 34 | 35 | tests: 36 | name: Run terraform tests 37 | runs-on: [self-hosted, linux, X64, jammy, large] 38 | steps: 39 | - name: Checkout 40 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 41 | - name: Setup Terraform 42 | run: 43 | sudo snap install terraform --channel latest/stable --classic 44 | - name: Setup operator environment 45 | uses: charmed-kubernetes/actions-operator@1c7c9a30d7d233e26e7a4fc1505cc44bbd937229 46 | with: 47 | provider: lxd 48 | - name: Run Main Module Unit Tests 49 | run: | 50 | terraform init && terraform test 51 | - name: Run Submodule Unit Tests 52 | working-directory: ./modules 53 | run: | 54 | set -eo pipefail 55 | for module in ./*; do 56 | pushd $module 57 | echo "===== Testing $module =====" 58 | terraform init && terraform test 59 | popd 60 | done 61 | - name: Deploy Main Plan 62 | env: 63 | ANBOX_CHANNEL: 1.27/stable 64 | run: | 65 | cat < ci.tfvars 66 | anbox_channel = "${ANBOX_CHANNEL}" 67 | subclusters = [ 68 | { 69 | name = "a" 70 | lxd_node_count = 1 71 | registry = { 72 | mode = "client" 73 | } 74 | } 75 | ] 76 | deploy_registry=true 77 | constraints=["arch=amd64"] 78 | EOF 79 | 80 | terraform init && terraform apply -parallelism=1 -auto-approve -var-file='ci.tfvars' 81 | 82 | - name: Wait for models to get active 83 | run: | 84 | set -e 85 | pids=() 86 | models="$( terraform output -json anbox_models | jq -r '.[] | @sh' | tr -d \' )" 87 | for model in $models; do 88 | juju wait-for model $model --query='life=="alive" && status=="available" && forEach(units, unit => unit.workload-status == "active")' --timeout 30m & 89 | pids+=($!) 90 | done 91 | for job in "${pids[@]}"; do 92 | if ! wait $job; then 93 | mkdir logs/ 94 | for model in $models; do 95 | echo "==== Status for $model ====" 96 | juju status -m $model 97 | echo "==== Dump logs for $model ====" 98 | juju-crashdump -m $model -o logs/ 99 | done 100 | exit 1 101 | fi 102 | done 103 | 104 | echo "All models deployed successfully" 105 | 106 | -------------------------------------------------------------------------------- /.github/actions/setup-trivy/action.yaml: -------------------------------------------------------------------------------- 1 | name: Setup Trivy for security scanning and SBOM generation 2 | description: | 3 | The action sets up Trivy, its database, and the KEV list, in an ideal case 4 | from the GitHub cache to avoid making any requests to upstream repositories. 5 | In case that no cached database, debian package, or KEV list is found, they 6 | will be downloaded and cached. 7 | 8 | inputs: 9 | arch: 10 | description: | 11 | Architecture to cache trivy for. Defaults to "amd64". 12 | require: false 13 | default: "amd64" 14 | 15 | runs: 16 | using: composite 17 | steps: 18 | - name: Calculate cache keys 19 | shell: bash 20 | id: cache_keys 21 | run: | 22 | date="$(date +'%Y-%m-%d')" 23 | echo "db=trivy-db-${{ inputs.arch }}-${date}" >> $GITHUB_OUTPUT 24 | echo "deb=trivy-deb-${{ inputs.arch }}-${date}" >> $GITHUB_OUTPUT 25 | echo "kev=kev-list-${date}" >> $GITHUB_OUTPUT 26 | 27 | - name: Restore trivy deb from cache 28 | id: cache_deb 29 | uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 30 | with: 31 | key: ${{ steps.cache_keys.outputs.deb }} 32 | path: ${{ github.workspace }}/trivy.deb 33 | restore-keys: 34 | trivy-deb-${{ inputs.arch }}- 35 | 36 | - name: Fetch debian package for Trivy 37 | if: ${{ steps.cache_deb.outputs.cache-hit != 'true' }} 38 | env: 39 | TRIVY_VERSION: "0.57.0" 40 | TRIVY_ARCH: ${{ inputs.arch == 'amd64' && '64bit' || 'ARM64' }} 41 | TRIVY_SHA256: ${{ inputs.arch == 'amd64' && '0ef038ae7078449b89af6dcdd1cdecd744f65b8b50432797cda78846448c62dd' || '8ae7a057a32d98818c8504c2484017598437e117b9c96858d5749942c99cf1dd' }} 42 | shell: bash 43 | run: | 44 | curl -L -o trivy.deb \ 45 | https://github.com/aquasecurity/trivy/releases/download/v"$TRIVY_VERSION"/trivy_"$TRIVY_VERSION"_Linux-"$TRIVY_ARCH".deb 46 | echo "$TRIVY_SHA256 trivy.deb" | sha256sum --check --status 47 | 48 | - name: Install trivy debian package 49 | shell: bash 50 | run: | 51 | sudo apt install -y ./trivy.deb 52 | 53 | - name: Restore trivy db from cache 54 | id: cache_db 55 | uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 56 | with: 57 | key: ${{ steps.cache_keys.outputs.db }} 58 | path: ${{ github.workspace }}/.cache/trivy 59 | restore-keys: 60 | trivy-db-${{ inputs.arch }}- 61 | 62 | - name: Set up oras 63 | if: ${{ steps.cache_db.outputs.cache-hit != 'true' }} 64 | shell: bash 65 | env: 66 | ORAS_VERSION: "1.2.0" 67 | ORAS_SHA256: ${{ inputs.arch == 'amd64' && '5b3f1cbb86d869eee68120b9b45b9be983f3738442f87ee5f06b00edd0bab336' || '27df680a39fc2fcedc549cb737891623bc696c9a92a03fd341e9356a35836bae' }} 68 | run: | 69 | curl -L -o oras.tar.gz \ 70 | https://github.com/oras-project/oras/releases/download/v"${ORAS_VERSION}"/oras_"${ORAS_VERSION}"_linux_${{ inputs.arch }}.tar.gz 71 | echo "$ORAS_SHA256 oras.tar.gz" | sha256sum --check --status 72 | tar xf oras.tar.gz oras 73 | chmod +x ./oras 74 | sudo mv oras /usr/local/bin 75 | 76 | - name: Download and extract the vulnerability DB 77 | if: ${{ steps.cache_db.outputs.cache-hit != 'true' }} 78 | shell: bash 79 | run: | 80 | mkdir -p "$GITHUB_WORKSPACE"/.cache/trivy/db 81 | oras pull ghcr.io/aquasecurity/trivy-db:2 82 | tar -xzf db.tar.gz -C "$GITHUB_WORKSPACE"/.cache/trivy/db 83 | rm db.tar.gz 84 | 85 | mkdir -p "$GITHUB_WORKSPACE"/.cache/trivy/java-db 86 | oras pull ghcr.io/aquasecurity/trivy-java-db:1 87 | tar -xzf javadb.tar.gz -C "$GITHUB_WORKSPACE"/.cache/trivy/java-db 88 | rm javadb.tar.gz 89 | 90 | - name: Restore KEV list from cache 91 | id: cache_kev 92 | uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 93 | with: 94 | key: ${{ steps.cache_keys.outputs.kev }} 95 | path: ${{ github.workspace }}/kev.json 96 | restore-keys: 97 | kev-list- 98 | 99 | - name: Fetch KEV list from CISA 100 | if: ${{ steps.cache_kev.outputs.cache-hit != 'true' }} 101 | shell: bash 102 | run: | 103 | curl -s -o kev.json https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json 104 | -------------------------------------------------------------------------------- /modules/controller/tests/controller.tftest.hcl: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2025 Canonical Ltd. All rights reserved. 3 | // 4 | 5 | variables { 6 | channel = "1.27/stable" 7 | constraints = [""] 8 | ssh_public_key = "ssh-rsa test-key a@b" 9 | } 10 | 11 | run "test_base_controller_resources" { 12 | command = plan 13 | assert { 14 | condition = length(juju_model.controller) > 0 15 | error_message = "Model not created in controller." 16 | } 17 | assert { 18 | condition = length(juju_ssh_key.this) > 0 19 | error_message = "SSH Key not imported in the model." 20 | } 21 | assert { 22 | condition = length(juju_machine.controller_node) == 1 23 | error_message = "NATS not deployed in controller." 24 | } 25 | assert { 26 | condition = length(juju_application.nats) > 0 27 | error_message = "NATS not deployed in controller." 28 | } 29 | assert { 30 | condition = length(juju_application.gateway) > 0 31 | error_message = "Gateway not deployed in controller." 32 | } 33 | assert { 34 | condition = length(juju_application.dashboard) > 0 35 | error_message = "Anbox Cloud Dashboard not deployed in controller." 36 | } 37 | assert { 38 | condition = length(juju_application.ca) > 0 39 | error_message = "CA not deployed in controller." 40 | } 41 | assert { 42 | condition = length(juju_integration.gateway_nats) > 0 43 | error_message = "Gateway not related to NATS." 44 | } 45 | assert { 46 | condition = length(juju_integration.gateway_ca) > 0 47 | error_message = "Gateway not related to CA." 48 | } 49 | assert { 50 | condition = length(juju_integration.dashboard_ca) > 0 51 | error_message = "Dashboard not related to CA." 52 | } 53 | assert { 54 | condition = length(juju_integration.nats_ca) > 0 55 | error_message = "NATS not related to CA." 56 | } 57 | assert { 58 | condition = length(juju_integration.dashboard_gateway) > 0 59 | error_message = "Dashboard not related to Gateway." 60 | } 61 | } 62 | 63 | run "test_ha_deployment" { 64 | command = plan 65 | variables { 66 | enable_ha = true 67 | } 68 | assert { 69 | condition = length(juju_machine.controller_node) == 3 70 | error_message = "NATS not deployed in controller." 71 | } 72 | assert { 73 | condition = length(juju_model.controller) > 0 74 | error_message = "Model not created in controller." 75 | } 76 | assert { 77 | condition = length(juju_application.nats) > 0 78 | error_message = "NATS not deployed in controller." 79 | } 80 | assert { 81 | condition = juju_application.nats.units == 3 82 | error_message = "HA for NATS must deploy 3 units" 83 | } 84 | assert { 85 | condition = length(juju_application.gateway) > 0 86 | error_message = "Gateway not deployed in controller." 87 | } 88 | assert { 89 | condition = juju_application.gateway.units == 3 90 | error_message = "HA for gateway must deploy 3 units" 91 | } 92 | assert { 93 | condition = length(juju_application.dashboard) > 0 94 | error_message = "Anbox Cloud Dashboard not deployed in controller." 95 | } 96 | assert { 97 | condition = juju_application.dashboard.units == 3 98 | error_message = "HA for dashboard must deploy 3 units" 99 | } 100 | assert { 101 | condition = length(juju_application.ca) > 0 102 | error_message = "CA not deployed in controller." 103 | } 104 | assert { 105 | condition = juju_application.ca.units == 3 106 | error_message = "HA for CA must deploy 3 units" 107 | } 108 | assert { 109 | condition = length(juju_integration.gateway_nats) > 0 110 | error_message = "Gateway not related to NATS." 111 | } 112 | assert { 113 | condition = length(juju_integration.gateway_ca) > 0 114 | error_message = "Gateway not related to CA." 115 | } 116 | assert { 117 | condition = length(juju_integration.dashboard_ca) > 0 118 | error_message = "Dashboard not related to CA." 119 | } 120 | assert { 121 | condition = length(juju_integration.nats_ca) > 0 122 | error_message = "NATS not related to CA." 123 | } 124 | assert { 125 | condition = length(juju_integration.dashboard_gateway) > 0 126 | error_message = "Dashboard not related to Gateway." 127 | } 128 | } 129 | 130 | -------------------------------------------------------------------------------- /modules/controller/README.md: -------------------------------------------------------------------------------- 1 | 2 | # Anbox Cloud Terraform 3 | 4 | This is a terraform module to deploy anbox controller model using juju and terraform. 5 | The module uses `terraform-provider-juju` to deploy the anbox charm to a 6 | juju model. 7 | 8 | ### Some features of the deployment 9 | 10 | * The module deploys a control plane for anbox cloud. The control plane currently 11 | includes: 12 | - NATS 13 | - Anbox Cloud Gateway 14 | - Certificate Authority (CA: self-signed-certificates) 15 | - Anbox Cloud Dashboard 16 | 17 | ## Requirements 18 | 19 | | Name | Version | 20 | |------|---------| 21 | | [terraform](#requirement\_terraform) | ~> 1.6 | 22 | | [juju](#requirement\_juju) | ~> 0.19.0 | 23 | 24 | ## Providers 25 | 26 | | Name | Version | 27 | |------|---------| 28 | | [juju](#provider\_juju) | 0.19.0 | 29 | 30 | ## Modules 31 | 32 | No modules. 33 | 34 | ## Resources 35 | 36 | | Name | Type | 37 | |------|------| 38 | | [juju_application.ca](https://registry.terraform.io/providers/juju/juju/latest/docs/resources/application) | resource | 39 | | [juju_application.cos_agent](https://registry.terraform.io/providers/juju/juju/latest/docs/resources/application) | resource | 40 | | [juju_application.dashboard](https://registry.terraform.io/providers/juju/juju/latest/docs/resources/application) | resource | 41 | | [juju_application.gateway](https://registry.terraform.io/providers/juju/juju/latest/docs/resources/application) | resource | 42 | | [juju_application.nats](https://registry.terraform.io/providers/juju/juju/latest/docs/resources/application) | resource | 43 | | [juju_integration.dashboard_ca](https://registry.terraform.io/providers/juju/juju/latest/docs/resources/integration) | resource | 44 | | [juju_integration.dashboard_gateway](https://registry.terraform.io/providers/juju/juju/latest/docs/resources/integration) | resource | 45 | | [juju_integration.gateway_ca](https://registry.terraform.io/providers/juju/juju/latest/docs/resources/integration) | resource | 46 | | [juju_integration.gateway_cos](https://registry.terraform.io/providers/juju/juju/latest/docs/resources/integration) | resource | 47 | | [juju_integration.gateway_nats](https://registry.terraform.io/providers/juju/juju/latest/docs/resources/integration) | resource | 48 | | [juju_integration.nats_ca](https://registry.terraform.io/providers/juju/juju/latest/docs/resources/integration) | resource | 49 | | [juju_machine.controller_node](https://registry.terraform.io/providers/juju/juju/latest/docs/resources/machine) | resource | 50 | | [juju_model.controller](https://registry.terraform.io/providers/juju/juju/latest/docs/resources/model) | resource | 51 | | [juju_offer.nats_offer](https://registry.terraform.io/providers/juju/juju/latest/docs/resources/offer) | resource | 52 | | [juju_ssh_key.this](https://registry.terraform.io/providers/juju/juju/latest/docs/resources/ssh_key) | resource | 53 | 54 | ## Inputs 55 | 56 | | Name | Description | Type | Default | Required | 57 | |------|-------------|------|---------|:--------:| 58 | | [channel](#input\_channel) | Channel for the deployed charm | `string` | `"latest/stable"` | no | 59 | | [constraints](#input\_constraints) | List of constraints that need to be applied to applications. Each constraint must be of format `=` | `list(string)` | `[]` | no | 60 | | [enable\_cos](#input\_enable\_cos) | Enable cos integration by deploying grafana-agent charm. | `bool` | `false` | no | 61 | | [enable\_ha](#input\_enable\_ha) | Number of lxd nodes to deploy per subcluster | `bool` | `false` | no | 62 | | [ssh\_public\_key](#input\_ssh\_public\_key) | SSH key to be imported in the juju models. No key is imported by default. | `string` | `""` | no | 63 | 64 | ## Outputs 65 | 66 | | Name | Description | 67 | |------|-------------| 68 | | [dashboard\_app\_name](#output\_dashboard\_app\_name) | Anbox Cloud Dashboard application name deployed in the controller model. | 69 | | [model\_name](#output\_model\_name) | Model name for the deployed controller. | 70 | | [nats\_offer\_url](#output\_nats\_offer\_url) | Juju offer url for connecting to the NATS charm. | 71 | 72 | -------------------------------------------------------------------------------- /modules/subcluster/tests/base_deploy.tftest.hcl: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2025 Canonical Ltd. All rights reserved. 3 | // 4 | 5 | run "test_model_name" { 6 | command = plan 7 | variables { 8 | model_suffix = "a" 9 | } 10 | assert { 11 | condition = juju_model.subcluster.name == "anbox-subcluster-a" 12 | error_message = "Model name for subcluster should be of the format `anbox-subcluster-`" 13 | } 14 | } 15 | 16 | run "test_default_lxd_nodes" { 17 | command = plan 18 | variables { 19 | model_suffix = "-a" 20 | } 21 | assert { 22 | condition = juju_application.lxd.units > 0 23 | error_message = "Default number of lxd nodes should be 1" 24 | } 25 | } 26 | 27 | run "test_lxd_nodes_scale" { 28 | command = plan 29 | variables { 30 | model_suffix = "-a" 31 | lxd_nodes = 3 32 | } 33 | assert { 34 | condition = juju_application.lxd.units == 3 35 | error_message = "Number of lxd applications should be 3" 36 | } 37 | assert { 38 | condition = length(juju_machine.lxd_node) == 3 39 | error_message = "Number of lxd machines should be 3" 40 | } 41 | } 42 | 43 | run "test_external_etcd_disabled" { 44 | command = plan 45 | variables { 46 | model_suffix = "-a" 47 | } 48 | assert { 49 | condition = length(juju_machine.db_node) == 0 50 | error_message = "ETCD should not be deployed by default" 51 | } 52 | assert { 53 | condition = length(juju_application.etcd) == 0 54 | error_message = "ETCD should not be deployed by default" 55 | } 56 | assert { 57 | condition = length(juju_integration.ams_db) == 0 58 | error_message = "AMS should not be related to etcd" 59 | } 60 | assert { 61 | condition = length(juju_integration.etcd_ca) == 0 62 | error_message = "ETCD should not be related to CA" 63 | } 64 | } 65 | 66 | run "test_base_deployment_layout" { 67 | command = plan 68 | variables { 69 | model_suffix = "-a" 70 | ssh_public_key = "ssh-rsa test-key a@b" 71 | } 72 | assert { 73 | condition = length(juju_machine.ams_node) == 1 74 | error_message = "A separate machine should be created for AMS." 75 | } 76 | assert { 77 | condition = length(juju_ssh_key.this) > 0 78 | error_message = "SSH Key not imported in the model." 79 | } 80 | assert { 81 | condition = length(juju_machine.lxd_node) == 1 82 | error_message = "A separate machine should be created for lxd." 83 | } 84 | assert { 85 | condition = length(juju_machine.db_node) == 0 86 | error_message = "ETCD should not be deployed by default." 87 | } 88 | assert { 89 | condition = length(juju_model.subcluster) > 0 90 | error_message = "A separate model should be created for subcluster." 91 | } 92 | assert { 93 | condition = length(juju_application.ams) > 0 94 | error_message = "AMS should be deployed by default." 95 | } 96 | assert { 97 | condition = length(juju_application.agent) > 0 98 | error_message = "Anbox Stream Agent should be deployed by default." 99 | } 100 | assert { 101 | condition = length(juju_application.ca) > 0 102 | error_message = "CA should be deployed by default." 103 | } 104 | assert { 105 | condition = length(juju_application.coturn) > 0 106 | error_message = "Coturn should not be deployed by default." 107 | } 108 | assert { 109 | condition = length(juju_integration.ams_agent_streaming) > 0 110 | error_message = "AMS should be related to agent to share api token." 111 | } 112 | assert { 113 | condition = length(juju_integration.agent_ams) > 0 114 | error_message = "AMS should be related to agent." 115 | } 116 | assert { 117 | condition = length(juju_integration.agent_ca) > 0 118 | error_message = "Agent should be related to CA." 119 | } 120 | assert { 121 | condition = length(juju_integration.coturn_agent) > 0 122 | error_message = "Coturn should be related to Agent." 123 | } 124 | assert { 125 | condition = length(juju_integration.ams_lxd) > 0 126 | error_message = "AMS should be related to LXD." 127 | } 128 | assert { 129 | condition = juju_application.lxd.units == 1 130 | error_message = "Default number of lxd nodes should be 1." 131 | } 132 | assert { 133 | condition = length(juju_integration.ams_aar) == 0 134 | error_message = "Registry relation should not be configured." 135 | } 136 | } 137 | 138 | run "test_registry" { 139 | command = plan 140 | variables { 141 | model_suffix = "a" 142 | registry_config = { mode = "client", offer_url = "client_url" } 143 | } 144 | assert { 145 | condition = length(juju_integration.ams_aar) > 0 146 | error_message = "Registry relation should be configured." 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Anbox Cloud Terraform 3 | 4 | > [!WARNING] 5 | > This terraform plan is a work in progress and makes use of [terraform-provider-juju](https://github.com/juju/terraform-provider-juju) 6 | > which is in active development too. Please expect breaking changes (if required) in the future for the plan and the module. 7 | 8 | This is a terraform plan to deploy anbox cloud using juju and terraform. 9 | The module uses `terraform-provider-juju` to deploy the anbox bundles to a 10 | bootstrapped juju cluster. 11 | 12 | This plan uses terraform modules to deploy an [anbox subcluster](./modules/subcluster/README.md) 13 | and a [control plane](./modules/controller/README.md) for Anbox Cloud into two separate juju models. 14 | The subclusters can be scaled up and down according to the requirements. The two terraform modules expose 15 | the required attributes as outputs to be used to connect apps across the two juju models using 16 | cross model relations. 17 | 18 | ## Requirements 19 | 20 | | Name | Version | 21 | |------|---------| 22 | | [terraform](#requirement\_terraform) | ~> 1.6 | 23 | | [juju](#requirement\_juju) | ~> 0.19.0 | 24 | 25 | ## Providers 26 | 27 | | Name | Version | 28 | |------|---------| 29 | | [juju](#provider\_juju) | 0.19.0 | 30 | 31 | ## Modules 32 | 33 | | Name | Source | Version | 34 | |------|--------|---------| 35 | | [controller](#module\_controller) | ./modules/controller | n/a | 36 | | [registry](#module\_registry) | ./modules/registry | n/a | 37 | | [subcluster](#module\_subcluster) | ./modules/subcluster | n/a | 38 | 39 | ## Resources 40 | 41 | | Name | Type | 42 | |------|------| 43 | | [juju_integration.agent_nats_cmr](https://registry.terraform.io/providers/juju/juju/latest/docs/resources/integration) | resource | 44 | | [juju_integration.dashboard_ams_cmr](https://registry.terraform.io/providers/juju/juju/latest/docs/resources/integration) | resource | 45 | 46 | ## Inputs 47 | 48 | | Name | Description | Type | Default | Required | 49 | |------|-------------|------|---------|:--------:| 50 | | [anbox\_channel](#input\_anbox\_channel) | Channel to deploy anbox cloud charms from. | `string` | n/a | yes | 51 | | [constraints](#input\_constraints) | List of constraints that need to be applied to applications. Each constraint must be of format `=` | `list(string)` | `[]` | no | 52 | | [deploy\_registry](#input\_deploy\_registry) | Deploy the Anbox Application Registry | `bool` | `false` | no | 53 | | [enable\_cos](#input\_enable\_cos) | Enable cos integration by deploying grafana-agent charm. | `bool` | `false` | no | 54 | | [enable\_ha](#input\_enable\_ha) | Enable HA mode for anbox cloud | `bool` | `false` | no | 55 | | [ssh\_key\_path](#input\_ssh\_key\_path) | Path to the SSH key to be imported in the juju models. No key is imported by default. | `string` | `""` | no | 56 | | [subclusters](#input\_subclusters) | List of subclusters to deploy. |
list(object({
name = string
lxd_node_count = number
registry = optional(object({
mode = optional(string)
}))
}))
| `[]` | no | 57 | | [ubuntu_pro_token](#input_ubuntu_pro_token) | Ubuntu Advantage token that is received with your license of Anbox Cloud. This will be forwarded to all relevant charms as `ua_token`. | `string` | `""` | no | 58 | 59 | ## Outputs 60 | 61 | | Name | Description | 62 | |------|-------------| 63 | | [anbox\_models](#output\_anbox\_models) | Name of the models created for Anbox Cloud | 64 | 65 | ## Usage 66 | The module can deploy a number of anbox subclusters per juju region using the 67 | variable `var.subclusters_per_region`. To execute the terraform plan: 68 | 69 | > Note: You need to have juju controller bootstrapped and a juju client 70 | > configured on your local system to be able to use the plan. 71 | 72 | * Create a file called `anbox.tfvars` and set the values for the variables e.g 73 | 74 | ```tfvars 75 | ubuntu_pro_token = "" 76 | anbox_channel = "1.27/stable" 77 | subclusters = [ 78 | { 79 | name = "a" 80 | lxd_node_count = 1 81 | registry = { 82 | mode = "client" 83 | } 84 | } 85 | ] 86 | ssh_key_path = "~/.ssh/id_rsa.pub" 87 | deploy_registry = true 88 | enable_ha = false 89 | enable_cos = false 90 | constraints = [ "arch=arm64" ] 91 | ``` 92 | 93 | * Initialise the terraform directory 94 | 95 | ```shell 96 | terraform init 97 | ``` 98 | 99 | * Create a terraform plan using 100 | 101 | ```shell 102 | terraform plan -out=tfplan -var-file=anbox.tfvars 103 | ``` 104 | 105 | * Apply the terraform plan using 106 | 107 | ```shell 108 | terraform apply tfplan 109 | ``` 110 | 111 | ## Known Issues 112 | - COS Support: This plan does not create integrations for the grafana agent charm. This is because the Juju Terraform provider [does not](https://github.com/juju/terraform-provider-juju/issues/119) support cross-controller model relations. 113 | - `var.constraints` may not work properly and needs to be specified to keep terraform consistent even when default constraints are being filled by Juju. [#344](https://github.com/juju/terraform-provider-juju/issues/344), [#632](https://github.com/juju/terraform-provider-juju/issues/632) 114 | - The plan might see failures from juju when running terraform with default parallelism. It is recommended to run terraform with `-parallelism=1` for most consistent results. 115 | 116 | ## Contributing 117 | ### Generate Docs 118 | This repository uses [terraform docs](https://terraform-docs.io/) to generate 119 | the docs. To generate docs run: 120 | 121 | ```shell 122 | ./scripts/generate-docs.sh 123 | ``` 124 | 125 | -------------------------------------------------------------------------------- /modules/subcluster/README.md: -------------------------------------------------------------------------------- 1 | 2 | # Anbox Cloud Subcluster 3 | 4 | This is a terraform module to deploy anbox cloud subcluster using juju and terraform. 5 | The module uses `terraform-provider-juju` to deploy the anbox charm to a 6 | juju model. 7 | 8 | ### Some features of the deployment 9 | 10 | * The module logically divides resources into a control plane and a data plane. 11 | * The control plane currently includes: 12 | - AMS 13 | - ETCD (optional) 14 | - Self-signed-certificates (optional) 15 | - Anbox Stream Agent 16 | - Coturn 17 | * The data plan includes: 18 | - LXD 19 | * This module can deploy a number of LXD machines to act as nodes to AMS using the 20 | input variable `var.lxd_nodes`. 21 | * Each LXD node is accompanied by a subordinate charm `ams-node-controller` to 22 | setup network rules properly on the lxd node. 23 | 24 | ## Requirements 25 | 26 | | Name | Version | 27 | |------|---------| 28 | | [terraform](#requirement\_terraform) | ~> 1.6 | 29 | | [juju](#requirement\_juju) | ~> 0.19.0 | 30 | 31 | ## Providers 32 | 33 | | Name | Version | 34 | |------|---------| 35 | | [juju](#provider\_juju) | 0.19.0 | 36 | 37 | ## Modules 38 | 39 | No modules. 40 | 41 | ## Resources 42 | 43 | | Name | Type | 44 | |------|------| 45 | | [juju_application.agent](https://registry.terraform.io/providers/juju/juju/latest/docs/resources/application) | resource | 46 | | [juju_application.ams](https://registry.terraform.io/providers/juju/juju/latest/docs/resources/application) | resource | 47 | | [juju_application.ca](https://registry.terraform.io/providers/juju/juju/latest/docs/resources/application) | resource | 48 | | [juju_application.cos_agent](https://registry.terraform.io/providers/juju/juju/latest/docs/resources/application) | resource | 49 | | [juju_application.coturn](https://registry.terraform.io/providers/juju/juju/latest/docs/resources/application) | resource | 50 | | [juju_application.etcd](https://registry.terraform.io/providers/juju/juju/latest/docs/resources/application) | resource | 51 | | [juju_application.etcd_ca](https://registry.terraform.io/providers/juju/juju/latest/docs/resources/application) | resource | 52 | | [juju_application.lxd](https://registry.terraform.io/providers/juju/juju/latest/docs/resources/application) | resource | 53 | | [juju_integration.agent_ams](https://registry.terraform.io/providers/juju/juju/latest/docs/resources/integration) | resource | 54 | | [juju_integration.agent_ca](https://registry.terraform.io/providers/juju/juju/latest/docs/resources/integration) | resource | 55 | | [juju_integration.ams_aar](https://registry.terraform.io/providers/juju/juju/latest/docs/resources/integration) | resource | 56 | | [juju_integration.ams_agent_streaming](https://registry.terraform.io/providers/juju/juju/latest/docs/resources/integration) | resource | 57 | | [juju_integration.ams_cos](https://registry.terraform.io/providers/juju/juju/latest/docs/resources/integration) | resource | 58 | | [juju_integration.ams_db](https://registry.terraform.io/providers/juju/juju/latest/docs/resources/integration) | resource | 59 | | [juju_integration.ams_lxd](https://registry.terraform.io/providers/juju/juju/latest/docs/resources/integration) | resource | 60 | | [juju_integration.coturn_agent](https://registry.terraform.io/providers/juju/juju/latest/docs/resources/integration) | resource | 61 | | [juju_integration.etcd_ca](https://registry.terraform.io/providers/juju/juju/latest/docs/resources/integration) | resource | 62 | | [juju_machine.ams_node](https://registry.terraform.io/providers/juju/juju/latest/docs/resources/machine) | resource | 63 | | [juju_machine.db_node](https://registry.terraform.io/providers/juju/juju/latest/docs/resources/machine) | resource | 64 | | [juju_machine.lxd_node](https://registry.terraform.io/providers/juju/juju/latest/docs/resources/machine) | resource | 65 | | [juju_model.subcluster](https://registry.terraform.io/providers/juju/juju/latest/docs/resources/model) | resource | 66 | | [juju_offer.ams_offer](https://registry.terraform.io/providers/juju/juju/latest/docs/resources/offer) | resource | 67 | | [juju_ssh_key.this](https://registry.terraform.io/providers/juju/juju/latest/docs/resources/ssh_key) | resource | 68 | 69 | ## Inputs 70 | 71 | | Name | Description | Type | Default | Required | 72 | |------|-------------|------|---------|:--------:| 73 | | [channel](#input\_channel) | Channel for the deployed charm | `string` | `"latest/stable"` | no | 74 | | [constraints](#input\_constraints) | List of constraints that need to be applied to applications. Each constraint must be of format `=` | `list(string)` | `[]` | no | 75 | | [enable\_cos](#input\_enable\_cos) | Enable cos integration by deploying grafana-agent charm. | `bool` | `false` | no | 76 | | [enable\_ha](#input\_enable\_ha) | Number of lxd nodes to deploy per subcluster | `bool` | `false` | no | 77 | | [external\_etcd](#input\_external\_etcd) | Channel for the deployed charm | `bool` | `false` | no | 78 | | [lxd\_nodes](#input\_lxd\_nodes) | Channel for the deployed charm | `number` | `1` | no | 79 | | [model\_suffix](#input\_model\_suffix) | Suffix to attach for model | `string` | n/a | yes | 80 | | [registry\_config](#input\_registry\_config) | Object to represent connection details for connecting to anbox registry |
object({
mode = string
offer_url = string
})
| `null` | no | 81 | | [ssh\_public\_key](#input\_ssh\_public\_key) | SSH key to be imported in the juju models. No key is imported by default. | `string` | `""` | no | 82 | 83 | ## Outputs 84 | 85 | | Name | Description | 86 | |------|-------------| 87 | | [agent\_app\_name](#output\_agent\_app\_name) | Anbox Stream Agent application name deployed in the subcluster model. | 88 | | [ams\_offer\_url](#output\_ams\_offer\_url) | Juju offer url for connecting to the AMS charm. | 89 | | [model\_name](#output\_model\_name) | Model name for the deployed subcluster. | 90 | 91 | -------------------------------------------------------------------------------- /modules/controller/main.tf: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2025 Canonical Ltd. All rights reserved. 3 | // 4 | 5 | locals { 6 | controller_model_name = "anbox-controller" 7 | num_units = var.enable_ha ? 3 : 1 8 | } 9 | 10 | resource "juju_model" "controller" { 11 | name = local.controller_model_name 12 | 13 | constraints = join(" ", var.constraints) 14 | 15 | config = { 16 | logging-config = "=INFO" 17 | update-status-hook-interval = "5m" 18 | } 19 | } 20 | 21 | resource "juju_ssh_key" "this" { 22 | count = length(var.ssh_public_key) > 0 ? 1 : 0 23 | model = juju_model.controller.name 24 | payload = trim(var.ssh_public_key, "\n") 25 | } 26 | 27 | resource "juju_application" "nats" { 28 | name = "nats" 29 | 30 | model = juju_model.controller.name 31 | constraints = join(" ", var.constraints) 32 | 33 | charm { 34 | name = "nats" 35 | channel = "2/stable" 36 | base = local.base 37 | } 38 | 39 | machines = juju_machine.controller_node[*].machine_id 40 | 41 | // FIXME: Currently the provider has some issues with reconciling state using 42 | // the response from the JUJU APIs. This is done just to ignore the changes in 43 | // string values returned. 44 | lifecycle { 45 | ignore_changes = [constraints] 46 | } 47 | } 48 | 49 | resource "juju_application" "gateway" { 50 | name = "anbox-stream-gateway" 51 | 52 | model = juju_model.controller.name 53 | constraints = join(" ", var.constraints) 54 | 55 | charm { 56 | name = "anbox-stream-gateway" 57 | channel = var.channel 58 | base = local.base 59 | } 60 | 61 | machines = juju_machine.controller_node[*].machine_id 62 | 63 | config = { 64 | snap_risk_level = local.risk 65 | ua_token = var.ubuntu_pro_token 66 | } 67 | 68 | // FIXME: Currently the provider has some issues with reconciling state using 69 | // the response from the JUJU APIs. This is done just to ignore the changes in 70 | // string values returned. 71 | lifecycle { 72 | ignore_changes = [constraints] 73 | } 74 | } 75 | 76 | resource "juju_application" "dashboard" { 77 | name = "anbox-cloud-dashboard" 78 | 79 | model = juju_model.controller.name 80 | constraints = join(" ", var.constraints) 81 | 82 | charm { 83 | name = "anbox-cloud-dashboard" 84 | channel = var.channel 85 | base = local.base 86 | } 87 | 88 | config = { 89 | snap_risk_level = local.risk 90 | ua_token = var.ubuntu_pro_token 91 | } 92 | 93 | machines = juju_machine.controller_node[*].machine_id 94 | 95 | // FIXME: Currently the provider has some issues with reconciling state using 96 | // the response from the JUJU APIs. This is done just to ignore the changes in 97 | // string values returned. 98 | lifecycle { 99 | ignore_changes = [constraints] 100 | } 101 | } 102 | 103 | resource "juju_application" "ca" { 104 | name = "ca" 105 | 106 | model = juju_model.controller.name 107 | constraints = join(" ", var.constraints) 108 | 109 | charm { 110 | name = "self-signed-certificates" 111 | base = local.base 112 | channel = "latest/stable" 113 | } 114 | 115 | machines = juju_machine.controller_node[*].machine_id 116 | 117 | // FIXME: Currently the provider has some issues with reconciling state using 118 | // the response from the JUJU APIs. This is done just to ignore the changes in 119 | // string values returned. 120 | lifecycle { 121 | ignore_changes = [constraints] 122 | } 123 | } 124 | 125 | resource "juju_integration" "gateway_nats" { 126 | model = juju_model.controller.name 127 | 128 | application { 129 | name = juju_application.gateway.name 130 | endpoint = "nats" 131 | } 132 | 133 | application { 134 | name = juju_application.nats.name 135 | endpoint = "client" 136 | } 137 | } 138 | 139 | resource "juju_integration" "dashboard_gateway" { 140 | model = juju_model.controller.name 141 | 142 | application { 143 | name = juju_application.gateway.name 144 | endpoint = "client" 145 | } 146 | 147 | application { 148 | name = juju_application.dashboard.name 149 | endpoint = "gateway" 150 | } 151 | } 152 | 153 | 154 | resource "juju_integration" "nats_ca" { 155 | model = juju_model.controller.name 156 | 157 | application { 158 | name = juju_application.ca.name 159 | endpoint = "certificates" 160 | } 161 | 162 | application { 163 | name = juju_application.nats.name 164 | endpoint = "ca-client" 165 | } 166 | } 167 | 168 | resource "juju_integration" "gateway_ca" { 169 | model = juju_model.controller.name 170 | 171 | application { 172 | name = juju_application.ca.name 173 | endpoint = "certificates" 174 | } 175 | 176 | application { 177 | name = juju_application.gateway.name 178 | endpoint = "certificates" 179 | } 180 | } 181 | 182 | resource "juju_integration" "dashboard_ca" { 183 | model = juju_model.controller.name 184 | 185 | application { 186 | name = juju_application.ca.name 187 | endpoint = "certificates" 188 | } 189 | 190 | application { 191 | name = juju_application.dashboard.name 192 | endpoint = "certificates" 193 | } 194 | } 195 | 196 | resource "juju_offer" "nats_offer" { 197 | model = juju_model.controller.name 198 | application_name = juju_application.nats.name 199 | endpoint = "client" 200 | } 201 | 202 | resource "juju_application" "cos_agent" { 203 | count = var.enable_cos ? 1 : 0 204 | name = "grafana-agent" 205 | 206 | model = juju_model.controller.name 207 | 208 | charm { 209 | name = "grafana-agent" 210 | base = local.base 211 | } 212 | 213 | // FIXME: Currently the provider has some issues with reconciling state using 214 | // the response from the JUJU APIs. This is done just to ignore the changes in 215 | // string values returned. 216 | lifecycle { 217 | ignore_changes = [constraints] 218 | } 219 | } 220 | 221 | resource "juju_integration" "gateway_cos" { 222 | count = var.enable_cos ? 1 : 0 223 | model = juju_model.controller.name 224 | 225 | application { 226 | name = juju_application.gateway.name 227 | endpoint = "cos-agent" 228 | } 229 | 230 | application { 231 | name = one(juju_application.cos_agent[*].name) 232 | endpoint = "cos-agent" 233 | } 234 | } 235 | 236 | 237 | resource "juju_machine" "controller_node" { 238 | model = juju_model.controller.name 239 | count = local.num_units 240 | base = local.base 241 | name = "anbox-controller-${count.index}" 242 | constraints = join(" ", var.constraints) 243 | } 244 | -------------------------------------------------------------------------------- /modules/subcluster/control_plane.tf: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2025 Canonical Ltd. All rights reserved. 3 | // 4 | 5 | locals { 6 | // we need to remove all special characters from the string to use it as 7 | // an identifier for an offer. 8 | offer_suffix = replace(var.model_suffix, "/[-_.]*/", "") 9 | num_units = var.enable_ha ? 3 : 1 10 | } 11 | 12 | resource "juju_model" "subcluster" { 13 | name = "anbox-subcluster-${var.model_suffix}" 14 | 15 | constraints = join(" ", var.constraints) 16 | 17 | config = { 18 | logging-config = "=INFO" 19 | update-status-hook-interval = "5m" 20 | } 21 | } 22 | 23 | resource "juju_ssh_key" "this" { 24 | count = length(var.ssh_public_key) > 0 ? 1 : 0 25 | model = juju_model.subcluster.name 26 | payload = trim(var.ssh_public_key, "\n") 27 | } 28 | 29 | resource "juju_application" "ams" { 30 | name = "ams" 31 | 32 | model = juju_model.subcluster.name 33 | constraints = join(" ", var.constraints) 34 | machines = juju_machine.ams_node[*].machine_id 35 | 36 | charm { 37 | name = "ams" 38 | channel = var.channel 39 | base = local.base 40 | } 41 | config = { 42 | use_embedded_etcd = !var.external_etcd 43 | snap_risk_level = local.risk 44 | ua_token = var.ubuntu_pro_token 45 | } 46 | 47 | // FIXME: Currently the provider has some issues with reconciling state using 48 | // the response from the JUJU APIs. This is done just to ignore the changes in 49 | // string values returned. 50 | lifecycle { 51 | ignore_changes = [constraints] 52 | } 53 | } 54 | 55 | resource "juju_application" "etcd" { 56 | count = var.external_etcd ? 1 : 0 57 | name = "etcd" 58 | 59 | model = juju_model.subcluster.name 60 | constraints = join(" ", var.constraints) 61 | 62 | charm { 63 | name = "etcd" 64 | channel = "latest/stable" 65 | base = local.base 66 | } 67 | 68 | config = { 69 | channel = "3.4/stable" 70 | } 71 | 72 | machines = juju_machine.db_node[*].machine_id 73 | // FIXME: Currently the provider has some issues with reconciling state using 74 | // the response from the JUJU APIs. This is done just to ignore the changes in 75 | // string values returned. 76 | lifecycle { 77 | ignore_changes = [constraints] 78 | } 79 | } 80 | 81 | resource "juju_application" "ca" { 82 | name = "ca" 83 | 84 | model = juju_model.subcluster.name 85 | constraints = join(" ", var.constraints) 86 | 87 | charm { 88 | name = "self-signed-certificates" 89 | channel = "latest/stable" 90 | base = local.base 91 | } 92 | 93 | machines = juju_machine.ams_node[*].machine_id 94 | } 95 | 96 | resource "juju_application" "etcd_ca" { 97 | count = var.external_etcd ? 1 : 0 98 | name = "etcd-ca" 99 | 100 | model = juju_model.subcluster.name 101 | constraints = join(" ", var.constraints) 102 | 103 | charm { 104 | name = "easyrsa" 105 | channel = "latest/stable" 106 | base = local.base 107 | } 108 | 109 | machines = juju_machine.db_node[*].machine_id 110 | } 111 | 112 | resource "juju_integration" "ams_db" { 113 | count = var.external_etcd ? 1 : 0 114 | model = juju_model.subcluster.name 115 | 116 | application { 117 | name = juju_application.ams.name 118 | endpoint = "etcd" 119 | } 120 | 121 | application { 122 | name = one(juju_application.etcd[*].name) 123 | endpoint = "db" 124 | } 125 | } 126 | 127 | resource "juju_integration" "etcd_ca" { 128 | count = var.external_etcd ? 1 : 0 129 | model = juju_model.subcluster.name 130 | 131 | application { 132 | name = one(juju_application.etcd_ca[*].name) 133 | endpoint = "client" 134 | } 135 | 136 | application { 137 | name = one(juju_application.etcd[*].name) 138 | endpoint = "certificates" 139 | } 140 | } 141 | 142 | resource "juju_application" "agent" { 143 | name = "anbox-stream-agent" 144 | 145 | model = juju_model.subcluster.name 146 | constraints = join(" ", var.constraints) 147 | 148 | charm { 149 | name = "anbox-stream-agent" 150 | channel = var.channel 151 | base = local.base 152 | } 153 | 154 | machines = juju_machine.ams_node[*].machine_id 155 | 156 | config = { 157 | region = "cloud-0" 158 | snap_risk_level = local.risk 159 | ua_token = var.ubuntu_pro_token 160 | } 161 | 162 | // FIXME: Currently the provider has some issues with reconciling state using 163 | // the response from the JUJU APIs. This is done just to ignore the changes in 164 | // string values returned. 165 | lifecycle { 166 | ignore_changes = [constraints] 167 | } 168 | } 169 | 170 | resource "juju_application" "coturn" { 171 | name = "coturn" 172 | 173 | model = juju_model.subcluster.name 174 | constraints = join(" ", var.constraints) 175 | 176 | charm { 177 | name = "coturn" 178 | base = local.base 179 | // Since this is released by Anbox Charmer, this charm is release with anbox 180 | // releases 181 | channel = var.channel 182 | } 183 | 184 | machines = juju_machine.ams_node[*].machine_id 185 | 186 | // FIXME: Currently the provider has some issues with reconciling state using 187 | // the response from the JUJU APIs. This is done just to ignore the changes in 188 | // string values returned. 189 | lifecycle { 190 | ignore_changes = [constraints] 191 | } 192 | } 193 | 194 | resource "juju_integration" "agent_ams" { 195 | model = juju_model.subcluster.name 196 | 197 | application { 198 | name = juju_application.agent.name 199 | endpoint = "ams" 200 | } 201 | 202 | application { 203 | name = juju_application.ams.name 204 | endpoint = "rest-api" 205 | } 206 | } 207 | 208 | resource "juju_integration" "ams_agent_streaming" { 209 | model = juju_model.subcluster.name 210 | 211 | application { 212 | name = juju_application.agent.name 213 | endpoint = "client" 214 | } 215 | 216 | application { 217 | name = juju_application.ams.name 218 | endpoint = "agent" 219 | } 220 | } 221 | 222 | 223 | resource "juju_integration" "agent_ca" { 224 | model = juju_model.subcluster.name 225 | 226 | application { 227 | name = juju_application.ca.name 228 | endpoint = "certificates" 229 | } 230 | 231 | application { 232 | name = juju_application.agent.name 233 | endpoint = "certificates" 234 | } 235 | } 236 | 237 | resource "juju_integration" "coturn_agent" { 238 | model = juju_model.subcluster.name 239 | 240 | application { 241 | name = juju_application.coturn.name 242 | endpoint = "stun" 243 | } 244 | 245 | application { 246 | name = juju_application.agent.name 247 | endpoint = "stun" 248 | } 249 | } 250 | 251 | resource "juju_integration" "ams_aar" { 252 | count = var.registry_config != null ? 1 : 0 253 | model = juju_model.subcluster.name 254 | 255 | application { 256 | name = juju_application.ams.name 257 | endpoint = "registry-${var.registry_config.mode}" 258 | } 259 | 260 | application { 261 | offer_url = var.registry_config.offer_url 262 | } 263 | } 264 | 265 | resource "juju_offer" "ams_offer" { 266 | model = juju_model.subcluster.name 267 | application_name = juju_application.ams.name 268 | endpoint = "rest-api" 269 | name = "ams${local.offer_suffix}" 270 | } 271 | 272 | 273 | resource "juju_application" "cos_agent" { 274 | count = var.enable_cos ? 1 : 0 275 | name = "grafana-agent" 276 | 277 | model = juju_model.subcluster.name 278 | 279 | charm { 280 | name = "grafana-agent" 281 | base = local.base 282 | } 283 | 284 | // FIXME: Currently the provider has some issues with reconciling state using 285 | // the response from the JUJU APIs. This is done just to ignore the changes in 286 | // string values returned. 287 | lifecycle { 288 | ignore_changes = [constraints] 289 | } 290 | } 291 | 292 | resource "juju_integration" "ams_cos" { 293 | count = var.enable_cos ? 1 : 0 294 | model = juju_model.subcluster.name 295 | 296 | application { 297 | name = juju_application.ams.name 298 | endpoint = "cos-agent" 299 | } 300 | 301 | application { 302 | name = one(juju_application.cos_agent[*].name) 303 | endpoint = "cos-agent" 304 | } 305 | } 306 | 307 | resource "juju_machine" "ams_node" { 308 | model = juju_model.subcluster.name 309 | count = local.num_units 310 | base = local.base 311 | name = "ams-${count.index}" 312 | constraints = join(" ", var.constraints) 313 | } 314 | 315 | resource "juju_machine" "db_node" { 316 | count = var.external_etcd ? local.num_units : 0 317 | model = juju_model.subcluster.name 318 | base = local.base 319 | name = "db-${count.index}" 320 | constraints = join(" ", var.constraints) 321 | } 322 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------