From c0f3daacfc9a42cfb91b0e743554c1c5d7a5756a Mon Sep 17 00:00:00 2001 From: Soumik Ghosh Date: Wed, 19 Aug 2026 18:11:37 +0530 Subject: [PATCH] Add conditional logic in ingress rendering --- CHANGELOG.md | 5 ++ ci/run-tests.sh | 2 + ci/test-chart/Chart.lock | 6 +- ci/test-chart/ci-values/gateway-when.yaml | 39 +++++++++++ ci/test-chart/ci-values/ingress-when.yaml | 69 +++++++++++++++++++ ci/test-chart/tests/gateway_when_test.yaml | 44 ++++++++++++ ci/test-chart/tests/ingress_when_test.yaml | 42 ++++++++++++ src/common/Chart.yaml | 2 +- src/common/templates/_gateway_httproute.tpl | 2 + src/common/templates/_ingress.tpl | 2 + src/common/templates/_utils.tpl | 75 +++++++++++++++++++++ 11 files changed, 284 insertions(+), 4 deletions(-) create mode 100644 ci/test-chart/ci-values/gateway-when.yaml create mode 100644 ci/test-chart/ci-values/ingress-when.yaml create mode 100644 ci/test-chart/tests/gateway_when_test.yaml create mode 100644 ci/test-chart/tests/ingress_when_test.yaml diff --git a/CHANGELOG.md b/CHANGELOG.md index e44b7a7..2e23ef0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/ci/run-tests.sh b/ci/run-tests.sh index 1d717ce..a88389f 100755 --- a/ci/run-tests.sh +++ b/ci/run-tests.sh @@ -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" diff --git a/ci/test-chart/Chart.lock b/ci/test-chart/Chart.lock index 5cded3b..d946464 100644 --- a/ci/test-chart/Chart.lock +++ b/ci/test-chart/Chart.lock @@ -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" diff --git a/ci/test-chart/ci-values/gateway-when.yaml b/ci/test-chart/ci-values/gateway-when.yaml new file mode 100644 index 0000000..ae1da4b --- /dev/null +++ b/ci/test-chart/ci-values/gateway-when.yaml @@ -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 diff --git a/ci/test-chart/ci-values/ingress-when.yaml b/ci/test-chart/ci-values/ingress-when.yaml new file mode 100644 index 0000000..805d542 --- /dev/null +++ b/ci/test-chart/ci-values/ingress-when.yaml @@ -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 diff --git a/ci/test-chart/tests/gateway_when_test.yaml b/ci/test-chart/tests/gateway_when_test.yaml new file mode 100644 index 0000000..85d3528 --- /dev/null +++ b/ci/test-chart/tests/gateway_when_test.yaml @@ -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 diff --git a/ci/test-chart/tests/ingress_when_test.yaml b/ci/test-chart/tests/ingress_when_test.yaml new file mode 100644 index 0000000..c4f8613 --- /dev/null +++ b/ci/test-chart/tests/ingress_when_test.yaml @@ -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 diff --git a/src/common/Chart.yaml b/src/common/Chart.yaml index 5844cd9..46070e6 100644 --- a/src/common/Chart.yaml +++ b/src/common/Chart.yaml @@ -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 diff --git a/src/common/templates/_gateway_httproute.tpl b/src/common/templates/_gateway_httproute.tpl index c824fc4..382403c 100644 --- a/src/common/templates/_gateway_httproute.tpl +++ b/src/common/templates/_gateway_httproute.tpl @@ -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 */}} @@ -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 */}} diff --git a/src/common/templates/_ingress.tpl b/src/common/templates/_ingress.tpl index c7433dc..a092eee 100644 --- a/src/common/templates/_ingress.tpl +++ b/src/common/templates/_ingress.tpl @@ -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 . }} @@ -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" . }} diff --git a/src/common/templates/_utils.tpl b/src/common/templates/_utils.tpl index c0f72f9..63658dc 100644 --- a/src/common/templates/_utils.tpl +++ b/src/common/templates/_utils.tpl @@ -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 -}}