From 2d9374b22eb46c6f54233c9f0b7a76496d642ccb Mon Sep 17 00:00:00 2001 From: Javier Castiarena Date: Mon, 20 Apr 2026 14:19:15 -0300 Subject: [PATCH 1/7] fix(endpoint-exposer): make spec tenant-agnostic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove the hardcoded enum on publicDomain/privateDomain (was ["hello.idp.poc.nullapps.io"], an example FQDN from another tenant's POC). The enum bound every tenant to that FQDN through the UI dropdown; the field is now free-text so each tenant provides its own. A description is added on both fields so developers know what kind of value is expected. - Remove the {{ env.Getenv "SERVICE_NAME" }} and {{ env.Getenv "NRN" }} templates. These are never rendered: the current nullplatform/tofu-modules service_definition module reads the spec via `data "http"` + `jsondecode()` (no gomplate) and resolves `name` and `visible_to` from TF variables (var.service_name, concat([var.nrn], ...)). So the templates "worked" only because other fields happened to be overridden at the TF layer. Leaving them in place invites future authors to add their own {{ env.Getenv }} for non-overridden fields, which would silently fail. The runtime workflow is unaffected — the Istio manifests are assembled from the scope instance's attributes at execution time, not from the spec's example values. --- .../install/specs/service-spec.json.tpl | 46 +++++-------------- 1 file changed, 12 insertions(+), 34 deletions(-) diff --git a/endpoint-exposer/install/specs/service-spec.json.tpl b/endpoint-exposer/install/specs/service-spec.json.tpl index a5c9af7..245b9c7 100644 --- a/endpoint-exposer/install/specs/service-spec.json.tpl +++ b/endpoint-exposer/install/specs/service-spec.json.tpl @@ -1,9 +1,7 @@ { - "name": "{{ env.Getenv \"SERVICE_NAME\" | default \"Endpoint Exposer\" }}", + "name": "Endpoint Exposer", "type": "dependency", - "visible_to": [ - "{{ env.Getenv \"NRN\" }}" - ], + "visible_to": [], "dimensions": {}, "scopes": {}, "assignable_to": "any", @@ -210,22 +208,18 @@ } }, "publicDomain": { - "enum": [ - "hello.idp.poc.nullapps.io" - ], "type": "string", "title": "Public Domain", + "description": "Base domain for routes with visibility=public. Tenant-specific — provide the FQDN that resolves to the public Istio gateway of the target cluster.", "editableOn": [ "create", "update" ] }, "privateDomain": { - "enum": [ - "hello.idp.poc.nullapps.io" - ], "type": "string", "title": "Private Domain", + "description": "Base domain for routes with visibility=private. Tenant-specific — provide the FQDN that resolves to the private (internal) Istio gateway of the target cluster.", "editableOn": [ "create", "update" @@ -464,11 +458,9 @@ } }, "publicDomain": { - "enum": [ - "hello.idp.poc.nullapps.io" - ], "type": "string", "title": "Public Domain", + "description": "Base domain for routes with visibility=public. Tenant-specific — provide the FQDN that resolves to the public Istio gateway of the target cluster.", "target": "publicDomain", "editableOn": [ "create", @@ -476,11 +468,9 @@ ] }, "privateDomain": { - "enum": [ - "hello.idp.poc.nullapps.io" - ], "type": "string", "title": "Private Domain", + "description": "Base domain for routes with visibility=private. Tenant-specific — provide the FQDN that resolves to the private (internal) Istio gateway of the target cluster.", "target": "privateDomain", "editableOn": [ "create", @@ -695,11 +685,9 @@ } }, "publicDomain": { - "enum": [ - "hello.idp.poc.nullapps.io" - ], "type": "string", "title": "Public Domain", + "description": "Base domain for routes with visibility=public. Tenant-specific — provide the FQDN that resolves to the public Istio gateway of the target cluster.", "target": "publicDomain", "editableOn": [ "create", @@ -707,11 +695,9 @@ ] }, "privateDomain": { - "enum": [ - "hello.idp.poc.nullapps.io" - ], "type": "string", "title": "Private Domain", + "description": "Base domain for routes with visibility=private. Tenant-specific — provide the FQDN that resolves to the private (internal) Istio gateway of the target cluster.", "target": "privateDomain", "editableOn": [ "create", @@ -932,22 +918,18 @@ } }, "publicDomain": { - "enum": [ - "hello.idp.poc.nullapps.io" - ], "type": "string", "title": "Public Domain", + "description": "Base domain for routes with visibility=public. Tenant-specific — provide the FQDN that resolves to the public Istio gateway of the target cluster.", "editableOn": [ "create", "update" ] }, "privateDomain": { - "enum": [ - "hello.idp.poc.nullapps.io" - ], "type": "string", "title": "Private Domain", + "description": "Base domain for routes with visibility=private. Tenant-specific — provide the FQDN that resolves to the private (internal) Istio gateway of the target cluster.", "editableOn": [ "create", "update" @@ -1159,22 +1141,18 @@ } }, "publicDomain": { - "enum": [ - "hello.idp.poc.nullapps.io" - ], "type": "string", "title": "Public Domain", + "description": "Base domain for routes with visibility=public. Tenant-specific — provide the FQDN that resolves to the public Istio gateway of the target cluster.", "editableOn": [ "create", "update" ] }, "privateDomain": { - "enum": [ - "hello.idp.poc.nullapps.io" - ], "type": "string", "title": "Private Domain", + "description": "Base domain for routes with visibility=private. Tenant-specific — provide the FQDN that resolves to the private (internal) Istio gateway of the target cluster.", "editableOn": [ "create", "update" From eea5bb472c69344726067a295bd810861cd3b4b8 Mon Sep 17 00:00:00 2001 From: Javier Castiarena Date: Mon, 20 Apr 2026 14:19:41 -0300 Subject: [PATCH 2/7] fix(endpoint-exposer/install): migrate tofu to current tofu-modules API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit install/tofu/main.tf referenced the old variable names of nullplatform/tofu-modules' service_definition module: - git_repo, git_ref, git_service_path, use_tpl_files, git_password Those were removed in tofu-modules v1.52.x. Running `tofu plan` against the current main of tofu-modules (what installation.md tells tenants to clone) fails with "Unsupported argument" on five lines, blocking any tenant that follows the guide verbatim. Alongside, install/tofu was passing `agent_command` and `workflow_override_path` to service_definition_agent_association — neither is accepted by the module. The module builds the cmdline automatically from base_clone_path + repository_service_spec_repo + service_path + "/entrypoint/entrypoint", and exposes `agent_arguments` for passing flags to that entrypoint. Changes: - main.tf: use repository_org/name/branch/service_path + repository_token on service_definition; drop the unsupported agent_command and workflow_override_path on the association module; forward --overrides-path via agent_arguments. - variables.tf: split git_repo into repository_org + repository_name; rename git_branch → repository_branch and git_service_path → spec_path; add agent_service_path so the specs path (which includes "install/" for this service) and the runtime path (just "endpoint-exposer") can differ. Make github_token optional (nullplatform/services is public; the token is only needed for private forks). - terraform.tfvars.example: update to match. - installation.md: update the variables table, note that github_token is optional for the default (public) spec repo, fix the cmdline sample (was missing the trailing /entrypoint), and add a short "Domains" section explaining the free-text contract introduced in the spec cleanup commit. - prerequisites.md: update path-override guidance to the new variable names; clarify github_token is optional. - .terraform.lock.hcl: regenerated — hashicorp/external and hashicorp/null are no longer pulled in (the old module used them; the new one doesn't). Tested: `tofu fmt -check` clean; `tofu init -backend=false && tofu validate` succeeds against nullplatform/tofu-modules main. --- endpoint-exposer/install/installation.md | 35 ++++++----- endpoint-exposer/install/prerequisites.md | 6 +- .../install/tofu/.terraform.lock.hcl | 39 +------------ endpoint-exposer/install/tofu/main.tf | 54 ++++++++--------- .../install/tofu/terraform.tfvars.example | 31 +++++----- endpoint-exposer/install/tofu/variables.tf | 58 +++++++++++-------- 6 files changed, 101 insertions(+), 122 deletions(-) diff --git a/endpoint-exposer/install/installation.md b/endpoint-exposer/install/installation.md index 11cd9d9..17173e4 100644 --- a/endpoint-exposer/install/installation.md +++ b/endpoint-exposer/install/installation.md @@ -22,7 +22,7 @@ git clone https://github.com/nullplatform/services /root/.np/nullplatform/servic git clone https://github.com/nullplatform/tofu-modules /root/.np/nullplatform/tofu-modules ``` -> The `repo_path` variable defaults to `/root/.np/nullplatform/services/endpoint-exposer`. Adjust if you clone elsewhere. +> The agent cmdline resolves to `////entrypoint/entrypoint`, which defaults to `/root/.np/nullplatform/services/endpoint-exposer/entrypoint/entrypoint`. Adjust the variables if you clone elsewhere. ### 2. Configure variables @@ -35,14 +35,18 @@ Edit `terraform.tfvars` with your values: | Variable | Required | Description | |---|---|---| -| `nrn` | ✅ | Nullplatform Resource Name (`organization:account`) | -| `np_api_key` | ✅ | Nullplatform API key | +| `nrn` | ✅ | Nullplatform Resource Name (`organization=:account=`) | +| `np_api_key` | ✅ | Nullplatform API key used by the agent | | `tags_selectors` | ✅ | Tags to select the agent (e.g. `{ environment = "production" }`) | -| `github_token` | ✅ | GitHub token with `contents: read` on `nullplatform/services` | -| `git_branch` | — | Branch to fetch specs from (default: `main`) | -| `repo_path` | — | Path where endpoint-exposer is located on the agent | -| `overrides_enabled` | — | Set `true` to enable config overrides | -| `overrides_repo_path` | — | Full path to the overrides directory on the agent | +| `github_token` | — | Only required if `repository_org`/`repository_name` point at a private fork. Not needed for the public `nullplatform/services` repo. | +| `repository_org` | — | Org that owns the spec repository (default: `nullplatform`) | +| `repository_name` | — | Spec repository name (default: `services`) | +| `repository_branch` | — | Branch to fetch specs from (default: `main`) | +| `spec_path` | — | In-repo path to `specs/service-spec.json.tpl` (default: `endpoint-exposer/install`) | +| `agent_service_path` | — | In-repo path where the agent runtime lives (default: `endpoint-exposer`) | +| `service_name` | — | Display name in nullplatform (default: `Endpoint Exposer`) | +| `overrides_enabled` | — | Set `true` to pass `--overrides-path` to the agent | +| `overrides_repo_path` | — | Absolute path to the overrides directory on the agent (required when `overrides_enabled = true`) | ### 3. Initialize OpenTofu @@ -59,25 +63,28 @@ tofu plan tofu apply ``` +## Domains + +The `publicDomain` / `privateDomain` fields in the service spec are free-text strings. Developers type the concrete FQDN at scope-creation time (via the nullplatform UI, CLI, or API). The base domain must resolve to the appropriate Istio gateway in the target cluster (public or private). + ## Overrides -If the account requires local configuration overrides (e.g. from a networking repo), enable the override flag so the agent appends `--overrides-path` to its command: +If the account requires local configuration overrides (e.g. from a networking repo), enable the override flag so the agent receives `--overrides-path` as an argument: ```hcl overrides_enabled = true overrides_repo_path = "/root/.np/nullplatform/scopes-networking/endpoint-exposer" ``` -This results in the agent running: +The agent cmdline becomes: ``` -/root/.np/nullplatform/services/endpoint-exposer/entrypoint \ - --service-path=/root/.np/nullplatform/services/endpoint-exposer \ +/root/.np/nullplatform/services/endpoint-exposer/entrypoint/entrypoint \ --overrides-path=/root/.np/nullplatform/scopes-networking/endpoint-exposer ``` ## Updating specs -To push spec changes after editing templates in `specs/`: +To push spec changes after editing templates in `install/specs/`: -1. Merge your branch to `main` (or update `git_branch` in tfvars) +1. Merge your branch to `main` (or update `repository_branch` in tfvars) 2. Run `tofu apply` — the module fetches templates from GitHub on each run diff --git a/endpoint-exposer/install/prerequisites.md b/endpoint-exposer/install/prerequisites.md index 3416347..4dd1b77 100644 --- a/endpoint-exposer/install/prerequisites.md +++ b/endpoint-exposer/install/prerequisites.md @@ -8,7 +8,7 @@ The agent pod must have the following repository cloned at the expected path: |---|---| | [nullplatform/services](https://github.com/nullplatform/services) | `/root/.np/nullplatform/services/endpoint-exposer` | -Override the default path via the `repo_path` variable in `terraform.tfvars`. +Override the default path via the `repository_org` / `repository_name` / `agent_service_path` variables in `terraform.tfvars`. ## Required tooling on the agent pod @@ -61,6 +61,6 @@ A `Gateway` resource must exist in the cluster for both public and private traff ## GitHub Token -A GitHub personal access token with `contents: read` permission on the `nullplatform/services` repository is required to fetch spec templates during `tofu apply`. +The `service_definition` module fetches spec templates from GitHub at `tofu apply` time via authenticated or anonymous HTTP. Since `nullplatform/services` is a **public** repository, **no token is required** for the default setup. -Set it in `terraform.tfvars` as `github_token`. +If you point `repository_org` / `repository_name` at a private fork, provide a GitHub personal access token with `contents: read` permission on that repo via the `github_token` variable in `terraform.tfvars`. diff --git a/endpoint-exposer/install/tofu/.terraform.lock.hcl b/endpoint-exposer/install/tofu/.terraform.lock.hcl index df1275e..955d031 100644 --- a/endpoint-exposer/install/tofu/.terraform.lock.hcl +++ b/endpoint-exposer/install/tofu/.terraform.lock.hcl @@ -1,25 +1,9 @@ # This file is maintained automatically by "tofu init". # Manual edits may be lost in future updates. -provider "registry.opentofu.org/hashicorp/external" { - version = "2.3.5" - hashes = [ - "h1:VsIY+hWGvWHaGvGTSKZslY13lPeAtSTxfZRPbpLMMhs=", - "zh:1fb9aca1f068374a09d438dba84c9d8ba5915d24934a72b6ef66ef6818329151", - "zh:3eab30e4fcc76369deffb185b4d225999fc82d2eaaa6484d3b3164a4ed0f7c49", - "zh:4f8b7a4832a68080f0bf4f155b56a691832d8a91ce8096dac0f13a90081abc50", - "zh:5ff1935612db62e48e4fe6cfb83dfac401b506a5b7b38342217616fbcab70ce0", - "zh:993192234d327ec86726041eb6d1efb001e41f32e4518ad8b9b162130b65ee9a", - "zh:ce445e68282a2c4b2d1f994a2730406df4ea47914c0932fb4a7eb040a7ec7061", - "zh:e305e17216840c54194141fb852839c2cedd6b41abd70cf8d606d6e88ed40e64", - "zh:edba65fb241d663c09aa2cbf75026c840e963d5195f27000f216829e49811437", - "zh:f306cc6f6ec9beaf75bdcefaadb7b77af320b1f9b56d8f50df5ebd2189a93148", - "zh:fb2ff9e1f86796fda87e1f122d40568912a904da51d477461b850d81a0105f3d", - ] -} - provider "registry.opentofu.org/hashicorp/http" { - version = "3.5.0" + version = "3.5.0" + constraints = "~> 3.0" hashes = [ "h1:eClUBisXme48lqiUl3U2+H2a2mzDawS9biqfkd9synw=", "zh:0a2b33494eec6a91a183629cf217e073be063624c5d3f70870456ddb478308e9", @@ -35,23 +19,6 @@ provider "registry.opentofu.org/hashicorp/http" { ] } -provider "registry.opentofu.org/hashicorp/null" { - version = "3.2.4" - hashes = [ - "h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=", - "zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3", - "zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb", - "zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2", - "zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4", - "zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d", - "zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6", - "zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072", - "zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447", - "zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58", - "zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80", - ] -} - provider "registry.opentofu.org/integrations/github" { version = "6.11.1" constraints = "~> 6.0" @@ -77,7 +44,7 @@ provider "registry.opentofu.org/integrations/github" { provider "registry.opentofu.org/nullplatform/nullplatform" { version = "0.0.83" - constraints = ">= 0.0.67, < 0.1.0" + constraints = ">= 0.0.67, ~> 0.0.67, < 0.1.0" hashes = [ "h1:OLoy7qBT2p/P9pxLL1QBT/NND3A/ik6JordAv8loU8E=", "zh:0e9c83e413ea5a8b960520805d2bcda2b73fef7532dc9f95f84814b7fb8a0c91", diff --git a/endpoint-exposer/install/tofu/main.tf b/endpoint-exposer/install/tofu/main.tf index 9443186..5984279 100644 --- a/endpoint-exposer/install/tofu/main.tf +++ b/endpoint-exposer/install/tofu/main.tf @@ -6,44 +6,44 @@ module "service_definition" { source = "../../../../tofu-modules/nullplatform/service_definition" - nrn = var.nrn - np_api_key = var.np_api_key - - # Spec templates are fetched from the nullplatform/services GitHub repository - git_repo = var.git_repo - git_ref = var.git_branch - git_service_path = var.git_service_path - use_tpl_files = true - - git_password = var.github_token - - service_name = var.service_name - service_description = var.service_description + nrn = var.nrn + + # Spec templates are fetched from GitHub via HTTP and parsed as JSON. + # The module expects specs at `/specs/service-spec.json.tpl` + # (plus `specs/actions/*.json.tpl` and `specs/links/*.json.tpl` if any). + repository_org = var.repository_org + repository_name = var.repository_name + repository_branch = var.repository_branch + service_path = var.spec_path + repository_token = var.github_token + + service_name = var.service_name } ################################################################################ # Service Definition Agent Association # Creates the notification channel that connects nullplatform events to the agent. +# +# The module constructs the agent cmdline as +# `${base_clone_path}/${repository_service_spec_repo}/${service_path}/entrypoint/entrypoint` +# so the agent must have the repo cloned at that location. `service_path` here +# is the runtime path (e.g. `endpoint-exposer`), NOT the specs path (which +# includes the `install/` prefix for this service). ################################################################################ module "service_definition_agent_association" { source = "../../../../tofu-modules/nullplatform/service_definition_agent_association" - nrn = var.nrn - api_key = var.np_api_key - - service_specification_id = module.service_definition.service_specification_id - service_specification_slug = module.service_definition.service_specification_slug - + nrn = var.nrn + api_key = var.np_api_key tags_selectors = var.tags_selectors - agent_command = { - type = "exec" - data = { - cmdline = "${var.repo_path}/entrypoint" - } - } + service_specification_slug = module.service_definition.service_specification_slug + repository_service_spec_repo = "${var.repository_org}/${var.repository_name}" + service_path = var.agent_service_path - service_path = var.repo_path - workflow_override_path = var.overrides_enabled ? var.overrides_repo_path : null + # Pass `--overrides-path=` to the entrypoint when local config + # overrides are enabled. The entrypoint handles the flag; the module just + # forwards arguments verbatim. + agent_arguments = var.overrides_enabled ? ["--overrides-path=${var.overrides_repo_path}"] : [] } diff --git a/endpoint-exposer/install/tofu/terraform.tfvars.example b/endpoint-exposer/install/tofu/terraform.tfvars.example index 8d04f2e..168cc7d 100644 --- a/endpoint-exposer/install/tofu/terraform.tfvars.example +++ b/endpoint-exposer/install/tofu/terraform.tfvars.example @@ -5,39 +5,36 @@ # Required ################################################################################ -nrn = "organization=2" +nrn = "organization=:account=" np_api_key = "your-nullplatform-api-key" tags_selectors = { environment = "production" } -github_token = "your-github-personal-access-token" +# Only needed if the spec repository is private. nullplatform/services is +# public, so leave commented out unless you're pointing at a fork. +# github_token = "your-github-personal-access-token" ################################################################################ -# Repository (override if using a fork or different branch) +# Repository (override if pointing at a fork or a non-default branch) ################################################################################ -# git_repo = "nullplatform/services" -# git_branch = "main" -# git_service_path = "endpoint-exposer/install" - -################################################################################ -# Agent -################################################################################ - -# repo_path = "/root/.np/nullplatform/services/endpoint-exposer" +# repository_org = "nullplatform" +# repository_name = "services" +# repository_branch = "main" +# spec_path = "endpoint-exposer/install" +# agent_service_path = "endpoint-exposer" ################################################################################ # Service Definition (override to customize the display name) ################################################################################ -# service_name = "Endpoint Exposer" -# service_description = "HTTP routing management via Kubernetes Gateway API" +# service_name = "Endpoint Exposer" ################################################################################ -# Overrides (optional — appends --overrides-path to the agent cmdline) +# Overrides (optional — passes --overrides-path to the agent entrypoint) ################################################################################ -# overrides_enabled = true -# overrides_repo_path = "/root/.np/nullplatform/scopes-networking/endpoint-exposer" +# overrides_enabled = true +# overrides_repo_path = "/root/.np/nullplatform/scopes-networking/endpoint-exposer" diff --git a/endpoint-exposer/install/tofu/variables.tf b/endpoint-exposer/install/tofu/variables.tf index 3c7b481..58c6e2f 100644 --- a/endpoint-exposer/install/tofu/variables.tf +++ b/endpoint-exposer/install/tofu/variables.tf @@ -8,7 +8,7 @@ variable "nrn" { } variable "np_api_key" { - description = "Nullplatform API key for authentication" + description = "Nullplatform API key used by the agent to authenticate with nullplatform" type = string sensitive = true } @@ -19,7 +19,7 @@ variable "tags_selectors" { } variable "github_token" { - description = "GitHub personal access token for fetching spec templates from nullplatform/services" + description = "GitHub token for fetching spec templates. Only required when the spec repository is private." type = string sensitive = true default = null @@ -27,34 +27,40 @@ variable "github_token" { ################################################################################ # Repository +# Where the service specs and runtime code live. Both are assumed to be in the +# same repository; `spec_path` and `agent_service_path` can differ if the specs +# are nested deeper (as is the case for endpoint-exposer with its `install/` +# subdirectory). ################################################################################ -variable "git_repo" { - description = "GitHub repository containing spec templates (org/repo format)" +variable "repository_org" { + description = "GitHub organization owning the service repository" type = string - default = "nullplatform/services" + default = "nullplatform" } -variable "git_branch" { - description = "Git branch to use when fetching spec templates" +variable "repository_name" { + description = "Name of the service repository" + type = string + default = "services" +} + +variable "repository_branch" { + description = "Branch of the service repository to fetch specs from. Must be a short branch name (e.g. \"main\"), not a full ref." type = string default = "main" } -variable "git_service_path" { - description = "Path within the repository where install/specs/ is located" +variable "spec_path" { + description = "Path within the repository where `specs/service-spec.json.tpl` lives (used at registration time by the service_definition module)" type = string default = "endpoint-exposer/install" } -################################################################################ -# Agent -################################################################################ - -variable "repo_path" { - description = "Local path where the endpoint-exposer directory is located on the agent" +variable "agent_service_path" { + description = "Path within the repository where the runtime `entrypoint/entrypoint` lives (used at execution time by the agent)" type = string - default = "/root/.np/nullplatform/services/endpoint-exposer" + default = "endpoint-exposer" } ################################################################################ @@ -67,24 +73,26 @@ variable "service_name" { default = "Endpoint Exposer" } -variable "service_description" { - description = "Description of the service" - type = string - default = "HTTP routing management via Kubernetes Gateway API" -} - ################################################################################ -# Overrides +# Overrides (optional) +# When enabled, the agent receives `--overrides-path=` as +# an argument, so the entrypoint can layer tenant-specific configuration on +# top of the in-repo defaults. ################################################################################ variable "overrides_enabled" { - description = "Append --overrides-path to the agent cmdline for local config overrides" + description = "Append `--overrides-path` to the agent arguments for local config overrides" type = bool default = false } variable "overrides_repo_path" { - description = "Full path to the overrides directory on the agent" + description = "Absolute path inside the agent container where the overrides directory is located. Required when overrides_enabled = true." type = string default = null + + validation { + condition = var.overrides_repo_path == null || startswith(coalesce(var.overrides_repo_path, "/"), "/") + error_message = "overrides_repo_path must be an absolute path (start with /)." + } } From 152db6501e591cee00175b0b7c4cccb41cd9f859 Mon Sep 17 00:00:00 2001 From: Javier Castiarena Date: Mon, 20 Apr 2026 14:39:59 -0300 Subject: [PATCH 3/7] fix(endpoint-exposer/install): override available_links to [] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Discovered when applying the migrated install/tofu module against a clean nullplatform account: `tofu plan` fails with Error: Error in function call ...Call to function "jsondecode" failed: extraneous data after JSON object. nullplatform/tofu-modules' `service_definition` module defaults `available_links = ["connect"]`, which makes it attempt to fetch `/specs/links/connect.json.tpl` via HTTP. Endpoint Exposer is a `type = "dependency"` service that ships no link spec (no `install/specs/links/` directory), so the fetch returns a 404 HTML page — `jsondecode()` then aborts the whole plan with the generic "extraneous data" error, which is hard to map back to the root cause on first encounter. Passing an explicit empty list resolves it cleanly. The same override is now required in any tenant's own Terraform that registers this service without going through install/tofu — see the galicia-banco POC for the downstream mirror. A deeper fix belongs in tofu-modules' `service_definition` (defaulting `available_links` to `[]` when the spec itself doesn't declare any, or deriving it from `spec.available_links`); left out of this PR to keep the scope focused on the endpoint-exposer install flow. --- endpoint-exposer/install/tofu/main.tf | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/endpoint-exposer/install/tofu/main.tf b/endpoint-exposer/install/tofu/main.tf index 5984279..1bb3901 100644 --- a/endpoint-exposer/install/tofu/main.tf +++ b/endpoint-exposer/install/tofu/main.tf @@ -18,6 +18,11 @@ module "service_definition" { repository_token = var.github_token service_name = var.service_name + + # Override the module's default `available_links = ["connect"]`: this service + # doesn't ship a `specs/links/connect.json.tpl`, and fetching a non-existent + # template would make `jsondecode()` abort planning. + available_links = [] } ################################################################################ From ba8ed9811dc97484b29e7acb6c3372420f20335b Mon Sep 17 00:00:00 2001 From: Javier Castiarena Date: Mon, 20 Apr 2026 16:00:55 -0300 Subject: [PATCH 4/7] fix(endpoint-exposer): drop non-existent "environment" from route required MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each route item's JSON Schema listed "environment" in `required` but did NOT declare it under `properties` — only `method`, `path`, `scope`, `visibility`, and `groups`. A route's JSON Schema that requires a field it doesn't define is an instant validation failure for any input, so the UI bounces service creation with: /routes/0 must have required property 'environment' …even when the user has filled in every visible field. `environment` is already a top-level property of the service (populated from the associated scopes' dimensions); the per-route duplicate is a leftover, not intended functionality. Removes "environment" from `routes[].items.required` in all four schemas that duplicate the routes definition (main service attributes + the two action specifications, each with parameters/results schemas). Discovered during the Galicia POC smoke test: registration succeeded end-to-end, but service creation was blocked for every tenant until this is fixed. --- endpoint-exposer/install/specs/service-spec.json.tpl | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/endpoint-exposer/install/specs/service-spec.json.tpl b/endpoint-exposer/install/specs/service-spec.json.tpl index 245b9c7..1d6b921 100644 --- a/endpoint-exposer/install/specs/service-spec.json.tpl +++ b/endpoint-exposer/install/specs/service-spec.json.tpl @@ -143,8 +143,7 @@ "method", "path", "scope", - "visibility", - "environment" + "visibility" ], "properties": { "path": { @@ -618,8 +617,7 @@ "method", "path", "scope", - "visibility", - "environment" + "visibility" ], "properties": { "path": { @@ -853,8 +851,7 @@ "method", "path", "scope", - "visibility", - "environment" + "visibility" ], "properties": { "path": { @@ -1076,8 +1073,7 @@ "method", "path", "scope", - "visibility", - "environment" + "visibility" ], "properties": { "path": { From ffe0ad3d88a0e45bd01107efcccddb9498b7d590 Mon Sep 17 00:00:00 2001 From: Javier Castiarena Date: Mon, 20 Apr 2026 16:22:56 -0300 Subject: [PATCH 5/7] fix(endpoint-exposer): default INGRESS_TYPE=istio, derive SERVICE_PATH MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two runtime bugs that made the service unusable out-of-the-box: 1) `INGRESS_TYPE` defaulted to `alb`, but the repo only ships `workflows/istio/`. Any tenant that didn't explicitly set the env var to `istio` saw: failed to read workflow file: open /workflows/alb/create.yaml: no such file or directory There is no `workflows/alb/` to fall back to — the default was effectively dead. Switching the default to `istio` matches what the repo actually contains; tenants that later add `workflows/alb/` and want ALB can export `INGRESS_TYPE=alb` explicitly. 2) `SERVICE_PATH` was only populated if the agent passed `--service-path=` as an argument. Without it, the path starts empty and every derived path becomes absolute (`/workflows/…`, `/values.yaml`), missing the service-root prefix entirely. The script already computes `WORKING_DIRECTORY` from its own location; the service root is `WORKING_DIRECTORY/..`. Using that as a fallback keeps `--service-path` as an allowed override but removes it as a silent requirement for basic operation. Both fixes are backward-compatible: tenants currently passing `INGRESS_TYPE=alb` + `--service-path=…` keep the same behaviour. --- endpoint-exposer/entrypoint/entrypoint | 9 +++++++++ endpoint-exposer/entrypoint/service | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/endpoint-exposer/entrypoint/entrypoint b/endpoint-exposer/entrypoint/entrypoint index 81c3f2b..45d50d7 100755 --- a/endpoint-exposer/entrypoint/entrypoint +++ b/endpoint-exposer/entrypoint/entrypoint @@ -46,6 +46,15 @@ for arg in "$@"; do esac done +# Fall back to deriving SERVICE_PATH from the script's own location when the +# caller doesn't pass --service-path. WORKING_DIRECTORY points at the +# `entrypoint/` directory inside the service; the parent is the service root +# (e.g. `/root/.np/nullplatform/services/endpoint-exposer`). Without this +# fallback, every absolute path below becomes `/workflows/...` instead of +# `/workflows/...`, and the np CLI fails with +# "failed to read workflow file: open /workflows//.yaml". +SERVICE_PATH="${SERVICE_PATH:-$(cd "$WORKING_DIRECTORY/.." && pwd)}" + OVERRIDES_PATH="${OVERRIDES_PATH:-$SERVICE_PATH/overrides}" export SERVICE_PATH diff --git a/endpoint-exposer/entrypoint/service b/endpoint-exposer/entrypoint/service index 877b2d2..cdf2292 100755 --- a/endpoint-exposer/entrypoint/service +++ b/endpoint-exposer/entrypoint/service @@ -10,7 +10,7 @@ case "$SERVICE_ACTION_TYPE" in ;; esac -INGRESS_TYPE="${INGRESS_TYPE:-alb}" +INGRESS_TYPE="${INGRESS_TYPE:-istio}" echo "INGRESS_TYPE is set to '$INGRESS_TYPE'" echo "OVERRIDES_PATH is set to '$OVERRIDES_PATH'" From fc71f4e14c0ca8b7628c29f28b506e6c40776c7b Mon Sep 17 00:00:00 2001 From: Javier Castiarena Date: Mon, 20 Apr 2026 16:50:47 -0300 Subject: [PATCH 6/7] fix(endpoint-exposer): auto-prepend "/" to Exact/PathPrefix values MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Kubernetes Gateway API rejects non-absolute values for `Exact` and `PathPrefix` match types: spec.rules[N].matches[M].path: Invalid value: "object": value must be an absolute path and start with '/' when type one of ['Exact', 'PathPrefix'] Developers entering "health" in the UI reasonably expect it to be treated as "/health" — the UI doesn't document the requirement, and the scope UI for other nullplatform services is forgiving on this. The rejected route surfaces as a failed kubectl apply deep in the agent workflow, with no hint that the fix is "add a slash". `detect_path_type` now normalizes the value for the two absolute-only types (`Exact`, `PathPrefix`), leaving `RegularExpression` untouched since regex paths are free-form. Also handles the pre-existing edge case where wildcard inputs like `users/*` stripped to `users` (no leading slash); now normalized to `/users`. --- .../scripts/istio/build_ingress_with_rule | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/endpoint-exposer/scripts/istio/build_ingress_with_rule b/endpoint-exposer/scripts/istio/build_ingress_with_rule index b7a997c..2518a4e 100755 --- a/endpoint-exposer/scripts/istio/build_ingress_with_rule +++ b/endpoint-exposer/scripts/istio/build_ingress_with_rule @@ -5,6 +5,21 @@ set -euo pipefail +# Ensure a path starts with "/". The Kubernetes Gateway API rejects +# non-absolute values for the `Exact` and `PathPrefix` match types with: +# spec.rules[N].matches[M].path: Invalid value: ...: value must be an +# absolute path and start with '/' when type one of ['Exact', 'PathPrefix'] +# Developers often enter "health" in the UI expecting it to become "/health", +# so normalize here instead of surfacing a validator error per route. +ensure_absolute_path() { + local path="$1" + if [[ "$path" != /* ]]; then + echo "/$path" + else + echo "$path" + fi +} + # Detect path type and convert path value accordingly # Returns: "type:value" format detect_path_type() { @@ -17,6 +32,7 @@ detect_path_type() { if [[ -z "$prefix_path" ]]; then prefix_path="/" fi + prefix_path=$(ensure_absolute_path "$prefix_path") echo "PathPrefix:$prefix_path" return fi @@ -27,11 +43,14 @@ detect_path_type() { local regex_path="${path//:+([^\/])/[^/]+}" # For bash pattern replacement, we need to handle it differently regex_path=$(echo "$path" | sed 's/:[^/]*/[^\/]+/g') + # RegularExpression values are not required to be absolute by Gateway API, + # so we leave them untouched. echo "RegularExpression:$regex_path" return fi # Default: Exact match + path=$(ensure_absolute_path "$path") echo "Exact:$path" } From 809009cf3b72f48a81a7b7d5eb90c0380b86845e Mon Sep 17 00:00:00 2001 From: Javier Castiarena Date: Mon, 20 Apr 2026 17:46:33 -0300 Subject: [PATCH 7/7] docs(endpoint-exposer/install): note spec fields overridden by TF Clarify that `name` and `visible_to` in service-spec.json.tpl are ignored at apply time because the service_definition module overrides them from TF variables. Prevents future authors from adding `{{ env.Getenv ... }}` template expressions to non-overridden fields, which would reach the nullplatform API as literals (no template engine in the pipeline). Co-Authored-By: Claude Opus 4.7 (1M context) --- endpoint-exposer/install/installation.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/endpoint-exposer/install/installation.md b/endpoint-exposer/install/installation.md index 17173e4..eb265c4 100644 --- a/endpoint-exposer/install/installation.md +++ b/endpoint-exposer/install/installation.md @@ -67,6 +67,17 @@ tofu apply The `publicDomain` / `privateDomain` fields in the service spec are free-text strings. Developers type the concrete FQDN at scope-creation time (via the nullplatform UI, CLI, or API). The base domain must resolve to the appropriate Istio gateway in the target cluster (public or private). +## Spec fields governed by Terraform + +A few top-level fields in `install/specs/service-spec.json.tpl` are **overridden by the `service_definition` module at apply time**, so their value in the `.tpl` is ignored: + +| Spec field | Source at apply time | +|---|---| +| `name` | `var.service_name` | +| `visible_to` | `concat([var.nrn], var.extra_visibile_to_nrns)` | + +Do not add `{{ env.Getenv ... }}` template expressions to other fields expecting runtime substitution — there is no template engine in the pipeline (the module reads the spec with `data "http"` + `jsondecode()`). Any template string in a non-overridden field will reach the nullplatform API as a literal. + ## Overrides If the account requires local configuration overrides (e.g. from a networking repo), enable the override flag so the agent receives `--overrides-path` as an argument: