diff --git a/packages/apps/kafka/Makefile b/packages/apps/kafka/Makefile index 00baff300..741d018b3 100644 --- a/packages/apps/kafka/Makefile +++ b/packages/apps/kafka/Makefile @@ -3,10 +3,6 @@ PRESET_ENUM := ["nano","micro","small","medium","large","xlarge","2xlarge"] generate: cozyvalues-gen -m 'kafka' -v values.yaml -s values.schema.json -r README.md -g ../../../api/apps/v1alpha1/kafka/types.go - sed -i 's/Enabled bool `json:"enabled,omitempty"`/Enabled *bool `json:"enabled,omitempty"`/' ../../../api/apps/v1alpha1/kafka/types.go - grep -q 'Enabled \*bool `json:"enabled,omitempty"`' ../../../api/apps/v1alpha1/kafka/types.go || (echo "ERROR: failed to patch TLS.Enabled to *bool in types.go" && exit 1) - jq '.properties.tls.properties.enabled.type = ["boolean", "null"]' values.schema.json > values.schema.json.tmp && mv values.schema.json.tmp values.schema.json - jq -e '.properties.tls.properties.enabled.type == ["boolean", "null"]' values.schema.json > /dev/null || (echo "ERROR: jq patch failed" && exit 1) ../../../hack/update-crd.sh test: diff --git a/packages/apps/kafka/templates/_tls.tpl b/packages/apps/kafka/templates/_tls.tpl new file mode 100644 index 000000000..5286a7966 --- /dev/null +++ b/packages/apps/kafka/templates/_tls.tpl @@ -0,0 +1,20 @@ +{{- /* + kafka.tls.enabled — tri-state TLS decision for the external listener (port 9094). + + Resolution order: + 1. If tls.enabled is explicitly set (true or false), that value wins. + 2. If tls.enabled is absent (nil / not provided), TLS inherits from .Values.external. + + Null detection uses `kindIs "invalid"` which is the canonical Helm pattern for + distinguishing an absent/nil value from an explicit false. + + Note: this helper only returns the TLS flag value. Whether the external listener + is rendered at all is controlled by .Values.external in the caller. +*/ -}} +{{- define "kafka.tls.enabled" -}} +{{- if kindIs "invalid" .Values.tls.enabled -}} + {{- .Values.external | default false -}} +{{- else -}} + {{- .Values.tls.enabled -}} +{{- end -}} +{{- end -}} diff --git a/packages/apps/kafka/templates/kafka.yaml b/packages/apps/kafka/templates/kafka.yaml index af0d58824..c17738b31 100644 --- a/packages/apps/kafka/templates/kafka.yaml +++ b/packages/apps/kafka/templates/kafka.yaml @@ -5,21 +5,14 @@ - tls.enabled controls TLS *on the external listener* (independent of whether the listener exists): - If tls.enabled is explicitly set (true or false), that value wins. - - If tls.enabled is unset (nil), TLS inherits from .Values.external + - If tls.enabled is unset (nil / absent), TLS inherits from .Values.external (auto-on when external is true, off otherwise). - tls.enabled does NOT control whether the external listener is created. Setting tls.enabled=true with external=false has no effect on listeners. Internal TLS listener (port 9093) is always on; Strimzi manages the PKI. + Null detection uses `kindIs "invalid"` (canonical pattern) — see _tls.tpl. */ -}} -{{- $tlsMap := .Values.tls | default dict -}} -{{- $tlsEnabledRaw := index $tlsMap "enabled" -}} -{{- $tlsEnabledExplicit := and (hasKey $tlsMap "enabled") (ne $tlsEnabledRaw nil) -}} -{{- $tlsEnabled := false -}} -{{- if $tlsEnabledExplicit -}} - {{- $tlsEnabled = $tlsEnabledRaw -}} -{{- else -}} - {{- $tlsEnabled = .Values.external | default false -}} -{{- end -}} +{{- $tlsEnabled := include "kafka.tls.enabled" . | trim -}} {{- $showExternal := .Values.external -}} apiVersion: kafka.strimzi.io/v1beta2 kind: Kafka diff --git a/packages/apps/kafka/tests/kafka_tls_test.yaml b/packages/apps/kafka/tests/kafka_tls_test.yaml index 708101aac..b549255c3 100644 --- a/packages/apps/kafka/tests/kafka_tls_test.yaml +++ b/packages/apps/kafka/tests/kafka_tls_test.yaml @@ -97,6 +97,56 @@ tests: path: spec.kafka.listeners count: 2 + ######################################## + # kindIs "invalid" guard — Category J # + ######################################## + + # J-1: external=true, tls.enabled absent → inherit path: TLS is on + - it: "kindIs-guard: TLS inherits from external when tls.enabled is absent and external is true" + release: + name: test-kafka + namespace: tenant-test + set: + external: true + asserts: + - contains: + path: spec.kafka.listeners + content: + name: external + port: 9094 + type: loadbalancer + tls: true + + # J-2: external=true, tls.enabled=false → explicit override wins: TLS is off + - it: "kindIs-guard: explicit tls.enabled=false overrides external=true" + release: + name: test-kafka + namespace: tenant-test + set: + external: true + tls.enabled: false + asserts: + - contains: + path: spec.kafka.listeners + content: + name: external + port: 9094 + type: loadbalancer + tls: false + + # J-3: external=false, tls.enabled=true → external gate wins: no listener rendered + - it: "kindIs-guard: external gate suppresses listener even when tls.enabled=true" + release: + name: test-kafka + namespace: tenant-test + set: + external: false + tls.enabled: true + asserts: + - lengthEqual: + path: spec.kafka.listeners + count: 2 + ######################################## # Internal listeners unaffected by TLS # ######################################## diff --git a/packages/apps/kafka/values.schema.json b/packages/apps/kafka/values.schema.json index 4429a27bf..233f6dbc6 100644 --- a/packages/apps/kafka/values.schema.json +++ b/packages/apps/kafka/values.schema.json @@ -8,16 +8,13 @@ "default": false }, "tls": { - "description": "TLS configuration. Strimzi manages the cluster PKI automatically (no cert-manager is involved for this chart): the operator auto-creates `-cluster-ca-cert` and `-clients-ca-cert` secrets, both exposed for client trust setup. The internal TLS listener on 9093 is always on; this toggle only controls the external listener on 9094.", + "description": "TLS configuration. Strimzi manages the cluster PKI automatically (no cert-manager is involved for this chart): the operator auto-creates `\u003crelease\u003e-cluster-ca-cert` and `\u003crelease\u003e-clients-ca-cert` secrets, both exposed for client trust setup. The internal TLS listener on 9093 is always on; this toggle only controls the external listener on 9094.", "type": "object", "default": {}, "properties": { "enabled": { "description": "Enable TLS on the external listener. When unset, inherits the value of `external` (TLS is on when external access is enabled). Warning: setting this to false while external is true exposes Kafka over plaintext on a public IP via LoadBalancer. Strimzi does not provide authentication on this listener unless SCRAM, mTLS, or OAuth is separately configured. Use only in controlled networks.", - "type": [ - "boolean", - "null" - ] + "type": "boolean" } } }, diff --git a/packages/system/kafka-rd/cozyrds/kafka.yaml b/packages/system/kafka-rd/cozyrds/kafka.yaml index 0da167f1b..f100ce03d 100644 --- a/packages/system/kafka-rd/cozyrds/kafka.yaml +++ b/packages/system/kafka-rd/cozyrds/kafka.yaml @@ -8,7 +8,7 @@ spec: plural: kafkas singular: kafka openAPISchema: |- - {"title":"Chart Values","type":"object","properties":{"external":{"description":"Enable external access from outside the cluster.","type":"boolean","default":false},"tls":{"description":"TLS configuration. Strimzi manages the cluster PKI automatically (no cert-manager is involved for this chart): the operator auto-creates `-cluster-ca-cert` and `-clients-ca-cert` secrets, both exposed for client trust setup. The internal TLS listener on 9093 is always on; this toggle only controls the external listener on 9094.","type":"object","default":{},"properties":{"enabled":{"description":"Enable TLS on the external listener. When unset, inherits the value of `external` (TLS is on when external access is enabled). Warning: setting this to false while external is true exposes Kafka over plaintext on a public IP via LoadBalancer. Strimzi does not provide authentication on this listener unless SCRAM, mTLS, or OAuth is separately configured. Use only in controlled networks.","type":["boolean","null"]}}},"topics":{"description":"Topics configuration.","type":"array","default":[],"items":{"type":"object","required":["config","name","partitions","replicas"],"properties":{"config":{"description":"Topic configuration.","type":"object","x-kubernetes-preserve-unknown-fields":true},"name":{"description":"Topic name.","type":"string"},"partitions":{"description":"Number of partitions.","type":"integer"},"replicas":{"description":"Number of replicas.","type":"integer"}}}},"kafka":{"description":"Kafka configuration.","type":"object","default":{},"required":["replicas","resourcesPreset","size","storageClass"],"properties":{"replicas":{"description":"Number of Kafka replicas.","type":"integer","default":3},"resources":{"description":"Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied.","type":"object","default":{},"properties":{"cpu":{"description":"CPU available to each replica.","pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true},"memory":{"description":"Memory (RAM) available to each replica.","pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true}}},"resourcesPreset":{"description":"Default sizing preset used when `resources` is omitted.","type":"string","default":"c1.small","enum":["t1.nano","t1.micro","t1.small","t1.medium","t1.large","t1.xlarge","t1.2xlarge","t1.4xlarge","c1.nano","c1.micro","c1.small","c1.medium","c1.large","c1.xlarge","c1.2xlarge","c1.4xlarge","s1.nano","s1.micro","s1.small","s1.medium","s1.large","s1.xlarge","s1.2xlarge","s1.4xlarge","u1.nano","u1.micro","u1.small","u1.medium","u1.large","u1.xlarge","u1.2xlarge","u1.4xlarge","m1.nano","m1.micro","m1.small","m1.medium","m1.large","m1.xlarge","m1.2xlarge","m1.4xlarge","nano","micro","small","medium","large","xlarge","2xlarge"]},"size":{"description":"Persistent Volume size for Kafka.","default":"10Gi","pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true},"storageClass":{"description":"StorageClass used to store the Kafka data.","type":"string","default":"","x-kubernetes-validations":[{"rule":"self == oldSelf","message":"storageClass is immutable"}]}}},"zookeeper":{"description":"ZooKeeper configuration.","type":"object","default":{},"required":["replicas","resourcesPreset","size","storageClass"],"properties":{"replicas":{"description":"Number of ZooKeeper replicas.","type":"integer","default":3},"resources":{"description":"Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied.","type":"object","default":{},"properties":{"cpu":{"description":"CPU available to each replica.","pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true},"memory":{"description":"Memory (RAM) available to each replica.","pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true}}},"resourcesPreset":{"description":"Default sizing preset used when `resources` is omitted.","type":"string","default":"c1.small","enum":["t1.nano","t1.micro","t1.small","t1.medium","t1.large","t1.xlarge","t1.2xlarge","t1.4xlarge","c1.nano","c1.micro","c1.small","c1.medium","c1.large","c1.xlarge","c1.2xlarge","c1.4xlarge","s1.nano","s1.micro","s1.small","s1.medium","s1.large","s1.xlarge","s1.2xlarge","s1.4xlarge","u1.nano","u1.micro","u1.small","u1.medium","u1.large","u1.xlarge","u1.2xlarge","u1.4xlarge","m1.nano","m1.micro","m1.small","m1.medium","m1.large","m1.xlarge","m1.2xlarge","m1.4xlarge","nano","micro","small","medium","large","xlarge","2xlarge"]},"size":{"description":"Persistent Volume size for ZooKeeper.","default":"5Gi","pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true},"storageClass":{"description":"StorageClass used to store the ZooKeeper data.","type":"string","default":"","x-kubernetes-validations":[{"rule":"self == oldSelf","message":"storageClass is immutable"}]}}}}} + {"title":"Chart Values","type":"object","properties":{"external":{"description":"Enable external access from outside the cluster.","type":"boolean","default":false},"tls":{"description":"TLS configuration. Strimzi manages the cluster PKI automatically (no cert-manager is involved for this chart): the operator auto-creates `-cluster-ca-cert` and `-clients-ca-cert` secrets, both exposed for client trust setup. The internal TLS listener on 9093 is always on; this toggle only controls the external listener on 9094.","type":"object","default":{},"properties":{"enabled":{"description":"Enable TLS on the external listener. When unset, inherits the value of `external` (TLS is on when external access is enabled). Warning: setting this to false while external is true exposes Kafka over plaintext on a public IP via LoadBalancer. Strimzi does not provide authentication on this listener unless SCRAM, mTLS, or OAuth is separately configured. Use only in controlled networks.","type":"boolean"}}},"topics":{"description":"Topics configuration.","type":"array","default":[],"items":{"type":"object","required":["config","name","partitions","replicas"],"properties":{"config":{"description":"Topic configuration.","type":"object","x-kubernetes-preserve-unknown-fields":true},"name":{"description":"Topic name.","type":"string"},"partitions":{"description":"Number of partitions.","type":"integer"},"replicas":{"description":"Number of replicas.","type":"integer"}}}},"kafka":{"description":"Kafka configuration.","type":"object","default":{},"required":["replicas","resourcesPreset","size","storageClass"],"properties":{"replicas":{"description":"Number of Kafka replicas.","type":"integer","default":3},"resources":{"description":"Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied.","type":"object","default":{},"properties":{"cpu":{"description":"CPU available to each replica.","pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true},"memory":{"description":"Memory (RAM) available to each replica.","pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true}}},"resourcesPreset":{"description":"Default sizing preset used when `resources` is omitted.","type":"string","default":"c1.small","enum":["t1.nano","t1.micro","t1.small","t1.medium","t1.large","t1.xlarge","t1.2xlarge","t1.4xlarge","c1.nano","c1.micro","c1.small","c1.medium","c1.large","c1.xlarge","c1.2xlarge","c1.4xlarge","s1.nano","s1.micro","s1.small","s1.medium","s1.large","s1.xlarge","s1.2xlarge","s1.4xlarge","u1.nano","u1.micro","u1.small","u1.medium","u1.large","u1.xlarge","u1.2xlarge","u1.4xlarge","m1.nano","m1.micro","m1.small","m1.medium","m1.large","m1.xlarge","m1.2xlarge","m1.4xlarge","nano","micro","small","medium","large","xlarge","2xlarge"]},"size":{"description":"Persistent Volume size for Kafka.","default":"10Gi","pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true},"storageClass":{"description":"StorageClass used to store the Kafka data.","type":"string","default":"","x-kubernetes-validations":[{"rule":"self == oldSelf","message":"storageClass is immutable"}]}}},"zookeeper":{"description":"ZooKeeper configuration.","type":"object","default":{},"required":["replicas","resourcesPreset","size","storageClass"],"properties":{"replicas":{"description":"Number of ZooKeeper replicas.","type":"integer","default":3},"resources":{"description":"Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied.","type":"object","default":{},"properties":{"cpu":{"description":"CPU available to each replica.","pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true},"memory":{"description":"Memory (RAM) available to each replica.","pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true}}},"resourcesPreset":{"description":"Default sizing preset used when `resources` is omitted.","type":"string","default":"c1.small","enum":["t1.nano","t1.micro","t1.small","t1.medium","t1.large","t1.xlarge","t1.2xlarge","t1.4xlarge","c1.nano","c1.micro","c1.small","c1.medium","c1.large","c1.xlarge","c1.2xlarge","c1.4xlarge","s1.nano","s1.micro","s1.small","s1.medium","s1.large","s1.xlarge","s1.2xlarge","s1.4xlarge","u1.nano","u1.micro","u1.small","u1.medium","u1.large","u1.xlarge","u1.2xlarge","u1.4xlarge","m1.nano","m1.micro","m1.small","m1.medium","m1.large","m1.xlarge","m1.2xlarge","m1.4xlarge","nano","micro","small","medium","large","xlarge","2xlarge"]},"size":{"description":"Persistent Volume size for ZooKeeper.","default":"5Gi","pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true},"storageClass":{"description":"StorageClass used to store the ZooKeeper data.","type":"string","default":"","x-kubernetes-validations":[{"rule":"self == oldSelf","message":"storageClass is immutable"}]}}}}} release: prefix: kafka- labels: