diff --git a/.github/workflows/generate_terraform_docs.yaml b/.github/workflows/generate_terraform_docs.yaml new file mode 100644 index 0000000..d0c6788 --- /dev/null +++ b/.github/workflows/generate_terraform_docs.yaml @@ -0,0 +1,30 @@ +# Copyright 2026 Canonical Ltd. +# See LICENSE file for licensing details. + +name: Update Terraform documentation + +# Keep the `paths` filters below and the `terraform-directory` input in sync with +# the directories where the Terraform modules live. +on: + pull_request: + paths: + - "terraform/**" + - ".github/workflows/generate_terraform_docs.yaml" + push: + branches: + - main + paths: + - "terraform/**" + - ".github/workflows/generate_terraform_docs.yaml" + +permissions: + contents: write + pull-requests: write + +jobs: + terraform-docs: + name: Update Terraform documentation + uses: canonical/operator-workflows/.github/workflows/generate_terraform_docs.yaml@030c5b20a0bb880dcf1ed326bff0fc16b6eb6ac1 # main + secrets: inherit + with: + terraform-directory: terraform/charm/cloudflared,terraform/charm/cloudflare_configurator diff --git a/.github/workflows/terraform_modules_compliance.yaml b/.github/workflows/terraform_modules_compliance.yaml new file mode 100644 index 0000000..1fc179f --- /dev/null +++ b/.github/workflows/terraform_modules_compliance.yaml @@ -0,0 +1,17 @@ +# Copyright 2026 Canonical Ltd. +# See LICENSE file for licensing details. + +name: Terraform modules compliance + +on: + workflow_dispatch: + pull_request: + paths: + - "**/terraform/**" + - ".github/workflows/terraform_modules_compliance.yaml" + +jobs: + terraform-compliance: + uses: canonical/operator-workflows/.github/workflows/terraform_modules_compliance.yaml@030c5b20a0bb880dcf1ed326bff0fc16b6eb6ac1 # main + with: + terraform-directories: '["terraform/charm/cloudflared", "terraform/charm/cloudflare_configurator"]' diff --git a/.github/workflows/terraform_modules_release.yaml b/.github/workflows/terraform_modules_release.yaml new file mode 100644 index 0000000..1b030d7 --- /dev/null +++ b/.github/workflows/terraform_modules_release.yaml @@ -0,0 +1,33 @@ +# Copyright 2026 Canonical Ltd. +# See LICENSE file for licensing details. + +name: Terraform modules release + +on: + workflow_dispatch: + pull_request: + paths: + - "terraform/**" + - ".github/workflows/terraform_modules_release.yaml" + push: + branches: + - main + paths: + - "terraform/**" + - ".github/workflows/terraform_modules_release.yaml" + +jobs: + cloudflared-terraform: + uses: canonical/operator-workflows/.github/workflows/terraform_modules_release.yaml@030c5b20a0bb880dcf1ed326bff0fc16b6eb6ac1 # main + permissions: + contents: write + with: + tag-prefix: cloudflared-tf- + major-version-file: terraform/charm/cloudflared/MAJOR_VERSION + cloudflare-configurator-terraform: + uses: canonical/operator-workflows/.github/workflows/terraform_modules_release.yaml@030c5b20a0bb880dcf1ed326bff0fc16b6eb6ac1 # main + permissions: + contents: write + with: + tag-prefix: cloudflare-configurator-tf- + major-version-file: terraform/charm/cloudflare_configurator/MAJOR_VERSION diff --git a/.github/workflows/test_terraform_modules.yaml b/.github/workflows/test_terraform_modules.yaml index 40aef55..6ec389f 100644 --- a/.github/workflows/test_terraform_modules.yaml +++ b/.github/workflows/test_terraform_modules.yaml @@ -1,4 +1,4 @@ -# Copyright 2025 Canonical Ltd. +# Copyright 2026 Canonical Ltd. # See LICENSE file for licensing details. name: Terraform modules tests @@ -7,16 +7,14 @@ on: workflow_dispatch: pull_request: paths: - - 'terraform/**' - - 'terraform-product/**' + - "**/terraform/**" + - ".github/workflows/test_terraform_modules.yaml" jobs: - terraform-tests: - uses: canonical/operator-workflows/.github/workflows/terraform_modules_test.yaml@main - permissions: - contents: read + terraform-tests-lxd: + uses: canonical/operator-workflows/.github/workflows/terraform_modules_test.yaml@030c5b20a0bb880dcf1ed326bff0fc16b6eb6ac1 # main secrets: inherit with: - k8s-controller: true - #lxd-controller: true - terraform-directories: '["terraform"]' + k8s-controller: false + lxd-controller: true + terraform-directories: '["terraform/charm/cloudflared", "terraform/charm/cloudflare_configurator"]' diff --git a/.gitignore b/.gitignore index 39d01a7..e651169 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,8 @@ __pycache__/ terraform/**/.terraform* terraform/**/.tfvars terraform/**/*.tfstate* +**/.terraform/ +**/.terraform.lock.hcl # opcli/spread generated files .spread-reuse.* .spread-run-*/ diff --git a/.licenserc.yaml b/.licenserc.yaml index 15c2d10..3cd8980 100644 --- a/.licenserc.yaml +++ b/.licenserc.yaml @@ -34,4 +34,5 @@ header: - 'docs/release-notes/template/*.yaml' - 'uv.lock' - '**/uv.lock' + - '**/MAJOR_VERSION' comment: on-failure diff --git a/docs/release-notes/artifacts/pr0030.yaml b/docs/release-notes/artifacts/pr0030.yaml new file mode 100644 index 0000000..037b686 --- /dev/null +++ b/docs/release-notes/artifacts/pr0030.yaml @@ -0,0 +1,20 @@ +# Copyright 2026 Canonical Ltd. +# See LICENSE file for licensing details. + +version_schema: 2 + +changes: + - title: Added CC008-compliant Terraform modules + author: weiiwang01 + type: minor + description: > + Added Terraform modules for the cloudflared and cloudflare-configurator + charms following the CC008 Charm Terraform Module standard, together with + the Terraform test, compliance, release and documentation workflows. + urls: + pr: + - https://github.com/canonical/cloudflared-operators/pull/30 + related_doc: + related_issue: + visibility: public + highlight: false diff --git a/terraform/.tflint.hcl b/terraform/.tflint.hcl new file mode 100644 index 0000000..ebfb77b --- /dev/null +++ b/terraform/.tflint.hcl @@ -0,0 +1,6 @@ +# Copyright 2026 Canonical Ltd. +# See LICENSE file for licensing details. + +rule "terraform_required_version" { + enabled = true +} diff --git a/terraform/MAJOR_VERSION b/terraform/MAJOR_VERSION new file mode 100644 index 0000000..56a6051 --- /dev/null +++ b/terraform/MAJOR_VERSION @@ -0,0 +1 @@ +1 \ No newline at end of file diff --git a/terraform/charm/cloudflare_configurator/MAJOR_VERSION b/terraform/charm/cloudflare_configurator/MAJOR_VERSION new file mode 120000 index 0000000..a80b14b --- /dev/null +++ b/terraform/charm/cloudflare_configurator/MAJOR_VERSION @@ -0,0 +1 @@ +../../MAJOR_VERSION \ No newline at end of file diff --git a/terraform/charm/cloudflare_configurator/README.md b/terraform/charm/cloudflare_configurator/README.md new file mode 100644 index 0000000..db4a400 --- /dev/null +++ b/terraform/charm/cloudflare_configurator/README.md @@ -0,0 +1,66 @@ + +# Cloudflare-configurator Terraform module + + +This folder contains a base [Terraform][Terraform] module for the cloudflare-configurator charm. + +The module uses the [Terraform Juju provider][Terraform Juju provider] to model the charm +deployment onto any machine environment managed by [Juju][Juju]. + +## Module structure + +- **main.tf** - Defines the Juju application to be deployed. +- **variables.tf** - Allows customization of the deployment. Also models the charm configuration, + except for exposing the deployment options (Juju model UUID, channel or application name). +- **outputs.tf** - Integrates the module with other Terraform modules, primarily + by defining potential integration endpoints (charm integrations), but also by exposing + the deployed Juju application. +- **terraform.tf** - Defines the Terraform and Juju provider versions. + +## Using cloudflare-configurator base module in higher level modules + +If you want to use the `cloudflare-configurator` base module as part of your Terraform module, +import it like shown below. Always pin the `?ref=` to a released tag or a commit hash: CC008 +does not allow floating references such as branches. + +```text +data "juju_model" "my_model" { + name = "my-model" +} + +module "cloudflare_configurator" { + source = "git::https://github.com/canonical/cloudflared-operators//terraform/charm/cloudflare_configurator?ref=cloudflare-configurator-tf-1.0.0" + + model_uuid = data.juju_model.my_model.uuid + # (Customize configuration variables here if needed) +} +``` + +Create integrations, for instance: + +```text +resource "juju_integration" "cloudflared-route" { + model_uuid = data.juju_model.my_model.uuid + application { + name = module.cloudflare_configurator.application.name + endpoint = module.cloudflare_configurator.provides.cloudflared_route.endpoint + } + application { + name = "cloudflared" + endpoint = "cloudflared-route" + } +} +``` + +The complete list of available integrations can be found [in the Integrations tab][cloudflare-configurator-integrations]. + + + + + + + +[Terraform]: https://www.terraform.io/ +[Terraform Juju provider]: https://registry.terraform.io/providers/juju/juju/latest +[Juju]: https://canonical.com/juju +[cloudflare-configurator-integrations]: https://charmhub.io/cloudflare-configurator/integrations diff --git a/terraform/charm/cloudflare_configurator/main.tf b/terraform/charm/cloudflare_configurator/main.tf new file mode 100644 index 0000000..87c2a21 --- /dev/null +++ b/terraform/charm/cloudflare_configurator/main.tf @@ -0,0 +1,18 @@ +# Copyright 2026 Canonical Ltd. +# See LICENSE file for licensing details. + +resource "juju_application" "cloudflare_configurator" { + name = var.app_name + model_uuid = var.model_uuid + + charm { + name = "cloudflare-configurator" + channel = var.channel + revision = var.revision + base = var.base + } + + config = var.config + constraints = var.constraints + units = var.units +} diff --git a/terraform/charm/cloudflare_configurator/outputs.tf b/terraform/charm/cloudflare_configurator/outputs.tf new file mode 100644 index 0000000..7a0257c --- /dev/null +++ b/terraform/charm/cloudflare_configurator/outputs.tf @@ -0,0 +1,28 @@ +# Copyright 2026 Canonical Ltd. +# See LICENSE file for licensing details. + +output "application" { + description = "The deployed application object." + value = juju_application.cloudflare_configurator +} + +output "provides" { + description = "Map of the provided integration endpoints." + value = { + cloudflared_route = { + kind = "endpoint" + name = juju_application.cloudflare_configurator.name + endpoint = "cloudflared-route" + } + ingress = { + kind = "endpoint" + name = juju_application.cloudflare_configurator.name + endpoint = "ingress" + } + } +} + +output "requires" { + description = "Map of the required integration endpoints." + value = {} +} diff --git a/terraform/charm/cloudflare_configurator/terraform.tf b/terraform/charm/cloudflare_configurator/terraform.tf new file mode 100644 index 0000000..637cc0a --- /dev/null +++ b/terraform/charm/cloudflare_configurator/terraform.tf @@ -0,0 +1,12 @@ +# Copyright 2026 Canonical Ltd. +# See LICENSE file for licensing details. + +terraform { + required_version = "~> 1.12" + required_providers { + juju = { + source = "juju/juju" + version = "~> 1.0" + } + } +} diff --git a/terraform/charm/cloudflare_configurator/tests/main.tftest.hcl b/terraform/charm/cloudflare_configurator/tests/main.tftest.hcl new file mode 100644 index 0000000..4147ac2 --- /dev/null +++ b/terraform/charm/cloudflare_configurator/tests/main.tftest.hcl @@ -0,0 +1,33 @@ +# Copyright 2026 Canonical Ltd. +# See LICENSE file for licensing details. + +run "setup_tests" { + module { + source = "./tests/setup" + } +} + +run "basic_deploy" { + variables { + model_uuid = run.setup_tests.model_uuid + base = "ubuntu@24.04" + channel = "latest/edge" + # renovate: depName="cloudflare-configurator" + revision = 11 + } + + assert { + condition = output.application.name == "cloudflare-configurator" + error_message = "cloudflare-configurator application name did not match expected" + } + + assert { + condition = output.provides.cloudflared_route.endpoint == "cloudflared-route" + error_message = "cloudflare-configurator module should provide 'provides.cloudflared_route' output" + } + + assert { + condition = output.provides.ingress.endpoint == "ingress" + error_message = "cloudflare-configurator module should provide 'provides.ingress' output" + } +} diff --git a/terraform/charm/cloudflare_configurator/tests/setup/main.tf b/terraform/charm/cloudflare_configurator/tests/setup/main.tf new file mode 100644 index 0000000..0236105 --- /dev/null +++ b/terraform/charm/cloudflare_configurator/tests/setup/main.tf @@ -0,0 +1,22 @@ +# Copyright 2026 Canonical Ltd. +# See LICENSE file for licensing details. + +terraform { + required_version = "~> 1.12" + required_providers { + juju = { + source = "juju/juju" + version = "~> 1.0" + } + } +} + +provider "juju" {} + +resource "juju_model" "test_model" { + name = "tf-testing-${formatdate("YYYYMMDDhhmmss", timestamp())}" +} + +output "model_uuid" { + value = juju_model.test_model.uuid +} diff --git a/terraform/charm/cloudflare_configurator/variables.tf b/terraform/charm/cloudflare_configurator/variables.tf new file mode 100644 index 0000000..2339b2f --- /dev/null +++ b/terraform/charm/cloudflare_configurator/variables.tf @@ -0,0 +1,52 @@ +# Copyright 2026 Canonical Ltd. +# See LICENSE file for licensing details. + +variable "app_name" { + description = "Name of the application in the Juju model." + type = string + default = "cloudflare-configurator" + nullable = false +} + +variable "base" { + description = "The operating system on which to deploy." + type = string + default = null +} + +variable "channel" { + description = "The channel to use when deploying a charm." + type = string + default = "latest/edge" + nullable = false +} + +variable "config" { + description = "Application config. Details about available options can be found at https://charmhub.io/cloudflare-configurator/configurations" + type = map(string) + default = {} +} + +variable "constraints" { + description = "Juju constraints to apply for this application." + type = string + default = null +} + +variable "model_uuid" { + description = "UUID of the Juju model where the application will be deployed." + type = string + nullable = false +} + +variable "revision" { + description = "Revision number of the charm." + type = number + default = null +} + +variable "units" { + description = "The number of units to deploy." + type = number + default = 1 +} diff --git a/terraform/charm/cloudflared/MAJOR_VERSION b/terraform/charm/cloudflared/MAJOR_VERSION new file mode 120000 index 0000000..a80b14b --- /dev/null +++ b/terraform/charm/cloudflared/MAJOR_VERSION @@ -0,0 +1 @@ +../../MAJOR_VERSION \ No newline at end of file diff --git a/terraform/charm/cloudflared/README.md b/terraform/charm/cloudflared/README.md new file mode 100644 index 0000000..ce7a962 --- /dev/null +++ b/terraform/charm/cloudflared/README.md @@ -0,0 +1,66 @@ + +# Cloudflared Terraform module + + +This folder contains a base [Terraform][Terraform] module for the cloudflared charm. + +The module uses the [Terraform Juju provider][Terraform Juju provider] to model the charm +deployment onto any machine environment managed by [Juju][Juju]. + +## Module structure + +- **main.tf** - Defines the Juju application to be deployed. +- **variables.tf** - Allows customization of the deployment. Also models the charm configuration, + except for exposing the deployment options (Juju model UUID, channel or application name). +- **outputs.tf** - Integrates the module with other Terraform modules, primarily + by defining potential integration endpoints (charm integrations), but also by exposing + the deployed Juju application. +- **terraform.tf** - Defines the Terraform and Juju provider versions. + +## Using cloudflared base module in higher level modules + +If you want to use the `cloudflared` base module as part of your Terraform module, import it +like shown below. Always pin the `?ref=` to a released tag or a commit hash: CC008 does not +allow floating references such as branches. + +```text +data "juju_model" "my_model" { + name = "my-model" +} + +module "cloudflared" { + source = "git::https://github.com/canonical/cloudflared-operators//terraform/charm/cloudflared?ref=cloudflared-tf-1.0.0" + + model_uuid = data.juju_model.my_model.uuid + # (Customize configuration variables here if needed) +} +``` + +Create integrations, for instance: + +```text +resource "juju_integration" "cloudflared-route" { + model_uuid = data.juju_model.my_model.uuid + application { + name = module.cloudflared.application.name + endpoint = module.cloudflared.requires.cloudflared_route.endpoint + } + application { + name = "cloudflare-configurator" + endpoint = "cloudflared-route" + } +} +``` + +The complete list of available integrations can be found [in the Integrations tab][cloudflared-integrations]. + + + + + + + +[Terraform]: https://www.terraform.io/ +[Terraform Juju provider]: https://registry.terraform.io/providers/juju/juju/latest +[Juju]: https://canonical.com/juju +[cloudflared-integrations]: https://charmhub.io/cloudflared/integrations diff --git a/terraform/charm/cloudflared/main.tf b/terraform/charm/cloudflared/main.tf new file mode 100644 index 0000000..05c4116 --- /dev/null +++ b/terraform/charm/cloudflared/main.tf @@ -0,0 +1,17 @@ +# Copyright 2026 Canonical Ltd. +# See LICENSE file for licensing details. + +resource "juju_application" "cloudflared" { + name = var.app_name + model_uuid = var.model_uuid + + charm { + name = "cloudflared" + channel = var.channel + revision = var.revision + base = var.base + } + + config = var.config + constraints = var.constraints +} diff --git a/terraform/charm/cloudflared/outputs.tf b/terraform/charm/cloudflared/outputs.tf new file mode 100644 index 0000000..bacd527 --- /dev/null +++ b/terraform/charm/cloudflared/outputs.tf @@ -0,0 +1,34 @@ +# Copyright 2026 Canonical Ltd. +# See LICENSE file for licensing details. + +output "application" { + description = "The deployed application object." + value = juju_application.cloudflared +} + +output "provides" { + description = "Map of the provided integration endpoints." + value = { + cos_agent = { + kind = "endpoint" + name = juju_application.cloudflared.name + endpoint = "cos-agent" + } + } +} + +output "requires" { + description = "Map of the required integration endpoints." + value = { + cloudflared_route = { + kind = "endpoint" + name = juju_application.cloudflared.name + endpoint = "cloudflared-route" + } + juju_info = { + kind = "endpoint" + name = juju_application.cloudflared.name + endpoint = "juju-info" + } + } +} diff --git a/terraform/charm/cloudflared/terraform.tf b/terraform/charm/cloudflared/terraform.tf new file mode 100644 index 0000000..637cc0a --- /dev/null +++ b/terraform/charm/cloudflared/terraform.tf @@ -0,0 +1,12 @@ +# Copyright 2026 Canonical Ltd. +# See LICENSE file for licensing details. + +terraform { + required_version = "~> 1.12" + required_providers { + juju = { + source = "juju/juju" + version = "~> 1.0" + } + } +} diff --git a/terraform/charm/cloudflared/tests/main.tftest.hcl b/terraform/charm/cloudflared/tests/main.tftest.hcl new file mode 100644 index 0000000..ea39e70 --- /dev/null +++ b/terraform/charm/cloudflared/tests/main.tftest.hcl @@ -0,0 +1,38 @@ +# Copyright 2026 Canonical Ltd. +# See LICENSE file for licensing details. + +run "setup_tests" { + module { + source = "./tests/setup" + } +} + +run "basic_deploy" { + variables { + model_uuid = run.setup_tests.model_uuid + base = "ubuntu@24.04" + channel = "latest/edge" + # renovate: depName="cloudflared" + revision = 40 + } + + assert { + condition = output.application.name == "cloudflared" + error_message = "cloudflared application name did not match expected" + } + + assert { + condition = output.provides.cos_agent.endpoint == "cos-agent" + error_message = "cloudflared module should provide 'provides.cos_agent' output" + } + + assert { + condition = output.requires.cloudflared_route.endpoint == "cloudflared-route" + error_message = "cloudflared module should provide 'requires.cloudflared_route' output" + } + + assert { + condition = output.requires.juju_info.endpoint == "juju-info" + error_message = "cloudflared module should provide 'requires.juju_info' output" + } +} diff --git a/terraform/charm/cloudflared/tests/setup/main.tf b/terraform/charm/cloudflared/tests/setup/main.tf new file mode 100644 index 0000000..0236105 --- /dev/null +++ b/terraform/charm/cloudflared/tests/setup/main.tf @@ -0,0 +1,22 @@ +# Copyright 2026 Canonical Ltd. +# See LICENSE file for licensing details. + +terraform { + required_version = "~> 1.12" + required_providers { + juju = { + source = "juju/juju" + version = "~> 1.0" + } + } +} + +provider "juju" {} + +resource "juju_model" "test_model" { + name = "tf-testing-${formatdate("YYYYMMDDhhmmss", timestamp())}" +} + +output "model_uuid" { + value = juju_model.test_model.uuid +} diff --git a/terraform/charm/cloudflared/variables.tf b/terraform/charm/cloudflared/variables.tf new file mode 100644 index 0000000..5826577 --- /dev/null +++ b/terraform/charm/cloudflared/variables.tf @@ -0,0 +1,46 @@ +# Copyright 2026 Canonical Ltd. +# See LICENSE file for licensing details. + +variable "app_name" { + description = "Name of the application in the Juju model." + type = string + default = "cloudflared" + nullable = false +} + +variable "base" { + description = "The operating system on which to deploy." + type = string + default = null +} + +variable "channel" { + description = "The channel to use when deploying a charm." + type = string + default = "latest/stable" + nullable = false +} + +variable "config" { + description = "Application config. Details about available options can be found at https://charmhub.io/cloudflared/configurations" + type = map(string) + default = {} +} + +variable "constraints" { + description = "Juju constraints to apply for this application." + type = string + default = null +} + +variable "model_uuid" { + description = "UUID of the Juju model where the application will be deployed." + type = string + nullable = false +} + +variable "revision" { + description = "Revision number of the charm." + type = number + default = null +}