From 8ee86b1fa97a056805d4cf324721c20a43482a6e Mon Sep 17 00:00:00 2001 From: Javier Castiarena Date: Fri, 7 Aug 2026 13:07:49 -0300 Subject: [PATCH 1/3] fix(nullplatform): fail template fetches that do not return 200 A non-2xx response is not an error for the http provider, so a renamed or missing template flowed on as if the response body were the template: gomplate passed it through and jq died with `parse error: Expected string key before ':'`, four layers away from the file that caused it. Since the branch defaults track a moving ref, an upstream rename can break an already-applied state with no change in the caller's configuration. Assert `status_code == 200` on every template fetch in scope_definition, service_definition, scope_definition_agent_association and parameter_storage_definition, so the failure names the URL instead. --- .../parameter_storage_definition/data.tf | 10 +++++ nullplatform/scope_definition/data.tf | 37 +++++++++++++++++++ .../data.tf | 10 +++++ nullplatform/service_definition/data.tf | 31 ++++++++++++++++ 4 files changed, 88 insertions(+) diff --git a/nullplatform/parameter_storage_definition/data.tf b/nullplatform/parameter_storage_definition/data.tf index 062e8e936..768777f8e 100644 --- a/nullplatform/parameter_storage_definition/data.tf +++ b/nullplatform/parameter_storage_definition/data.tf @@ -1,5 +1,15 @@ +# A non-2xx response is NOT an error for the http provider: the body (e.g. +# `404: Not Found`) would flow on as if it were the template and fail later +# while rendering, far from the file that is actually missing. data "http" "parameter_storage_spec_template" { url = "${var.repository_parameter_storage_spec}/${var.repository_parameter_storage_spec_branch}/${var.template_path}" + + lifecycle { + postcondition { + condition = self.status_code == 200 + error_message = "Fetch of ${self.url} returned HTTP ${self.status_code}, expected 200." + } + } } data "external" "parameter_storage_spec" { diff --git a/nullplatform/scope_definition/data.tf b/nullplatform/scope_definition/data.tf index d72d4fee0..0f08e40d8 100644 --- a/nullplatform/scope_definition/data.tf +++ b/nullplatform/scope_definition/data.tf @@ -1,22 +1,59 @@ ################################################################################ # Template Fetching +# +# A non-2xx response is NOT an error for the http provider, so without the +# postconditions below the response body (e.g. `404: Not Found`) flows on as if +# it were the template: gomplate passes it through and jq dies with +# `parse error: Expected string key before ':'`, four layers away from the +# renamed or missing file that actually caused it. The branch defaults track a +# moving ref, so a rename upstream can break an already-applied state without +# anything changing in the caller's configuration — assert 200 and let the +# failure name the URL. ################################################################################ data "http" "service_spec_template" { url = "${var.repository_service_spec}/${var.repository_service_spec_branch}/${var.service_path}/specs/service-spec.json.tpl" + + lifecycle { + postcondition { + condition = self.status_code == 200 + error_message = "Fetch of ${self.url} returned HTTP ${self.status_code}, expected 200." + } + } } data "http" "scope_type_template" { url = "${var.repository_scope_template}/${var.repository_scope_template_branch}/${var.service_path}/specs/scope-type-definition.json.tpl" + + lifecycle { + postcondition { + condition = self.status_code == 200 + error_message = "Fetch of ${self.url} returned HTTP ${self.status_code}, expected 200." + } + } } data "http" "action_templates" { for_each = local.static_action_specs url = "${var.repository_action_templates}/${var.repository_action_templates_branch}/${var.service_path}/specs/actions/${each.key}.json.tpl" + + lifecycle { + postcondition { + condition = self.status_code == 200 + error_message = "Fetch of ${self.url} returned HTTP ${self.status_code}, expected 200. Check that the action name is spelled as the template file in the scope repository." + } + } } data "http" "scope_configuration_template" { count = var.create_scope_configuration ? 1 : 0 url = "${var.repository_scope_template}/${var.repository_scope_template_branch}/${var.service_path}/specs/scope-configuration.json.tpl" + + lifecycle { + postcondition { + condition = self.status_code == 200 + error_message = "Fetch of ${self.url} returned HTTP ${self.status_code}, expected 200." + } + } } # Process service specification template using gomplate with NRN variable diff --git a/nullplatform/scope_definition_agent_association/data.tf b/nullplatform/scope_definition_agent_association/data.tf index 29782e792..621ef87f6 100644 --- a/nullplatform/scope_definition_agent_association/data.tf +++ b/nullplatform/scope_definition_agent_association/data.tf @@ -2,8 +2,18 @@ # Notification Channel Template Fetching ################################################################################ +# A non-2xx response is NOT an error for the http provider: the body (e.g. +# `404: Not Found`) would flow on as if it were the template and fail later +# while rendering, far from the file that is actually missing. data "http" "notification_channel_template" { url = "${var.repository_notification_channel}/${var.repository_notification_channel_branch}/${var.service_path}/specs/notification-channel.json.tpl" + + lifecycle { + postcondition { + condition = self.status_code == 200 + error_message = "Fetch of ${self.url} returned HTTP ${self.status_code}, expected 200." + } + } } ################################################################################ diff --git a/nullplatform/service_definition/data.tf b/nullplatform/service_definition/data.tf index 130352355..71bd424e7 100644 --- a/nullplatform/service_definition/data.tf +++ b/nullplatform/service_definition/data.tf @@ -1,3 +1,13 @@ +################################################################################ +# Template Fetching +# +# A non-2xx response is NOT an error for the http provider, so without the +# postconditions below the response body (e.g. `404: Not Found`) flows on as if +# it were the template and fails much later while rendering, far from the file +# that is actually missing. On private repositories a 401/403/404 here usually +# means the token in `auth_headers` or `repository_branch` is wrong, not that +# the template is gone. +################################################################################ data "http" "service_spec_template" { count = var.git_provider != "local" ? 1 : 0 url = ( @@ -6,6 +16,13 @@ data "http" "service_spec_template" { "${local.gitlab_api_file_prefix}${local.gitlab_path_sep}specs%2Fservice-spec.json.tpl/raw?ref=${var.repository_branch}" ) request_headers = local.auth_headers + + lifecycle { + postcondition { + condition = self.status_code == 200 + error_message = "Fetch of ${self.url} returned HTTP ${self.status_code}, expected 200." + } + } } data "http" "action_templates" { @@ -16,6 +33,13 @@ data "http" "action_templates" { "${local.gitlab_api_file_prefix}${local.gitlab_path_sep}specs%2Factions%2F${each.key}.json.tpl/raw?ref=${var.repository_branch}" ) request_headers = local.auth_headers + + lifecycle { + postcondition { + condition = self.status_code == 200 + error_message = "Fetch of ${self.url} returned HTTP ${self.status_code}, expected 200. Check that the action name is spelled as the template file in the service repository." + } + } } data "http" "link_templates" { @@ -26,4 +50,11 @@ data "http" "link_templates" { "${local.gitlab_api_file_prefix}${local.gitlab_path_sep}specs%2Flinks%2F${each.key}.json.tpl/raw?ref=${var.repository_branch}" ) request_headers = local.auth_headers + + lifecycle { + postcondition { + condition = self.status_code == 200 + error_message = "Fetch of ${self.url} returned HTTP ${self.status_code}, expected 200. Check that the link name is spelled as the template file in the service repository." + } + } } From 1bb1efcd0630baaa7061b6cabb8c99004f69696b Mon Sep 17 00:00:00 2001 From: Javier Castiarena Date: Fri, 7 Aug 2026 14:13:14 -0300 Subject: [PATCH 2/3] chore(nullplatform): silence tflint on three pre-existing unused variables The tflint-unused-declarations job lints every changed module, so this PR surfaced debt it did not introduce: np_api_key in parameter_storage_definition and github_repo_url/github_ref in scope_definition_agent_association are declared and unused on main too. None of the three can be deleted, which is what tflint marks as [Fixable]: - np_api_key is published and passed by consumers for interface consistency across the parameter-storage modules. The provider is configured at the root, which is why nothing inside the module references it. - github_repo_url and github_ref are published inputs with defaults, superseded by repository_notification_channel/_branch, which is what the template fetch actually uses. Dropping either breaks any consumer that sets it. Annotate all three with the rationale inline. Removing them belongs in a major version bump with coordinated consumer updates, not here. --- nullplatform/parameter_storage_definition/variables.tf | 4 ++++ .../scope_definition_agent_association/variables.tf | 7 +++++++ 2 files changed, 11 insertions(+) diff --git a/nullplatform/parameter_storage_definition/variables.tf b/nullplatform/parameter_storage_definition/variables.tf index 0a7364e49..295767e61 100644 --- a/nullplatform/parameter_storage_definition/variables.tf +++ b/nullplatform/parameter_storage_definition/variables.tf @@ -3,6 +3,10 @@ variable "nrn" { type = string } +# Not referenced inside the module (the provider is configured at the root), but published +# and passed by consumers, so deleting it — what tflint marks as [Fixable] — is a breaking +# interface change that belongs in a major bump. +# tflint-ignore: terraform_unused_declarations variable "np_api_key" { description = "nullplatform API key. Kept for interface consistency across the parameter-storage modules; the provider is configured at the root." type = string diff --git a/nullplatform/scope_definition_agent_association/variables.tf b/nullplatform/scope_definition_agent_association/variables.tf index 602ac55c0..577965fe2 100644 --- a/nullplatform/scope_definition_agent_association/variables.tf +++ b/nullplatform/scope_definition_agent_association/variables.tf @@ -16,6 +16,10 @@ variable "override_repo_path" { default = null } +# Not referenced inside the module: the template fetch uses +# repository_notification_channel/_branch. Still a published input with a default, so +# deleting it breaks any consumer that sets it — removal belongs in a major bump. +# tflint-ignore: terraform_unused_declarations variable "github_repo_url" { description = "GitHub repository URL containing scope and action templates" type = string @@ -27,6 +31,9 @@ variable "github_repo_url" { } } +# Same as github_repo_url above: unused here, published with a default, so it cannot be +# dropped without breaking consumers. +# tflint-ignore: terraform_unused_declarations variable "github_ref" { description = "Git reference to use (branch name, tag, or commit SHA)" type = string From 3348c48c2bf35a81cc81760a80b88ccf84d80bf7 Mon Sep 17 00:00:00 2001 From: Javier Castiarena Date: Fri, 7 Aug 2026 15:29:51 -0300 Subject: [PATCH 3/3] style(nullplatform): trim the template-fetch comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same content in half the lines: the why stays, the retelling of the failure chain goes. No functional change — comments only. --- nullplatform/parameter_storage_definition/data.tf | 4 +--- .../parameter_storage_definition/variables.tf | 5 ++--- nullplatform/scope_definition/data.tf | 12 ++++-------- .../scope_definition_agent_association/data.tf | 4 +--- .../scope_definition_agent_association/variables.tf | 8 +++----- nullplatform/service_definition/data.tf | 9 +++------ 6 files changed, 14 insertions(+), 28 deletions(-) diff --git a/nullplatform/parameter_storage_definition/data.tf b/nullplatform/parameter_storage_definition/data.tf index 768777f8e..35bc1fc19 100644 --- a/nullplatform/parameter_storage_definition/data.tf +++ b/nullplatform/parameter_storage_definition/data.tf @@ -1,6 +1,4 @@ -# A non-2xx response is NOT an error for the http provider: the body (e.g. -# `404: Not Found`) would flow on as if it were the template and fail later -# while rendering, far from the file that is actually missing. +# A non-2xx is not an error for the http provider: the body would render as the template. data "http" "parameter_storage_spec_template" { url = "${var.repository_parameter_storage_spec}/${var.repository_parameter_storage_spec_branch}/${var.template_path}" diff --git a/nullplatform/parameter_storage_definition/variables.tf b/nullplatform/parameter_storage_definition/variables.tf index 295767e61..85d0d4ba7 100644 --- a/nullplatform/parameter_storage_definition/variables.tf +++ b/nullplatform/parameter_storage_definition/variables.tf @@ -3,9 +3,8 @@ variable "nrn" { type = string } -# Not referenced inside the module (the provider is configured at the root), but published -# and passed by consumers, so deleting it — what tflint marks as [Fixable] — is a breaking -# interface change that belongs in a major bump. +# Unused here (the provider is configured at the root) but passed by consumers, so +# deleting it is a breaking interface change. # tflint-ignore: terraform_unused_declarations variable "np_api_key" { description = "nullplatform API key. Kept for interface consistency across the parameter-storage modules; the provider is configured at the root." diff --git a/nullplatform/scope_definition/data.tf b/nullplatform/scope_definition/data.tf index 0f08e40d8..73d650db7 100644 --- a/nullplatform/scope_definition/data.tf +++ b/nullplatform/scope_definition/data.tf @@ -1,14 +1,10 @@ ################################################################################ # Template Fetching # -# A non-2xx response is NOT an error for the http provider, so without the -# postconditions below the response body (e.g. `404: Not Found`) flows on as if -# it were the template: gomplate passes it through and jq dies with -# `parse error: Expected string key before ':'`, four layers away from the -# renamed or missing file that actually caused it. The branch defaults track a -# moving ref, so a rename upstream can break an already-applied state without -# anything changing in the caller's configuration — assert 200 and let the -# failure name the URL. +# A non-2xx is not an error for the http provider: without these postconditions +# a missing template's body (`404: Not Found`) renders as the template and dies +# later in jq. Branch defaults track a moving ref, so an upstream rename can +# break an already-applied state. ################################################################################ data "http" "service_spec_template" { url = "${var.repository_service_spec}/${var.repository_service_spec_branch}/${var.service_path}/specs/service-spec.json.tpl" diff --git a/nullplatform/scope_definition_agent_association/data.tf b/nullplatform/scope_definition_agent_association/data.tf index 621ef87f6..8fb463000 100644 --- a/nullplatform/scope_definition_agent_association/data.tf +++ b/nullplatform/scope_definition_agent_association/data.tf @@ -2,9 +2,7 @@ # Notification Channel Template Fetching ################################################################################ -# A non-2xx response is NOT an error for the http provider: the body (e.g. -# `404: Not Found`) would flow on as if it were the template and fail later -# while rendering, far from the file that is actually missing. +# A non-2xx is not an error for the http provider: the body would render as the template. data "http" "notification_channel_template" { url = "${var.repository_notification_channel}/${var.repository_notification_channel_branch}/${var.service_path}/specs/notification-channel.json.tpl" diff --git a/nullplatform/scope_definition_agent_association/variables.tf b/nullplatform/scope_definition_agent_association/variables.tf index 577965fe2..fdd5da169 100644 --- a/nullplatform/scope_definition_agent_association/variables.tf +++ b/nullplatform/scope_definition_agent_association/variables.tf @@ -16,9 +16,8 @@ variable "override_repo_path" { default = null } -# Not referenced inside the module: the template fetch uses -# repository_notification_channel/_branch. Still a published input with a default, so -# deleting it breaks any consumer that sets it — removal belongs in a major bump. +# Unused here (the fetch uses repository_notification_channel/_branch) but published with +# a default, so deleting it breaks any consumer that sets it. # tflint-ignore: terraform_unused_declarations variable "github_repo_url" { description = "GitHub repository URL containing scope and action templates" @@ -31,8 +30,7 @@ variable "github_repo_url" { } } -# Same as github_repo_url above: unused here, published with a default, so it cannot be -# dropped without breaking consumers. +# Same as github_repo_url above. # tflint-ignore: terraform_unused_declarations variable "github_ref" { description = "Git reference to use (branch name, tag, or commit SHA)" diff --git a/nullplatform/service_definition/data.tf b/nullplatform/service_definition/data.tf index 71bd424e7..7d8c31943 100644 --- a/nullplatform/service_definition/data.tf +++ b/nullplatform/service_definition/data.tf @@ -1,12 +1,9 @@ ################################################################################ # Template Fetching # -# A non-2xx response is NOT an error for the http provider, so without the -# postconditions below the response body (e.g. `404: Not Found`) flows on as if -# it were the template and fails much later while rendering, far from the file -# that is actually missing. On private repositories a 401/403/404 here usually -# means the token in `auth_headers` or `repository_branch` is wrong, not that -# the template is gone. +# A non-2xx is not an error for the http provider: without these postconditions +# a missing template's body renders as the template and fails later. On private +# repositories a 401/403/404 usually means the token or branch is wrong. ################################################################################ data "http" "service_spec_template" { count = var.git_provider != "local" ? 1 : 0