Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# Changelog

## [1.8.1] - 2026-06-25

### Added
- **Generic global service secrets**: Added `harnesscommon.services.renderServiceSecretsEnv` and `harnesscommon.services.generateServiceExternalSecrets`, which render secrets (Kubernetes secrets + External Secrets Operator) declared under `global.services.<service>` as environment variables / `ExternalSecret` CRDs. Env var names are auto-derived from the declared secret keys, so any service can opt in by listing its secrets in `values.yaml`. Each service entry supports an optional `enabled` flag and a `ctxIdentifier` (ESO secret name prefix, defaults to the service key).
- **Dependency filtering**: Both helpers are scoped by a chart-level `serviceSecretDependencies` list (or an explicit `services` argument). A workload only receives the credentials of the services it declares as dependencies; with no dependencies declared, nothing is rendered. This prevents global service credentials from leaking into every workload and avoids duplicate `ExternalSecret` resources across charts in a shared namespace. Resolved via `harnesscommon.services.dependencies`.
- **RFC-1123 safe ESO names**: `harnesscommon.services.esoSecretCtxIdentifier` now kebab-cases the resolved identifier, so a camelCase service key (e.g. `resourceHierarchy`) yields a valid lowercase `ExternalSecret` name (`resource-hierarchy-ext-secret-0`) even when no `ctxIdentifier` is set.

### Removed
- `harnesscommon.services.rhsEnv` has been removed in favor of `harnesscommon.services.renderServiceSecretsEnv`. The only consumer (Resource Hierarchy Service) now uses the generic helper.

## [1.8.0] - 2026-06-22

### Added
Expand Down
1 change: 1 addition & 0 deletions ci/run-tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ run_scenario "Gateway API (policies)" "${VALUES_DIR}/gateway-policies.yaml"
run_scenario "Gateway API (headers)" "${VALUES_DIR}/gateway-headers.yaml"
run_scenario "Gateway API (migration)" "${VALUES_DIR}/gateway-migration.yaml"
run_scenario "Gateway API (per-route overrides)" "${VALUES_DIR}/gateway-per-route-override.yaml"
run_scenario "Service Secrets (generic)" "${VALUES_DIR}/services-secrets.yaml"
echo "All template scenarios passed."

echo ""
Expand Down
6 changes: 3 additions & 3 deletions ci/test-chart/Chart.lock
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
dependencies:
- name: harness-common
repository: file://../../src/common
version: 1.7.2
digest: sha256:0ed6cadfefa49ca3a981733b86861bd32d129c9d8111838dfba28ebb6f48a930
generated: "2026-05-18T10:51:34.093756-06:00"
version: 1.8.1
digest: sha256:89dc9f243c922edca78a670defa3d355f8cbd4cf9ade1f03a45f308bd649ff5c
generated: "2026-06-25T15:42:54.138257+05:30"
38 changes: 38 additions & 0 deletions ci/test-chart/ci-values/services-secrets.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Only resourceHierarchy is a declared dependency: pipeline (a defined but
# non-dependency service) must NOT be rendered, proving the filter works.
serviceSecretDependencies:
- resourceHierarchy
global:
services:
resourceHierarchy:
ctxIdentifier: resource-hierarchy-service
secrets:
kubernetesSecrets: []
secretManagement:
externalSecretsOperator:
- secretStore:
name: my-store
kind: ClusterSecretStore
remoteKeys:
RESOURCE_HIERARCHY_SERVICE_SECRET:
name: rhs/secret
property: token
pipeline:
secrets:
kubernetesSecrets:
- secretName: pipeline-k8s-secret
keys:
PIPELINE_SERVICE_SECRET: pipeline-secret-key
secretManagement:
externalSecretsOperator: []
disabledService:
enabled: false
secrets:
secretManagement:
externalSecretsOperator:
- secretStore:
name: my-store
kind: ClusterSecretStore
remoteKeys:
SHOULD_NOT_RENDER:
name: nope/secret
11 changes: 11 additions & 0 deletions ci/test-chart/templates/services-secrets.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{{- if and .Values.global .Values.global.services }}
apiVersion: v1
kind: ConfigMap
metadata:
name: services-env-test
data:
env: |
{{ include "harnesscommon.services.renderServiceSecretsEnv" (dict "ctx" $) | indent 4 }}
---
{{- include "harnesscommon.services.generateServiceExternalSecrets" (dict "ctx" $) }}
{{- end }}
2 changes: 1 addition & 1 deletion src/common/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ type: library
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 1.8.0
version: 1.8.1


# This is the version number of the application being deployed. This version number should be
Expand Down
188 changes: 162 additions & 26 deletions src/common/templates/_services.tpl
Original file line number Diff line number Diff line change
@@ -1,37 +1,173 @@
{{/*
Generate K8S Env Spec for Resource Hierarchy Service Secret
Resolve the ESO Secret Context Identifier for a global service entry.

Uses the explicit `ctxIdentifier` field on the service entry when present,
otherwise falls back to the service key from `global.services`.

USAGE:
{{- include "harnesscommon.services.rhsEnv" (dict "ctx" $ "variableName" "RESOURCE_HIERARCHY_SERVICE_SECRET") | indent 12 }}
{{- include "harnesscommon.services.esoSecretCtxIdentifier" (dict "ctx" $ "serviceKey" "resourceHierarchy" "serviceCtx" $serviceCtx) }}
*/}}
{{- define "harnesscommon.services.esoSecretCtxIdentifier" }}
{{- $ := .ctx }}
{{- $ctxIdentifier := default .serviceKey (dig "ctxIdentifier" "" .serviceCtx) }}
{{- /* Normalize to a valid lowercase RFC-1123 name (e.g. camelCase service keys like "resourceHierarchy" -> "resource-hierarchy"). */}}
{{- $ctxIdentifier = $ctxIdentifier | kebabcase }}
{{- include "harnesscommon.secrets.globalESOSecretCtxIdentifier" (dict "ctx" $ "ctxIdentifier" $ctxIdentifier) | trim }}
{{- end }}

PARAMETERS:
REQUIRED:
1. ctx - Helm context ($)
{{/*
Collect the unique secret keys declared for a service secrets context.

OPTIONAL:
1. variableName - Override the environment variable name (default: "RESOURCE_HIERARCHY_SERVICE_SECRET")
Inspects both `kubernetesSecrets[].keys` and
`secretManagement.externalSecretsOperator[].remoteKeys`.

EXAMPLE:
env:
{{- include "harnesscommon.services.rhsEnv" (dict "ctx" $) | indent 12 }}
USAGE:
{{- include "harnesscommon.services.secretKeys" (dict "secretsCtx" $serviceSecretsCtx) }}
*/}}
{{- define "harnesscommon.services.rhsEnv" }}
{{- define "harnesscommon.services.secretKeys" }}
{{- $secretKeys := list }}
{{- $secretsCtx := .secretsCtx }}
{{- range (dig "kubernetesSecrets" (list) $secretsCtx) }}
{{- range $key, $value := (dig "keys" (dict) .) }}
{{- $secretKeys = append $secretKeys $key }}
{{- end }}
{{- end }}
{{- range (dig "secretManagement" "externalSecretsOperator" (list) $secretsCtx) }}
{{- range $key, $value := (dig "remoteKeys" (dict) .) }}
{{- $secretKeys = append $secretKeys $key }}
{{- end }}
{{- end }}
{{- $secretKeys | uniq | join "," }}
{{- end }}

{{/*
Resolve the list of global service keys a chart depends on.

The dependency list is sourced (in precedence order) from:
1. an explicit `services` argument (list), when provided
2. `.Values.serviceSecretDependencies` (list)

Returns a comma-joined string of service keys (empty when no dependencies are
declared). This is the filter that ensures a workload only receives the
credentials of the services it actually depends on.

USAGE:
{{- include "harnesscommon.services.dependencies" (dict "ctx" $) }}
*/}}
{{- define "harnesscommon.services.dependencies" }}
{{- $ := .ctx }}
{{- $variableName := default "RESOURCE_HIERARCHY_SERVICE_SECRET" .variableName }}
{{- $globalServicesCtx := $.Values.global.services }}
{{- $rhsCtx := $globalServicesCtx.resourceHierarchy }}

{{- if and $globalServicesCtx $rhsCtx }}
{{- $enabled := dig "enabled" true $rhsCtx }}
{{- if $enabled }}
{{- $globalESOSecretIdentifier := include "harnesscommon.secrets.globalESOSecretCtxIdentifier" (dict "ctx" $ "ctxIdentifier" "resource-hierarchy-service") }}
{{- include "harnesscommon.secrets.manageEnv" (dict
"ctx" $
"variableName" "RESOURCE_HIERARCHY_SERVICE_SECRET"
"overrideEnvName" $variableName
"extKubernetesSecretCtxs" (list $rhsCtx.secrets.kubernetesSecrets)
"esoSecretCtxs" (list (dict "secretCtxIdentifier" $globalESOSecretIdentifier "secretCtx" $rhsCtx.secrets.secretManagement.externalSecretsOperator))
) }}
{{- $dependencies := .services }}
{{- if not $dependencies }}
{{- $dependencies = ($.Values.serviceSecretDependencies | default (list)) }}
{{- end }}
{{- $dependencies | join "," }}
{{- end }}

{{/*
Generic: Render K8S Env Spec for the secrets of the services a chart depends on.

For every dependency declared via `.Values.serviceSecretDependencies` (or the
optional `services` argument), env vars are auto-derived from that service's
declared secret keys (Kubernetes secrets + ESO remoteKeys) and rendered using
the standard secret precedence (ESO > External K8S Secret > Default).

Services NOT listed as a dependency are skipped, so a workload never receives
credentials it does not depend on. When no dependencies are declared, nothing
is rendered.

Each service entry under `global.services` may optionally declare:
- enabled (default: true) whether to render the service secrets
- ctxIdentifier (default: <serviceKey>) prefix used for the ESO secret name

VALUES SHAPE:
serviceSecretDependencies:
- resourceHierarchy
global:
services:
resourceHierarchy:
ctxIdentifier: resource-hierarchy-service
secrets:
kubernetesSecrets: []
secretManagement:
externalSecretsOperator: []

USAGE:
{{- include "harnesscommon.services.renderServiceSecretsEnv" (dict "ctx" $) | indent 12 }}
{{- include "harnesscommon.services.renderServiceSecretsEnv" (dict "ctx" $ "services" (list "resourceHierarchy")) | indent 12 }}
*/}}
{{- define "harnesscommon.services.renderServiceSecretsEnv" }}
{{- $ := .ctx }}
{{- $globalServicesCtx := dict }}
{{- if and $.Values.global $.Values.global.services }}
{{- $globalServicesCtx = $.Values.global.services }}
{{- end }}
{{- $dependenciesStr := include "harnesscommon.services.dependencies" (dict "ctx" $ "services" .services) | trim }}
{{- $dependencies := list }}
{{- if $dependenciesStr }}
{{- $dependencies = splitList "," $dependenciesStr }}
{{- end }}
{{- range $serviceKey, $serviceCtx := $globalServicesCtx }}
{{- if and $serviceCtx (has $serviceKey $dependencies) }}
{{- $enabled := dig "enabled" true $serviceCtx }}
{{- $serviceSecretsCtx := dig "secrets" (dict) $serviceCtx }}
{{- if and $enabled $serviceSecretsCtx }}
{{- $globalESOSecretIdentifier := include "harnesscommon.services.esoSecretCtxIdentifier" (dict "ctx" $ "serviceKey" $serviceKey "serviceCtx" $serviceCtx) }}
{{- $extKubernetesSecretsCtx := dig "kubernetesSecrets" (list) $serviceSecretsCtx }}
{{- $esoSecretsCtx := dig "secretManagement" "externalSecretsOperator" (list) $serviceSecretsCtx }}
{{- $secretKeysStr := include "harnesscommon.services.secretKeys" (dict "secretsCtx" $serviceSecretsCtx) | trim }}
{{- if $secretKeysStr }}
{{- range $variableName := (splitList "," $secretKeysStr) }}
{{- include "harnesscommon.secrets.manageEnv" (dict
"ctx" $
"variableName" $variableName
"extKubernetesSecretCtxs" (list $extKubernetesSecretsCtx)
"esoSecretCtxs" (list (dict "secretCtxIdentifier" $globalESOSecretIdentifier "secretCtx" $esoSecretsCtx))
) }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}

{{/*
Generic: Generate ESO ExternalSecret CRDs for the services a chart is responsible
for materializing.

Like the env helper, the set of services is filtered by
`.Values.serviceSecretDependencies` (or the optional `services` argument), so a
chart only emits the ExternalSecret CRDs it needs. This prevents multiple charts
in a shared namespace from emitting duplicate ExternalSecret resources with the
same name.

For every filtered, enabled service entry with valid ESO secrets, an
ExternalSecret is generated using the service's ESO secret context identifier as
the name prefix.

USAGE:
{{- include "harnesscommon.services.generateServiceExternalSecrets" (dict "ctx" $) }}
{{- include "harnesscommon.services.generateServiceExternalSecrets" (dict "ctx" $ "services" (list "resourceHierarchy")) }}
*/}}
{{- define "harnesscommon.services.generateServiceExternalSecrets" }}
{{- $ := .ctx }}
{{- $globalServicesCtx := dict }}
{{- if and $.Values.global $.Values.global.services }}
{{- $globalServicesCtx = $.Values.global.services }}
{{- end }}
{{- $dependenciesStr := include "harnesscommon.services.dependencies" (dict "ctx" $ "services" .services) | trim }}
{{- $dependencies := list }}
{{- if $dependenciesStr }}
{{- $dependencies = splitList "," $dependenciesStr }}
{{- end }}
{{- range $serviceKey, $serviceCtx := $globalServicesCtx }}
{{- if and $serviceCtx (has $serviceKey $dependencies) }}
{{- $enabled := dig "enabled" true $serviceCtx }}
{{- $serviceSecretsCtx := dig "secrets" (dict) $serviceCtx }}
{{- if and $enabled (eq (include "harnesscommon.secrets.hasESOSecrets" (dict "secretsCtx" $serviceSecretsCtx)) "true") }}
{{- $globalESOSecretIdentifier := include "harnesscommon.services.esoSecretCtxIdentifier" (dict "ctx" $ "serviceKey" $serviceKey "serviceCtx" $serviceCtx) }}
{{- include "harnesscommon.secrets.generateExternalSecret" (dict "secretsCtx" $serviceSecretsCtx "secretNamePrefix" $globalESOSecretIdentifier) }}
{{- print "\n---" }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}