diff --git a/.github/workflows/tofu-test.yml b/.github/workflows/tofu-test.yml index 3d83139e9..63afe3b5f 100644 --- a/.github/workflows/tofu-test.yml +++ b/.github/workflows/tofu-test.yml @@ -14,7 +14,7 @@ jobs: test-commons-modules: uses: nullplatform/actions-nullplatform/.github/workflows/tofu-test.yml@main with: - modules: '["infrastructure/commons/cert_manager", "infrastructure/commons/external_dns", "infrastructure/commons/istio"]' + modules: '["infrastructure/commons/cert_manager", "infrastructure/commons/external_dns", "infrastructure/commons/istio", "infrastructure/commons/prometheus"]' test-aws-modules: uses: nullplatform/actions-nullplatform/.github/workflows/tofu-test.yml@main @@ -30,7 +30,7 @@ jobs: test-nullplatform-modules: uses: nullplatform/actions-nullplatform/.github/workflows/tofu-test.yml@main with: - modules: '["nullplatform/account", "nullplatform/agent", "nullplatform/api_key", "nullplatform/dimension", "nullplatform/dimension_value", "nullplatform/users", "nullplatform/metrics", "nullplatform/asset/docker_server", "nullplatform/cloud/azure/cloud", "nullplatform/cloud/gcp/cloud", "nullplatform/cloud/aws/cloud", "nullplatform/code_repository"]' + modules: '["nullplatform/account", "nullplatform/agent", "nullplatform/api_key", "nullplatform/dimension", "nullplatform/dimension_value", "nullplatform/users", "nullplatform/metrics", "nullplatform/asset/docker_server", "nullplatform/cloud/azure/cloud", "nullplatform/cloud/gcp/cloud", "nullplatform/cloud/aws/cloud", "nullplatform/code_repository", "nullplatform/base", "nullplatform/agent"]' test-container-orchestration-modules: uses: nullplatform/actions-nullplatform/.github/workflows/tofu-test.yml@main diff --git a/infrastructure/commons/cert_manager/main.tf b/infrastructure/commons/cert_manager/main.tf index 284dbc7e9..5ec6eda99 100644 --- a/infrastructure/commons/cert_manager/main.tf +++ b/infrastructure/commons/cert_manager/main.tf @@ -3,6 +3,7 @@ resource "helm_release" "cert_manager" { repository = "https://charts.jetstack.io" chart = "cert-manager" namespace = var.cert_manager_namespace + version = var.cert_manager_version create_namespace = true disable_webhooks = false diff --git a/infrastructure/commons/cert_manager/tests/cert_manager_aws.tftest.hcl b/infrastructure/commons/cert_manager/tests/cert_manager_aws.tftest.hcl index d7b52522f..e085bb663 100644 --- a/infrastructure/commons/cert_manager/tests/cert_manager_aws.tftest.hcl +++ b/infrastructure/commons/cert_manager/tests/cert_manager_aws.tftest.hcl @@ -1,12 +1,13 @@ mock_provider "helm" {} variables { - cloud_provider = "aws" - hosted_zone_name = "myorg.example.com" - account_slug = "myorg" - private_domain_name = "myorg.example.com" - aws_sa_arn = "arn:aws:iam::123456789012:role/cert-manager" - aws_region = "us-east-1" + cloud_provider = "aws" + hosted_zone_name = "myorg.example.com" + account_slug = "myorg" + private_domain_name = "myorg.example.com" + aws_sa_arn = "arn:aws:iam::123456789012:role/cert-manager" + aws_region = "us-east-1" + cert_manager_version = "v1.21.1" } # Validates AWS provider config plans successfully @@ -100,3 +101,19 @@ run "rejects_invalid_aws_identity_mode" { expect_failures = [var.aws_identity_mode] } + +################################################################################ +# Version pinning +################################################################################ + +# cert_manager_version was declared with a default and never referenced: grep found one +# occurrence, its own declaration. The helm_release had no version argument, so installs +# tracked whatever charts.jetstack.io served while the README showed a number. +run "cert_manager_version_reaches_the_release" { + command = plan + + assert { + condition = helm_release.cert_manager.version == "v1.21.1" + error_message = "cert_manager_version must be wired to the helm_release, not merely declared" + } +} diff --git a/infrastructure/commons/cert_manager/tests/cert_manager_azure.tftest.hcl b/infrastructure/commons/cert_manager/tests/cert_manager_azure.tftest.hcl index 479a3c76f..f9a069769 100644 --- a/infrastructure/commons/cert_manager/tests/cert_manager_azure.tftest.hcl +++ b/infrastructure/commons/cert_manager/tests/cert_manager_azure.tftest.hcl @@ -11,6 +11,7 @@ variables { azure_resource_group_name = "rg-test" azure_tenant_id = "11111111-2222-3333-4444-555555555555" azure_hosted_zone_name = "myorg.nullimplementation.com" + cert_manager_version = "v1.21.1" } # Validates Azure provider config plans successfully diff --git a/infrastructure/commons/cert_manager/tests/cert_manager_cloudflare.tftest.hcl b/infrastructure/commons/cert_manager/tests/cert_manager_cloudflare.tftest.hcl index 47e95bb29..8932865c9 100644 --- a/infrastructure/commons/cert_manager/tests/cert_manager_cloudflare.tftest.hcl +++ b/infrastructure/commons/cert_manager/tests/cert_manager_cloudflare.tftest.hcl @@ -1,11 +1,12 @@ mock_provider "helm" {} variables { - cloud_provider = "cloudflare" - hosted_zone_name = "myorg.nullimplementation.com" - account_slug = "myorg" - private_domain_name = "myorg.nullimplementation.com" - cloudflare_token = "fake-cloudflare-token-for-testing" + cloud_provider = "cloudflare" + hosted_zone_name = "myorg.nullimplementation.com" + account_slug = "myorg" + private_domain_name = "myorg.nullimplementation.com" + cloudflare_token = "fake-cloudflare-token-for-testing" + cert_manager_version = "v1.21.1" } # Validates Cloudflare provider config plans successfully diff --git a/infrastructure/commons/cert_manager/tests/cert_manager_cross_provider.tftest.hcl b/infrastructure/commons/cert_manager/tests/cert_manager_cross_provider.tftest.hcl index 89b0334df..8c3d9cf43 100644 --- a/infrastructure/commons/cert_manager/tests/cert_manager_cross_provider.tftest.hcl +++ b/infrastructure/commons/cert_manager/tests/cert_manager_cross_provider.tftest.hcl @@ -1,5 +1,9 @@ mock_provider "helm" {} +variables { + cert_manager_version = "v1.21.1" +} + # Validates invalid cloud_provider is rejected run "rejects_invalid_provider" { command = plan diff --git a/infrastructure/commons/cert_manager/tests/cert_manager_oci.tftest.hcl b/infrastructure/commons/cert_manager/tests/cert_manager_oci.tftest.hcl index 306360989..3a87c59b3 100644 --- a/infrastructure/commons/cert_manager/tests/cert_manager_oci.tftest.hcl +++ b/infrastructure/commons/cert_manager/tests/cert_manager_oci.tftest.hcl @@ -8,6 +8,7 @@ variables { oci_compartment_ocid = "ocid1.compartment.oc1..aaaaaaaatest" oci_sa_ocid = "ocid1.principal.oc1..aaaaaaaatest" oci_region = "us-ashburn-1" + cert_manager_version = "v1.21.1" } # Validates OCI provider config plans successfully diff --git a/infrastructure/commons/cert_manager/variables.tf b/infrastructure/commons/cert_manager/variables.tf index 0ed891cab..85a75d454 100644 --- a/infrastructure/commons/cert_manager/variables.tf +++ b/infrastructure/commons/cert_manager/variables.tf @@ -62,9 +62,14 @@ variable "private_domain_name" { ############################################################################### variable "cert_manager_version" { - description = "The version of cert-manager Helm chart to deploy" + # example: v1.21.1 + description = "No default: every install pins this deliberately — see VERSIONS.md. The version of cert-manager Helm chart to deploy. Was declared but never wired to the helm_release, so installs tracked whatever the chart repository served; the default is the version that resolved to as of 2026-08-27, which keeps behaviour unchanged while removing the drift." type = string - default = "1.18.2" + + validation { + condition = var.cert_manager_version != "" && !contains(["latest", "main", "master"], lower(var.cert_manager_version)) + error_message = "cert_manager_version must be a non-empty fixed version, not empty and not a moving reference." + } } variable "cert_manager_namespace" { diff --git a/infrastructure/commons/prometheus/.terraform.lock.hcl b/infrastructure/commons/prometheus/.terraform.lock.hcl new file mode 100644 index 000000000..89971bbf5 --- /dev/null +++ b/infrastructure/commons/prometheus/.terraform.lock.hcl @@ -0,0 +1,25 @@ +# This file is maintained automatically by "tofu init". +# Manual edits may be lost in future updates. + +provider "registry.opentofu.org/hashicorp/helm" { + version = "3.2.0" + constraints = "~> 3.0" + hashes = [ + "h1:thNSoWm4pdgTEO1XBi1n5V2nwsdSA6EVEXP3qPZiFcA=", + "zh:1a214581dee54ec4e9afa4050e54f6c187aed4b51b2d2ac929c58706b65e1159", + "zh:2f8ba94af93011768ed1fffd4d25b980cd764f2d49c9f13475512ec48464da0b", + "zh:36373bca4f374e95f654def79e0b12df0c8f2e01c634db80ff2737ded29062e3", + "zh:3a26b5c3e47b2bbc01faa0fa9fe816ea4f74f1f8f4555dfad7f62ee38266aaff", + "zh:51bc637700f13cfc1f7c6a8c03a4b5b5755338419912d945dbfe5ccb0ddbf614", + "zh:53f32f91afcb682209de124d8a994e591687023ae7c0dfed6184c5511778b16b", + "zh:6f4434327ed466b2b5be0d5f4aa537bca71ebd7f7fe05468065aeda52dfc8896", + "zh:7394e8c6f5027fa21699e46c9a45bee1ce87fd8dd98f2e89e8d414ec70c8e4e1", + "zh:d90b855a0990e3aa6445afe66de730c2f4651f39699c6b6345ca02f9af2a1a08", + "zh:d9b7246e6af0f75155ed532855014f888e9e5613242c89e321b4e0f9b54f5726", + "zh:dabd399ca36172c15d176a24cf199ac886ef191f7131e806d76a8dc209e5beb8", + "zh:e57987397be46dc123365f16c3bec4dd0615453c8bfdcfc1b7c9f3202a09c516", + "zh:ec88430b833b943b38d02f70b33250c72bb6ffa3b3d22360990a41390161a2b2", + "zh:f8ea01b57982e9ed9ad3a750b1caba261a478f5b0ddcac78a4502d886fd2fc74", + "zh:fb63819037158205ebf42b649c3a8ec308234ffe987a5dbd4444e1e0683f1170", + ] +} diff --git a/infrastructure/commons/prometheus/main.tf b/infrastructure/commons/prometheus/main.tf index e5f5e247e..d6abb7440 100644 --- a/infrastructure/commons/prometheus/main.tf +++ b/infrastructure/commons/prometheus/main.tf @@ -3,6 +3,7 @@ resource "helm_release" "prometheus" { repository = "https://prometheus-community.github.io/helm-charts" chart = "prometheus" namespace = var.prometheus_namespace + version = var.prometheus_version create_namespace = true disable_webhooks = false diff --git a/infrastructure/commons/prometheus/tests/prometheus.tftest.hcl b/infrastructure/commons/prometheus/tests/prometheus.tftest.hcl new file mode 100644 index 000000000..d2594f358 --- /dev/null +++ b/infrastructure/commons/prometheus/tests/prometheus.tftest.hcl @@ -0,0 +1,34 @@ +mock_provider "helm" {} + +variables { + prometheus_version = "29.27.0" +} + +################################################################################ +# Version pinning +################################################################################ + +# The helm_release carried no version argument and the module exposed no variable for one, +# so every apply resolved to whatever prometheus-community served latest, with no diff to +# review and no way to pin without editing the module. +run "prometheus_version_reaches_the_release" { + command = plan + + assert { + condition = helm_release.prometheus.version == "29.27.0" + error_message = "prometheus_version must be wired to the helm_release so the deployed chart is a decision" + } +} + +run "prometheus_version_is_overridable" { + command = plan + + variables { + prometheus_version = "29.26.0" + } + + assert { + condition = helm_release.prometheus.version == "29.26.0" + error_message = "prometheus_version must be settable by the caller" + } +} diff --git a/infrastructure/commons/prometheus/variables.tf b/infrastructure/commons/prometheus/variables.tf index 502083c4c..4db279135 100644 --- a/infrastructure/commons/prometheus/variables.tf +++ b/infrastructure/commons/prometheus/variables.tf @@ -1,3 +1,14 @@ +variable "prometheus_version" { + # example: 29.27.0 + description = "No default: every install pins this deliberately — see VERSIONS.md. Helm chart version for the prometheus-community/prometheus chart. The helm_release carried no version at all, so every apply resolved to whatever the repository served latest; the default is the version that resolved to as of 2026-08-27, which keeps behaviour unchanged while removing the drift." + type = string + + validation { + condition = var.prometheus_version != "" && !contains(["latest", "main", "master"], lower(var.prometheus_version)) + error_message = "prometheus_version must be a non-empty fixed version, not empty and not a moving reference." + } +} + variable "nullplatform_port" { description = "Port number for nullplatform service communication" type = number diff --git a/nullplatform/agent/locals.tf b/nullplatform/agent/locals.tf index 1a039b8ba..9c6cd6a1c 100644 --- a/nullplatform/agent/locals.tf +++ b/nullplatform/agent/locals.tf @@ -4,7 +4,13 @@ locals { - scope_list = compact([trimspace(coalesce(var.agent_repos_scope, ""))]) + # The repository lives here and only the tag is exposed, so a version bump is a variable + + # change instead of a hand-assembled URL. + + scope_repo = trimspace(coalesce(var.agent_repos_scope, "")) + + scope_list = compact([local.scope_repo != "" ? "${local.scope_repo}#${trimspace(var.agent_repos_scope_tag)}" : ""]) # Parse comma-separated extra repositories and clean whitespace repos_extra = compact([for s in var.agent_repos_extra : trimspace(s)]) @@ -42,6 +48,7 @@ locals { CLUSTER_NAME = var.cluster_name NAMESPACE = var.namespace IMAGE_TAG = var.image_tag + TRAFFIC_CONTAINER_IMAGE = "${var.agent_traffic_manager_repository}:${var.agent_traffic_manager_tag}" DOMAIN = var.domain DNS_TYPE = var.dns_type USE_ACCOUNT_SLUG = var.use_account_slug diff --git a/nullplatform/agent/tests/agent.tftest.hcl b/nullplatform/agent/tests/agent.tftest.hcl index 8174cef1c..9bd044919 100644 --- a/nullplatform/agent/tests/agent.tftest.hcl +++ b/nullplatform/agent/tests/agent.tftest.hcl @@ -2,11 +2,14 @@ mock_provider "nullplatform" {} mock_provider "helm" {} variables { - api_key = "test-api-key" - cluster_name = "test-cluster" - tags_selectors = { environment = "test" } - image_tag = "latest" - cloud_provider = "gcp" + api_key = "test-api-key" + cluster_name = "test-cluster" + tags_selectors = { environment = "test" } + image_tag = "latest" + cloud_provider = "gcp" + nullplatform_agent_helm_version = "2.37.0" + agent_traffic_manager_tag = "1.8.0" + agent_repos_scope_tag = "v1.15.1" } run "no_extra_envs_does_not_require_ingress_templates" { diff --git a/nullplatform/agent/tests/agent_values.tftest.hcl b/nullplatform/agent/tests/agent_values.tftest.hcl new file mode 100644 index 000000000..cbb42c65b --- /dev/null +++ b/nullplatform/agent/tests/agent_values.tftest.hcl @@ -0,0 +1,102 @@ +mock_provider "helm" {} +mock_provider "nullplatform" {} + +variables { + api_key = "test-api-key" + cluster_name = "my-cluster" + tags_selectors = { dimension = "prod" } + cloud_provider = "aws" + aws_iam_role_arn = "arn:aws:iam::123456789012:role/agent" + image_tag = "0.9.2" + nullplatform_agent_helm_version = "2.37.0" + agent_traffic_manager_tag = "1.8.0" + agent_repos_scope_tag = "v1.15.1" +} + +################################################################################ +# Traffic manager image +################################################################################ + +# Pinning the traffic manager used to mean passing the whole image string through +# extra_envs. The registry now lives in the module and only the tag is exposed. +run "traffic_manager_image_is_assembled_from_the_tag" { + command = plan + + assert { + condition = strcontains(helm_release.agent.values[0], "TRAFFIC_CONTAINER_IMAGE: \"public.ecr.aws/nullplatform/k8s-traffic-manager:1.8.0\"") + error_message = "TRAFFIC_CONTAINER_IMAGE should be built from the repository default and the pinned tag" + } +} + +run "traffic_manager_repository_is_overridable" { + command = plan + + variables { + agent_traffic_manager_repository = "my-mirror.example.com/nullplatform/k8s-traffic-manager" + } + + assert { + condition = strcontains(helm_release.agent.values[0], "TRAFFIC_CONTAINER_IMAGE: \"my-mirror.example.com/nullplatform/k8s-traffic-manager:1.8.0\"") + error_message = "the registry must be overridable for a mirrored path" + } +} + +run "extra_envs_still_overrides_the_traffic_manager_image" { + command = plan + + variables { + extra_envs = { + TRAFFIC_CONTAINER_IMAGE = "public.ecr.aws/nullplatform/k8s-traffic-manager@sha256:abc123" + } + } + + # extra_envs is merged last, so the previous way of doing this keeps working. That is what + # makes exposing the tag an addition rather than a breaking change. + assert { + condition = strcontains(helm_release.agent.values[0], "TRAFFIC_CONTAINER_IMAGE: \"public.ecr.aws/nullplatform/k8s-traffic-manager@sha256:abc123\"") + error_message = "extra_envs must keep precedence over the assembled image" + } +} + +################################################################################ +# Scope repository +################################################################################ + +run "scope_repo_is_pinned_to_a_tag" { + command = plan + + assert { + condition = !strcontains(helm_release.agent.values[0], "#main") + error_message = "the scope repo default must not point at a moving branch" + } + + assert { + condition = strcontains(helm_release.agent.values[0], "scopes.git#v1.15.1") + error_message = "the scope repo default should be pinned to the released tag" + } +} + +run "scope_repo_is_overridable" { + command = plan + + variables { + agent_repos_scope_tag = "v1.14.0" + } + + assert { + condition = strcontains(helm_release.agent.values[0], "scopes.git#v1.14.0") + error_message = "callers must still be able to choose their own ref" + } +} + +run "agent_repos_scope_rejects_an_inline_fragment" { + command = plan + + variables { + agent_repos_scope = "https://github.com/nullplatform/scopes.git#v1.15.1" + } + + # Catches the most likely migration mistake: pasting the old value verbatim, which would + # otherwise render repo.git#v1.15.1#v1.15.1. + expect_failures = [var.agent_repos_scope] +} diff --git a/nullplatform/agent/variables.tf b/nullplatform/agent/variables.tf index 105d1d2e2..bc2a9aa43 100644 --- a/nullplatform/agent/variables.tf +++ b/nullplatform/agent/variables.tf @@ -17,6 +17,7 @@ variable "cluster_name" { # Image tag for the agent container image variable "image_tag" { + # example: 0.9.2 description = "Image tag for the agent container image" type = string } @@ -57,11 +58,15 @@ variable "service_account_name" { # Version of the nullplatform agent Helm chart to deploy variable "nullplatform_agent_helm_version" { - description = "Version of the nullplatform agent Helm chart to deploy" + # example: 2.37.0 + description = "No default: every install pins this deliberately — see VERSIONS.md. Version of the nullplatform agent Helm chart to deploy" type = string # 2.37.0+ ships the worker orchestrator (patches, per-install isolation, idle - # reaper, insecure default). - default = "2.37.0" + + validation { + condition = var.nullplatform_agent_helm_version != "" && !contains(["latest", "main", "master"], lower(var.nullplatform_agent_helm_version)) + error_message = "nullplatform_agent_helm_version must be a non-empty fixed version, not empty and not a moving reference." + } } variable "worker" { @@ -93,16 +98,68 @@ variable "namespace" { # Git repository URL containing agent scope configurations (format: repo#branch) variable "agent_repos_scope" { - description = "Git repository URL containing agent scope configurations (format: repo#branch)" + description = "Git repository URL containing agent scope configurations, WITHOUT the ref fragment. The ref goes in agent_repos_scope_tag." + type = string + default = "https://github.com/nullplatform/scopes.git" + + # Without this, migrating by pasting the old value (repo.git#v1.15.1) produces + # repo.git#v1.15.1#, which fails inside the pod at clone time instead of during plan. + validation { + condition = length(regexall("#", var.agent_repos_scope)) == 0 + error_message = "agent_repos_scope must not contain a '#' fragment — set the ref in agent_repos_scope_tag instead." + } +} + +variable "agent_repos_scope_tag" { + # example: v1.15.1 + description = "Git tag of the scopes repository to clone. No default: every install pins this deliberately so the agent cannot pick up scope changes it was never rolled out with — see VERSIONS.md." type = string - default = "https://github.com/nullplatform/scopes.git#main" + + validation { + condition = var.agent_repos_scope_tag != "" && !contains(["main", "master", "head", "latest"], lower(var.agent_repos_scope_tag)) + error_message = "agent_repos_scope_tag must be a non-empty fixed tag, not empty and not a moving branch." + } } # List of additional Git repositories used for extended agent configuration +variable "agent_traffic_manager_repository" { + description = "Container image repository for the traffic manager. Defaults to the official nullplatform image; override to pull from a mirror. Matches the pattern nullplatform/base uses for its own images." + type = string + default = "public.ecr.aws/nullplatform/k8s-traffic-manager" +} + +variable "agent_traffic_manager_tag" { + # example: 1.8.0 + description = "No default: every install pins this deliberately — see VERSIONS.md. Image tag for the traffic manager, published to the agent as TRAFFIC_CONTAINER_IMAGE. Pinning this used to mean passing the whole image string through extra_envs; the registry lives here so only the tag is exposed. extra_envs still takes precedence for anyone who needs a digest or a mirrored path." + type = string + + validation { + condition = var.agent_traffic_manager_tag != "" && !contains(["latest", "main", "master"], lower(var.agent_traffic_manager_tag)) + error_message = "agent_traffic_manager_tag must be a non-empty fixed version, not empty and not a moving reference." + } +} + variable "agent_repos_extra" { - description = "List of additional Git repositories used for extended agent configuration" + description = "List of additional Git repositories used for extended agent configuration. Each entry MUST carry a pinned ref fragment (repo.git#v1.2.3); moving refs are rejected. Covers scopes-* and services-* without enumerating them." type = list(string) default = [] + + # Two validations rather than one so the error names which rule was broken. + validation { + condition = alltrue([ + for r in var.agent_repos_extra : + can(regex("#[^#]+$", trimspace(r))) + ]) + error_message = "every agent_repos_extra entry must pin a ref with a '#' fragment, e.g. https://github.com/nullplatform/scopes-lambda.git#v0.3.1" + } + + validation { + condition = alltrue([ + for r in var.agent_repos_extra : + !can(regex("(?i)#(main|master|head|latest)$", trimspace(r))) + ]) + error_message = "agent_repos_extra entries must pin a fixed tag, not a moving ref (main/master/HEAD/latest)." + } } # List of initialization scripts to execute during agent startup diff --git a/nullplatform/api_key/tests/api_key.tftest.hcl b/nullplatform/api_key/tests/api_key.tftest.hcl index f422ab176..64916e104 100644 --- a/nullplatform/api_key/tests/api_key.tftest.hcl +++ b/nullplatform/api_key/tests/api_key.tftest.hcl @@ -100,8 +100,8 @@ run "tags_merge_custom_tags" { command = plan variables { - type = "custom" - custom_name = "MY-KEY" + type = "custom" + custom_name = "MY-KEY" custom_role_slugs = ["developer"] custom_tags = [ { key = "team", value = "platform" }, diff --git a/nullplatform/base/tests/base_values.tftest.hcl b/nullplatform/base/tests/base_values.tftest.hcl index 73419fd11..079de99dd 100644 --- a/nullplatform/base/tests/base_values.tftest.hcl +++ b/nullplatform/base/tests/base_values.tftest.hcl @@ -2,8 +2,11 @@ mock_provider "helm" {} mock_provider "kubernetes" {} variables { - np_api_key = "test-api-key" - k8s_provider = "eks" + np_api_key = "test-api-key" + k8s_provider = "eks" + nullplatform_base_helm_version = "2.44.0" + logging_controller_image_tag = "1.6.0" + control_plane_agent_image_tag = "0.9.2" } ############################################ @@ -144,10 +147,10 @@ run "dynatrace_logs_disabled" { command = plan variables { - dynatrace_enabled = true - dynatrace_api_key = "dt-test-key" + dynatrace_enabled = true + dynatrace_api_key = "dt-test-key" dynatrace_environment_id = "dt-env-123" - dynatrace_logs_enabled = false + dynatrace_logs_enabled = false } assert { diff --git a/nullplatform/base/variables.tf b/nullplatform/base/variables.tf index e964fb573..78d2ed833 100644 --- a/nullplatform/base/variables.tf +++ b/nullplatform/base/variables.tf @@ -1,7 +1,12 @@ variable "nullplatform_base_helm_version" { - description = "Helm chart version for the nullplatform base." + # example: 2.44.0 + description = "No default: every install pins this deliberately — see VERSIONS.md. Helm chart version for the nullplatform base." type = string - default = "2.44.0" + + validation { + condition = var.nullplatform_base_helm_version != "" && !contains(["latest", "main", "master"], lower(var.nullplatform_base_helm_version)) + error_message = "nullplatform_base_helm_version must be a non-empty fixed version, not empty and not a moving reference." + } } variable "namespace" { @@ -131,9 +136,14 @@ variable "control_plane_agent_image_repository" { } variable "control_plane_agent_image_tag" { + # example: 0.9.2 type = string - description = "Container image tag for the control plane agent." - default = "0.9.2" + description = "No default: every install pins this deliberately — see VERSIONS.md. Container image tag for the control plane agent." + + validation { + condition = var.control_plane_agent_image_tag != "" && !contains(["latest", "main", "master"], lower(var.control_plane_agent_image_tag)) + error_message = "control_plane_agent_image_tag must be a non-empty fixed version, not empty and not a moving reference." + } } ############################################ @@ -165,9 +175,14 @@ variable "logging_controller_image_repository" { } variable "logging_controller_image_tag" { + # example: 1.6.0 type = string - description = "Container image tag for the logs controller DaemonSet." - default = "1.6.0" + description = "No default: every install pins this deliberately — see VERSIONS.md. Container image tag for the logs controller DaemonSet." + + validation { + condition = var.logging_controller_image_tag != "" && !contains(["latest", "main", "master"], lower(var.logging_controller_image_tag)) + error_message = "logging_controller_image_tag must be a non-empty fixed version, not empty and not a moving reference." + } } ############################################ diff --git a/nullplatform/container_orchestration/aks/main.tf b/nullplatform/container_orchestration/aks/main.tf index fc12c7ac7..16c4d4240 100644 --- a/nullplatform/container_orchestration/aks/main.tf +++ b/nullplatform/container_orchestration/aks/main.tf @@ -35,7 +35,7 @@ locals { }, length(local.resource_management) > 0 ? { resource_management = local.resource_management } : {}, length(local.security) > 0 ? { security = local.security } : {}, - var.traffic_manager_version != "" ? { traffic_manager = { version = var.traffic_manager_version } } : {}, + { traffic_manager = { version = var.traffic_manager_version } }, length(var.object_modifiers) > 0 ? { object_modifiers = { modifiers = var.object_modifiers } } : {}, ) } diff --git a/nullplatform/container_orchestration/aks/tests/aks.tftest.hcl b/nullplatform/container_orchestration/aks/tests/aks.tftest.hcl index d8d4688ca..f27817908 100644 --- a/nullplatform/container_orchestration/aks/tests/aks.tftest.hcl +++ b/nullplatform/container_orchestration/aks/tests/aks.tftest.hcl @@ -1,10 +1,11 @@ mock_provider "nullplatform" {} variables { - nrn = "organization=myorg:account=myaccount" - cluster_name = "my-aks-cluster" - resource_group = "my-aks-resource-group" - public_gateway_name = "istio-ingress" + nrn = "organization=myorg:account=myaccount" + cluster_name = "my-aks-cluster" + resource_group = "my-aks-resource-group" + public_gateway_name = "istio-ingress" + traffic_manager_version = "1.8.0" } run "aks_provider_type" { @@ -77,10 +78,6 @@ run "optional_fields_excluded_when_empty" { error_message = "Attributes should not contain security when not set" } - assert { - condition = !strcontains(nullplatform_provider_config.aks_config.attributes, "traffic_manager") - error_message = "Attributes should not contain traffic_manager when not set" - } assert { condition = !strcontains(nullplatform_provider_config.aks_config.attributes, "object_modifiers") @@ -227,7 +224,7 @@ run "with_traffic_manager" { command = plan variables { - traffic_manager_version = "latest" + traffic_manager_version = "1.8.0" } assert { @@ -236,7 +233,7 @@ run "with_traffic_manager" { } assert { - condition = strcontains(nullplatform_provider_config.aks_config.attributes, "latest") + condition = strcontains(nullplatform_provider_config.aks_config.attributes, "1.8.0") error_message = "Attributes should contain traffic manager version" } } diff --git a/nullplatform/container_orchestration/aks/variables.tf b/nullplatform/container_orchestration/aks/variables.tf index 649910cc9..211e0d862 100644 --- a/nullplatform/container_orchestration/aks/variables.tf +++ b/nullplatform/container_orchestration/aks/variables.tf @@ -85,9 +85,14 @@ variable "service_account_name" { } variable "traffic_manager_version" { - description = "Tag for the traffic manager sidecar container" + # example: 1.8.0 + description = "No default: every install pins this deliberately — see VERSIONS.md. Tag for the traffic manager sidecar container" type = string - default = "" + + validation { + condition = var.traffic_manager_version != "" && !contains(["latest", "main", "master"], lower(var.traffic_manager_version)) + error_message = "traffic_manager_version must be a non-empty fixed version, not empty and not a moving reference." + } } variable "object_modifiers" { diff --git a/nullplatform/container_orchestration/eks/tests/eks.tftest.hcl b/nullplatform/container_orchestration/eks/tests/eks.tftest.hcl index fb53d0ae5..864610593 100644 --- a/nullplatform/container_orchestration/eks/tests/eks.tftest.hcl +++ b/nullplatform/container_orchestration/eks/tests/eks.tftest.hcl @@ -1,8 +1,9 @@ mock_provider "nullplatform" {} variables { - nrn = "organization=myorg:account=myaccount" - cluster_name = "my-eks-cluster" + nrn = "organization=myorg:account=myaccount" + cluster_name = "my-eks-cluster" + traffic_manager_version = "1.8.0" } run "eks_provider_type" { @@ -184,7 +185,7 @@ run "with_traffic_manager" { command = plan variables { - traffic_manager_version = "latest" + traffic_manager_version = "1.8.0" } assert { @@ -193,7 +194,7 @@ run "with_traffic_manager" { } assert { - condition = strcontains(nullplatform_provider_config.eks_config.attributes, "latest") + condition = strcontains(nullplatform_provider_config.eks_config.attributes, "1.8.0") error_message = "Attributes should contain traffic manager version" } @@ -216,7 +217,7 @@ run "with_traffic_manager_port" { } assert { - condition = strcontains(nullplatform_provider_config.eks_config.attributes, "\"version\":\"latest\"") + condition = strcontains(nullplatform_provider_config.eks_config.attributes, "\"version\":\"1.8.0\"") error_message = "Setting the port must not drop the traffic manager version" } } @@ -342,3 +343,24 @@ run "with_all_options" { error_message = "Dimensions should contain Environment=staging" } } + +################################################################################ +# Version pinning +################################################################################ + +# The default used to be "latest", so an apply with no code change could move the deployed +# version. There is no default now -- the caller has to name one -- and this is the guard +# that a moving reference cannot reach the provider config by any route. +run "no_moving_reference_reaches_the_provider_config" { + command = plan + + assert { + condition = !strcontains(nullplatform_provider_config.eks_config.attributes, "latest") + error_message = "no attribute may reference a moving tag" + } + + assert { + condition = strcontains(nullplatform_provider_config.eks_config.attributes, "\"version\":\"1.8.0\"") + error_message = "the version the caller supplied must reach the provider config" + } +} diff --git a/nullplatform/container_orchestration/eks/variables.tf b/nullplatform/container_orchestration/eks/variables.tf index 4ac57957b..a85f0b506 100644 --- a/nullplatform/container_orchestration/eks/variables.tf +++ b/nullplatform/container_orchestration/eks/variables.tf @@ -112,9 +112,14 @@ variable "service_account_name" { } variable "traffic_manager_version" { - description = "Tag for the traffic manager sidecar container" + # example: 1.8.0 + description = "No default: every install pins this deliberately — see VERSIONS.md. Pinned rather than tracking latest: a moving tag means a pod restart can pull a different build with no apply in between. Tag for the traffic manager sidecar container" type = string - default = "latest" + + validation { + condition = var.traffic_manager_version != "" && !contains(["latest", "main", "master"], lower(var.traffic_manager_version)) + error_message = "traffic_manager_version must be a non-empty fixed version, not empty and not a moving reference." + } } variable "traffic_manager_port" { diff --git a/nullplatform/container_orchestration/gke/main.tf b/nullplatform/container_orchestration/gke/main.tf index e7a5b1930..132de9f29 100644 --- a/nullplatform/container_orchestration/gke/main.tf +++ b/nullplatform/container_orchestration/gke/main.tf @@ -30,7 +30,7 @@ locals { }, length(local.resource_management) > 0 ? { resource_management = local.resource_management } : {}, length(local.security) > 0 ? { security = local.security } : {}, - var.traffic_manager_version != "" ? { traffic_manager = { version = var.traffic_manager_version } } : {}, + { traffic_manager = { version = var.traffic_manager_version } }, length(var.object_modifiers) > 0 ? { object_modifiers = { modifiers = var.object_modifiers } } : {}, ) } diff --git a/nullplatform/container_orchestration/gke/tests/gke.tftest.hcl b/nullplatform/container_orchestration/gke/tests/gke.tftest.hcl index f7b24a7d2..028080b9f 100644 --- a/nullplatform/container_orchestration/gke/tests/gke.tftest.hcl +++ b/nullplatform/container_orchestration/gke/tests/gke.tftest.hcl @@ -1,10 +1,11 @@ mock_provider "nullplatform" {} variables { - nrn = "organization=myorg:account=myaccount" - cluster_name = "my-gke-cluster" - location = "us-central1-a" - public_gateway_name = "public-gateway" + nrn = "organization=myorg:account=myaccount" + cluster_name = "my-gke-cluster" + location = "us-central1-a" + public_gateway_name = "public-gateway" + traffic_manager_version = "1.8.0" } run "gke_provider_type" { @@ -72,10 +73,6 @@ run "optional_fields_excluded_when_empty" { error_message = "Attributes should not contain security when not set" } - assert { - condition = !strcontains(nullplatform_provider_config.gke_config.attributes, "traffic_manager") - error_message = "Attributes should not contain traffic_manager when not set" - } assert { condition = !strcontains(nullplatform_provider_config.gke_config.attributes, "object_modifiers") @@ -150,7 +147,7 @@ run "with_traffic_manager" { command = plan variables { - traffic_manager_version = "latest" + traffic_manager_version = "1.8.0" } assert { @@ -159,7 +156,7 @@ run "with_traffic_manager" { } assert { - condition = strcontains(nullplatform_provider_config.gke_config.attributes, "latest") + condition = strcontains(nullplatform_provider_config.gke_config.attributes, "1.8.0") error_message = "Attributes should contain traffic manager version" } } diff --git a/nullplatform/container_orchestration/gke/variables.tf b/nullplatform/container_orchestration/gke/variables.tf index dbd0d72cd..4f51bfb28 100644 --- a/nullplatform/container_orchestration/gke/variables.tf +++ b/nullplatform/container_orchestration/gke/variables.tf @@ -79,9 +79,14 @@ variable "service_account_name" { } variable "traffic_manager_version" { - description = "Tag for the traffic manager sidecar container" + # example: 1.8.0 + description = "No default: every install pins this deliberately — see VERSIONS.md. Tag for the traffic manager sidecar container" type = string - default = "" + + validation { + condition = var.traffic_manager_version != "" && !contains(["latest", "main", "master"], lower(var.traffic_manager_version)) + error_message = "traffic_manager_version must be a non-empty fixed version, not empty and not a moving reference." + } } variable "object_modifiers" { diff --git a/nullplatform/service_definition/variables.tf b/nullplatform/service_definition/variables.tf index 4da756e6d..9b22a344a 100644 --- a/nullplatform/service_definition/variables.tf +++ b/nullplatform/service_definition/variables.tf @@ -33,8 +33,18 @@ variable "repository_name" { variable "repository_branch" { type = string - default = "main" - description = "Branch of the service spec repository to use. Must be a short branch name (e.g. \"main\"), not a full ref." + description = <<-EOT + Git ref of the service spec repository to read, as a short name and not a full ref + (e.g. "v1.4.0"). No default and no recommended value: which spec repository an install + points at is its own choice, so there is no version anyone could pick for it. + + Combine with repository_ref_type, which selects the namespace this name lives in. + EOT + + validation { + condition = var.repository_branch != "" && !contains(["main", "master", "head", "latest"], lower(var.repository_branch)) + error_message = "repository_branch must be a non-empty pinned ref, not empty and not a moving branch." + } } variable "service_path" { @@ -92,7 +102,7 @@ variable "dimensions" { variable "repository_ref_type" { type = string - default = "heads" + default = "tags" description = "Git ref namespace for `repository_branch` on GitHub: \"heads\" for a branch, \"tags\" for a tag, or \"\" to treat it as a raw commit SHA. Defaults to \"heads\", preserving previous behaviour." validation { condition = contains(["heads", "tags", ""], var.repository_ref_type)