Skip to content
Merged
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changelog

## [1.9.6] - 2026-08-19

### Added
- **Per-object Ingress / HTTPRoute `when` conditions**: `harnesscommon.utils.evalWhen` gates each `ingress.objects[]` entry (`key`/`equals` default `"true"`, `allOf`, `anyOf`, `not`). If `when` is omitted, the object still renders (backward compatible). When `when` is set, the object is rendered only if the tree evaluates to true. A missing or non-map values path is treated as `"false"` rather than failing the Helm render. The same helper is used for Ingress, HTTPRoute, and HTTPRouteFilter (filters follow the parent object's `when`).

## [1.9.5] - 2026-08-11

### Fixed
Expand Down
2 changes: 2 additions & 0 deletions ci/run-tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ run_scenario "PDB" "${VALUES_DIR}/pdb.yaml"
run_scenario "KEDA" "${VALUES_DIR}/keda.yaml"
run_scenario "Ingress" "${VALUES_DIR}/ingress.yaml"
run_scenario "Ingress (compatibilityHosts)" "${VALUES_DIR}/ingress-compatibility.yaml"
run_scenario "Ingress (when conditions)" "${VALUES_DIR}/ingress-when.yaml"
run_scenario "Gateway API (when conditions)" "${VALUES_DIR}/gateway-when.yaml"
run_scenario "VirtualService" "${VALUES_DIR}/virtualservice.yaml"
run_scenario "JFR" "${VALUES_DIR}/jfr.yaml"
run_scenario "Gateway API (basic)" "${VALUES_DIR}/gateway-basic.yaml"
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.8.3
digest: sha256:0517b50f3921f9f96605e2105852eb90d11e1527cac1896467810218686286e3
generated: "2026-07-24T09:02:56.043624-06:00"
version: 1.9.6
digest: sha256:82422af6ca6edf1d63ba1ee4626a93062141723ab471e18e5f0db7c650504700
generated: "2026-08-19T16:05:30.742282+05:30"
39 changes: 39 additions & 0 deletions ci/test-chart/ci-values/gateway-when.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Per-object when for HTTPRoute and HTTPRouteFilter (same objects as Ingress)
global:
ng:
enabled: true
cg:
enabled: true
ingress:
enabled: true
className: nginx
hosts:
- test.example.com
gatewayAPI:
enabled: true
parentRef:
name: test-gateway
ingress:
objects:
- name: skipped-httproute
when:
not:
key: global.cg.enabled
equals: "true"
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /$2
paths:
- path: /skipped(/|$)(.*)
- name: included-httproute
when:
key: global.ng.enabled
equals: "true"
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /$2
paths:
- path: /ng(/|$)(.*)
- name: always-httproute
paths:
- path: /always
service:
port: 8080
69 changes: 69 additions & 0 deletions ci/test-chart/ci-values/ingress-when.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Scenario: per-object when (allOf / anyOf / not) for harnesscommon.v1.renderIngress
global:
ng:
enabled: true
cg:
enabled: true
featureA:
enabled: false
featureB:
enabled: true
ingress:
enabled: true
className: "nginx"
hosts:
- "test.example.com"
objects: {}
ingress:
objects:
# Skipped: cg.enabled is true, so not equals "true" fails
- name: skipped-when-cg
when:
not:
key: global.cg.enabled
equals: "true"
paths:
- path: /
pathType: Prefix
# Included: ng on AND (featureA OR featureB)
- name: included-allof-anyof
when:
allOf:
- key: global.ng.enabled
equals: "true"
- anyOf:
- key: global.featureA.enabled
equals: "true"
- key: global.featureB.enabled
equals: "true"
paths:
- path: /ng
pathType: Prefix
# Always included (no when)
- name: always-included
paths:
- path: /always
pathType: Prefix
# Included: false values must not be treated as empty
- name: included-equals-false
when:
key: global.featureA.enabled
equals: "false"
paths:
- path: /feature-a-disabled
pathType: Prefix
# Skipped: absent path is treated as false, not a fail
- name: skipped-missing-key
when:
key: global.missing.enabled
paths:
- path: /missing
pathType: Prefix
# Included: absent path equals "false"
- name: included-missing-equals-false
when:
key: global.missing.enabled
equals: "false"
paths:
- path: /missing-off
pathType: Prefix
44 changes: 44 additions & 0 deletions ci/test-chart/tests/gateway_when_test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
suite: HTTPRoute when conditions (harnesscommon.v2.renderHTTPRoute)
values:
- ../values.yaml
- ../ci-values/gateway-when.yaml
templates:
- ingress.yaml
release:
name: harness-common-test
namespace: default
tests:
- it: should skip the false when object (no extra Ingress/HTTPRoute/HTTPRouteFilter)
asserts:
- hasDocuments:
count: 5

- it: should render HTTPRoute when when is true
documentSelector:
path: spec.rules[0].matches[0].path.value
value: /ng(/|$)(.*)
asserts:
- isKind:
of: HTTPRoute
- equal:
path: metadata.name
value: included-httproute

- it: should render HTTPRoute when when is omitted
documentSelector:
path: spec.rules[0].matches[0].path.value
value: /always
asserts:
- isKind:
of: HTTPRoute
- equal:
path: metadata.name
value: always-httproute

- it: should emit HTTPRouteFilter for included rewrite objects
documentSelector:
path: metadata.name
value: included-httproute-ng-ad548b
asserts:
- isKind:
of: HTTPRouteFilter
42 changes: 42 additions & 0 deletions ci/test-chart/tests/ingress_when_test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
suite: Ingress when conditions (harnesscommon.v1.renderIngress)
values:
- ../values.yaml
- ../ci-values/ingress-when.yaml
templates:
- ingress.yaml
release:
name: harness-common-test
namespace: default
tests:
- it: should skip objects whose when evaluates false and render the rest
asserts:
- hasDocuments:
count: 4
- equal:
path: metadata.name
value: included-allof-anyof
documentIndex: 0
- equal:
path: metadata.name
value: always-included
documentIndex: 1
- isKind:
of: Ingress
documentIndex: 0
- isKind:
of: Ingress
documentIndex: 1
- equal:
path: metadata.name
value: included-equals-false
documentIndex: 2
- isKind:
of: Ingress
documentIndex: 2
- equal:
path: metadata.name
value: included-missing-equals-false
documentIndex: 3
- isKind:
of: Ingress
documentIndex: 3
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.9.5
version: 1.9.6


# This is the version number of the application being deployed. This version number should be
Expand Down
2 changes: 2 additions & 0 deletions src/common/templates/_gateway_httproute.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ where $renderedPath is the already-rendered path string.
{{- end }}
{{- if and (dig "gatewayAPI" "enabled" false $.Values.global) (dig "ingress" "enabled" false $.Values.global) -}}
{{- range $index, $object := $ingress.objects }}
{{- if eq (include "harnesscommon.utils.evalWhen" (dict "ctx" $ "when" (dig "when" dict $object)) | trim) "true" }}
{{- $routeName := dig "name" ((cat (coalesce $ingress.name $.Values.nameOverride $.Chart.Name | trunc 63 | trimSuffix "-") "-" $index) | nospace) $object }}
{{- $objectAnnotations := dig "annotations" dict $object }}
{{- /* Print migration suggestions if nginx annotations are detected */}}
Expand Down Expand Up @@ -370,6 +371,7 @@ spec:
{{- end }} {{/* Range over chunk paths */}}
{{- end }} {{/* If to create HTTPRouteFilter */}}
{{- end }} {{/* Range over chunks */}}
{{- end }} {{/* object when */}}
{{- end }} {{/* Range over all the ingress keys */}}
{{- end }} {{/* if gateway / ingress enabled */}}
{{- end }} {{/* define */}}
2 changes: 2 additions & 0 deletions src/common/templates/_ingress.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ resources (HTTPRoute, BackendTrafficPolicy, ClientTrafficPolicy, SecurityPolicy)
{{- end }}
{{- if $.Values.global.ingress.enabled -}}
{{- range $index, $object := $ingress.objects }}
{{- if eq (include "harnesscommon.utils.evalWhen" (dict "ctx" $ "when" (dig "when" dict $object)) | trim) "true" }}
{{- $resolvedHosts := list }}
{{- range $.Values.global.ingress.hosts }}
{{- $resolvedHosts = append $resolvedHosts . }}
Expand Down Expand Up @@ -106,6 +107,7 @@ spec:
---
{{- end }}
{{- end }}
{{- end }}
{{- if and (hasKey .ctx.Values.global "gatewayAPI") (dig "gatewayAPI" "enabled" false .ctx.Values.global) }}
# Gateway API resources (rendered by harnesscommon.v1.renderIngress)
{{- include "harnesscommon.v2.renderHTTPRoute" . }}
Expand Down
75 changes: 75 additions & 0 deletions src/common/templates/_utils.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,78 @@
{{- end -}}
{{- printf "%s" $key -}}
{{- end -}}

{{/*
Look up a dotted values path without failing.

Missing keys, nil intermediates, and non-map intermediates print "false".
Boolean false prints "false" (unlike harnesscommon.utils.getValueFromKey, whose
`default` treats false as empty).

USAGE:
{{ include "harnesscommon.utils.getValueFromKeyOrFalse" (dict "key" "global.ng.enabled" "ctx" $) }}
*/}}
{{- define "harnesscommon.utils.getValueFromKeyOrFalse" -}}
{{- $value := .ctx.Values -}}
{{- $missing := false -}}
{{- range splitList "." .key -}}
{{- if or $missing (not (kindIs "map" $value)) (not (hasKey $value .)) -}}
{{- $missing = true -}}
{{- else -}}
{{- $value = index $value . -}}
{{- end -}}
{{- end -}}
{{- if $missing -}}
false
{{- else -}}
{{- printf "%v" $value -}}
{{- end -}}
{{- end -}}

{{/*
Evaluate a recursive `when` condition for Ingress / HTTPRoute objects.

Supported nodes:
key/equals - compare a values path; equals defaults to "true"
allOf - all child nodes must match
anyOf - at least one child node must match
not - negate a child node

Missing `when` evaluates to true (always render).
A missing or non-map values path evaluates to false (not a render failure).

USAGE:
{{ include "harnesscommon.utils.evalWhen" (dict "ctx" $ "when" $object.when) }}
*/}}
{{- define "harnesscommon.utils.evalWhen" -}}
{{- $ctx := .ctx -}}
{{- $when := .when | default dict -}}
{{- if not $when -}}
true
{{- else if hasKey $when "key" -}}
{{- $actual := include "harnesscommon.utils.getValueFromKeyOrFalse" (dict "key" $when.key "ctx" $ctx) | trim -}}
{{- $expected := dig "equals" "true" $when | toString -}}
{{- if eq $actual $expected -}}true{{- else -}}false{{- end -}}
{{- else if hasKey $when "not" -}}
{{- $result := include "harnesscommon.utils.evalWhen" (dict "ctx" $ctx "when" $when.not) | trim -}}
{{- if eq $result "true" -}}false{{- else -}}true{{- end -}}
{{- else if hasKey $when "allOf" -}}
{{- $matches := true -}}
{{- range $when.allOf -}}
{{- if ne (include "harnesscommon.utils.evalWhen" (dict "ctx" $ctx "when" .) | trim) "true" -}}
{{- $matches = false -}}
{{- end -}}
{{- end -}}
{{- if $matches -}}true{{- else -}}false{{- end -}}
{{- else if hasKey $when "anyOf" -}}
{{- $matches := false -}}
{{- range $when.anyOf -}}
{{- if eq (include "harnesscommon.utils.evalWhen" (dict "ctx" $ctx "when" .) | trim) "true" -}}
{{- $matches = true -}}
{{- end -}}
{{- end -}}
{{- if $matches -}}true{{- else -}}false{{- end -}}
{{- else -}}
true
{{- end -}}
{{- end -}}
Loading