You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by vi...@apache.org on 2026/09/30 18:36:49 UTC

(superset-kubernetes-operator) branch main updated: feat(helm): add dnsPolicy and dnsConfig values (#394)

This is an automated email from the ASF dual-hosted git repository.

villebro pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/superset-kubernetes-operator.git


The following commit(s) were added to refs/heads/main by this push:
     new ec2a091  feat(helm): add dnsPolicy and dnsConfig values (#394)
ec2a091 is described below

commit ec2a0916c041751a12fecd120e94ed09bfc9d003
Author: Younsung Lee <cy...@kakao.com>
AuthorDate: Thu Oct 1 03:36:42 2026 +0900

    feat(helm): add dnsPolicy and dnsConfig values (#394)
    
    * feat(helm): add dnsPolicy and dnsConfig values for manager pod
    
    Expose dnsPolicy and dnsConfig on the operator Deployment so installs can
    tune the manager pod resolver, for example lowering ndots to 2 to avoid
    unnecessary cluster search-domain queries for external names. Both are
    unset by default, preserving the Kubernetes ClusterFirst behavior.
    
    Rendering fails when dnsPolicy is None without dnsConfig.nameservers,
    since the API server rejects that pod spec. The schema restricts
    dnsPolicy to the Kubernetes enum and dnsConfig to PodDNSConfig fields.
    
    Signed-off-by: younsl <cy...@kakao.com>
    
    * docs: link dnsPolicy and dnsConfig changelog entry to #394
    
    Signed-off-by: younsl <cy...@kakao.com>
    
    * fix(helm): drop dnsPolicy None nameserver guard test
    
    The API server already rejects a pod spec with dnsPolicy None and no
    nameservers, so the chart does not need to cover that case.
    
    Signed-off-by: younsl <cy...@kakao.com>
    
    * fix(helm): drop dnsPolicy None nameserver guard
    
    Deployment admission already validates the pod template, so helm install
    fails with the API server error directly. Avoid echoing API server
    policy in chart templates.
    
    Signed-off-by: younsl <cy...@kakao.com>
    
    * fix(helm): drop default value from dnsPolicy schema description
    
    Naming the Kubernetes default in the schema risks drifting from upstream.
    
    Signed-off-by: younsl <cy...@kakao.com>
    
    * fix(helm): link corev1.PodDNSConfig from dnsConfig value docs
    
    Signed-off-by: younsl <cy...@kakao.com>
    
    * fix(helm): default unset string values to null
    
    Standardize optional string values on ~ instead of "", matching the
    chart's existing null defaults (revisionHistoryLimit, PDB bounds). The
    schema now accepts null for each of these values and still accepts an
    explicit empty string, so existing overrides keep validating. Default
    rendered manifests are unchanged.
    
    Signed-off-by: younsl <cy...@kakao.com>
    
    ---------
    
    Signed-off-by: younsl <cy...@kakao.com>
---
 charts/superset-operator/README.md                 | 14 ++++----
 charts/superset-operator/templates/deployment.yaml |  7 ++++
 .../tests/__snapshot__/full_options_test.yaml.snap |  5 +++
 .../tests/optout_and_helpers_test.yaml             | 37 +++++++++++++++++--
 .../tests/values/full-options.yaml                 |  7 ++++
 charts/superset-operator/values.schema.json        | 41 ++++++++++++++++++----
 charts/superset-operator/values.yaml               | 28 +++++++++------
 docs/reference/releases.md                         |  2 ++
 8 files changed, 115 insertions(+), 26 deletions(-)

diff --git a/charts/superset-operator/README.md b/charts/superset-operator/README.md
index 5028ae9..d57493e 100644
--- a/charts/superset-operator/README.md
+++ b/charts/superset-operator/README.md
@@ -53,30 +53,32 @@ The chart values schema intentionally allows undeclared top-level keys so shared
 | Key | Type | Default | Description |
 |-----|------|---------|-------------|
 | affinity | object | `{}` | Affinity rules for the manager pod. See [corev1.Affinity](https://pkg.go.dev/k8s.io/api/core/v1#Affinity). |
+| dnsConfig | object | `{}` | DNS config for the manager pod. See [corev1.PodDNSConfig](https://pkg.go.dev/k8s.io/api/core/v1#PodDNSConfig). |
+| dnsPolicy | string | `nil` | DNS policy for the manager pod. |
 | extraManifests | list | `[]` | Additional Kubernetes manifests rendered alongside the chart's own resources. Each entry is a YAML object or a string; both are passed through Helm's `tpl` function so template expressions such as `{{ .Release.Namespace }}` are resolved. Use for release-scoped companion resources (e.g. a cert-manager Certificate, NetworkPolicy, or ExternalSecret). Do not use for shared cluster infrastructure such as Gateway controllers, CRDs, or shared Gateways — those ha [...]
 | healthProbes.livenessProbe | object | `{"httpGet":{"path":"/healthz","port":8081},"initialDelaySeconds":15,"periodSeconds":20}` | Liveness probe for the manager container. See [corev1.Probe](https://pkg.go.dev/k8s.io/api/core/v1#Probe). |
 | healthProbes.readinessProbe | object | `{"httpGet":{"path":"/readyz","port":8081},"initialDelaySeconds":5,"periodSeconds":10}` | Readiness probe for the manager container. See [corev1.Probe](https://pkg.go.dev/k8s.io/api/core/v1#Probe). |
 | image.pullPolicy | string | `"IfNotPresent"` | Image pull policy. |
 | image.repository | string | `"ghcr.io/apache/superset-kubernetes-operator"` | Docker image repository for the operator manager. |
-| image.tag | string | `""` | Image tag. Defaults to the chart's appVersion when empty. |
+| image.tag | string | `nil` | Image tag. Defaults to the chart's appVersion when unset. |
 | imagePullSecrets | list | `[]` | Existing Secrets used to pull the operator image from a private registry. |
 | leaderElection.enabled | bool | `true` | Enable leader election so only one replica is active at a time. Required when `replicas > 1`. |
-| logLevel | string | `""` | Operator log verbosity (`--zap-log-level`). Leave empty for the default (`info`). Set to `debug` (alias `1`) for per-reconcile progress logs, or `2` for trace-level internals. |
-| metrics.certSecretName | string | `""` | Name of a Secret containing `tls.crt`, `tls.key`, and `ca.crt` to use for the metrics server instead of the built-in self-signed certificate. Typically written by cert-manager. |
+| logLevel | string | `nil` | Operator log verbosity (`--zap-log-level`). Leave unset for the default (`info`). Set to `debug` (alias `1`) for per-reconcile progress logs, or `2` for trace-level internals. |
+| metrics.certSecretName | string | `nil` | Name of a Secret containing `tls.crt`, `tls.key`, and `ca.crt` to use for the metrics server instead of the built-in self-signed certificate. Typically written by cert-manager. |
 | metrics.enabled | bool | `true` | Enable the metrics endpoint. |
 | metrics.service.port | int | `8443` | Port the metrics Service listens on. |
 | metrics.serviceMonitor.bearerTokenFile | string | `"/var/run/secrets/kubernetes.io/serviceaccount/token"` | Path to the bearer token Prometheus uses to authenticate to the metrics endpoint. |
 | metrics.serviceMonitor.enabled | bool | `false` | Create a Prometheus Operator ServiceMonitor for the operator metrics endpoint. Requires the prometheus-operator CRDs to be installed. |
 | metrics.serviceMonitor.interval | string | `"30s"` | How often Prometheus scrapes the metrics endpoint. |
 | metrics.serviceMonitor.labels | object | `{}` | Extra labels on the ServiceMonitor so Prometheus Operator selects it (e.g. `{release: prometheus}` for kube-prometheus-stack). |
-| metrics.serviceMonitor.scrapeTimeout | string | `""` | Scrape timeout. Empty inherits Prometheus' default. |
+| metrics.serviceMonitor.scrapeTimeout | string | `nil` | Scrape timeout. Unset inherits Prometheus' default. |
 | metrics.serviceMonitor.tlsConfig | object | `{"insecureSkipVerify":true}` | TLS configuration for the ServiceMonitor scrape. The default trusts any certificate, matching the operator's built-in self-signed flow. Override to scrape over verified TLS. |
 | nodeSelector | object | `{}` | Node selector for the manager pod. |
 | podAnnotations | object | `{}` | Extra annotations to add to the manager pod. |
 | podDisruptionBudget.enabled | bool | `false` | Create a PodDisruptionBudget for the manager pod. Only useful with `replicas >= 2`. |
 | podDisruptionBudget.maxUnavailable | intOrString | `nil` | Maximum unavailable pods (integer or percentage). Defaults to `1` when neither is set. |
 | podDisruptionBudget.minAvailable | intOrString | `nil` | Minimum available pods (integer or percentage). Mutually exclusive with `maxUnavailable`. |
-| podDisruptionBudget.unhealthyPodEvictionPolicy | string | `""` | Unhealthy pod eviction policy (`IfHealthyBudget` or `AlwaysAllow`). Empty uses the Kubernetes default. |
+| podDisruptionBudget.unhealthyPodEvictionPolicy | string | `nil` | Unhealthy pod eviction policy (`IfHealthyBudget` or `AlwaysAllow`). Unset uses the Kubernetes default. |
 | podLabels | object | `{}` | Extra labels to add to the manager pod. |
 | podSecurityContext | object | `{"runAsNonRoot":true,"seccompProfile":{"type":"RuntimeDefault"}}` | Pod-level security context for the manager pod. See [corev1.PodSecurityContext](https://pkg.go.dev/k8s.io/api/core/v1#PodSecurityContext). |
 | replicas | int | `1` | Number of operator manager replicas. |
@@ -86,7 +88,7 @@ The chart values schema intentionally allows undeclared top-level keys so shared
 | securityContext | object | `{"allowPrivilegeEscalation":false,"capabilities":{"drop":["ALL"]},"readOnlyRootFilesystem":true}` | Container-level security context for the manager container. See [corev1.SecurityContext](https://pkg.go.dev/k8s.io/api/core/v1#SecurityContext). |
 | serviceAccount.annotations | object | `{}` | Annotations to add to the ServiceAccount. |
 | serviceAccount.create | bool | `true` | Whether to create a ServiceAccount for the operator. |
-| serviceAccount.name | string | `""` | Name of the ServiceAccount to use. Auto-generated from the release name when empty. |
+| serviceAccount.name | string | `nil` | Name of the ServiceAccount to use. Auto-generated from the release name when unset. |
 | tolerations | list | `[]` | Tolerations for the manager pod. See [corev1.Toleration](https://pkg.go.dev/k8s.io/api/core/v1#Toleration). |
 | topologySpreadConstraints | list | `[]` | Topology spread constraints for the manager pod. See [corev1.TopologySpreadConstraint](https://pkg.go.dev/k8s.io/api/core/v1#TopologySpreadConstraint). |
 | watch | object | `{"namespaces":[],"scope":"cluster"}` | Namespace watch configuration.  `scope: cluster` (default) watches all namespaces; manager RBAC is rendered as ClusterRole + ClusterRoleBinding and requires cluster-scoped RBAC on install.  `scope: namespaces` watches only the namespaces listed in `watch.namespaces` (falls back to the release namespace when empty); manager RBAC is rendered as Role + RoleBinding per watched namespace with no manager ClusterRole. Use on restricted  [...]
diff --git a/charts/superset-operator/templates/deployment.yaml b/charts/superset-operator/templates/deployment.yaml
index 016d558..88dc99d 100644
--- a/charts/superset-operator/templates/deployment.yaml
+++ b/charts/superset-operator/templates/deployment.yaml
@@ -59,6 +59,13 @@ spec:
       securityContext:
         {{- toYaml . | nindent 8 }}
       {{- end }}
+      {{- with .Values.dnsPolicy }}
+      dnsPolicy: {{ . }}
+      {{- end }}
+      {{- with .Values.dnsConfig }}
+      dnsConfig:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
       containers:
         - name: manager
           image: {{ include "superset-operator.image" . }}
diff --git a/charts/superset-operator/tests/__snapshot__/full_options_test.yaml.snap b/charts/superset-operator/tests/__snapshot__/full_options_test.yaml.snap
index d81735c..5e43052 100644
--- a/charts/superset-operator/tests/__snapshot__/full_options_test.yaml.snap
+++ b/charts/superset-operator/tests/__snapshot__/full_options_test.yaml.snap
@@ -91,6 +91,11 @@ renders the full install manifests:
                 - mountPath: /tmp/k8s-metrics-server/metrics-certs
                   name: metrics-certs
                   readOnly: true
+          dnsConfig:
+            options:
+              - name: ndots
+                value: "2"
+          dnsPolicy: ClusterFirst
           imagePullSecrets:
             - name: superset-operator-registry
           nodeSelector:
diff --git a/charts/superset-operator/tests/optout_and_helpers_test.yaml b/charts/superset-operator/tests/optout_and_helpers_test.yaml
index eac37fd..35ccfed 100644
--- a/charts/superset-operator/tests/optout_and_helpers_test.yaml
+++ b/charts/superset-operator/tests/optout_and_helpers_test.yaml
@@ -55,7 +55,7 @@ tests:
     set:
       serviceAccount:
         create: true
-        name: ""
+        name: ~
     template: templates/deployment.yaml
     asserts:
       - equal:
@@ -84,7 +84,7 @@ tests:
     set:
       serviceAccount:
         create: false
-        name: ""
+        name: ~
     asserts:
       - template: templates/deployment.yaml
         equal:
@@ -161,6 +161,39 @@ tests:
           path: spec.replicas
           value: 0
 
+  # --- dnsPolicy / dnsConfig ---
+  - it: omits dnsPolicy and dnsConfig by default
+    template: templates/deployment.yaml
+    asserts:
+      - notExists:
+          path: spec.template.spec.dnsPolicy
+      - notExists:
+          path: spec.template.spec.dnsConfig
+
+  - it: renders dnsPolicy None with nameservers
+    set:
+      dnsPolicy: None
+      dnsConfig:
+        nameservers:
+          - 169.254.20.10
+    template: templates/deployment.yaml
+    asserts:
+      - equal:
+          path: spec.template.spec.dnsPolicy
+          value: None
+      - equal:
+          path: spec.template.spec.dnsConfig.nameservers
+          value:
+            - 169.254.20.10
+
+  - it: rejects an unknown dnsPolicy
+    set:
+      dnsPolicy: ClusterLast
+    template: templates/deployment.yaml
+    asserts:
+      - failedTemplate:
+          errorPattern: "dnsPolicy"
+
   # --- podDisruptionBudget ---
   - it: renders no PDB by default
     template: templates/poddisruptionbudget.yaml
diff --git a/charts/superset-operator/tests/values/full-options.yaml b/charts/superset-operator/tests/values/full-options.yaml
index 0fc5e29..afc272b 100644
--- a/charts/superset-operator/tests/values/full-options.yaml
+++ b/charts/superset-operator/tests/values/full-options.yaml
@@ -124,6 +124,13 @@ topologySpreadConstraints:
       matchLabels:
         control-plane: controller-manager
 
+dnsPolicy: ClusterFirst
+
+dnsConfig:
+  options:
+    - name: ndots
+      value: "2"
+
 podDisruptionBudget:
   enabled: true
   minAvailable: 1
diff --git a/charts/superset-operator/values.schema.json b/charts/superset-operator/values.schema.json
index a4c4256..3479e58 100644
--- a/charts/superset-operator/values.schema.json
+++ b/charts/superset-operator/values.schema.json
@@ -14,7 +14,7 @@
       "additionalProperties": false,
       "properties": {
         "repository": { "type": "string" },
-        "tag": { "type": "string" },
+        "tag": { "type": ["string", "null"] },
         "pullPolicy": {
           "type": "string",
           "enum": ["Always", "IfNotPresent", "Never"]
@@ -57,7 +57,7 @@
       "additionalProperties": false,
       "properties": {
         "create": { "type": "boolean" },
-        "name": { "type": "string" },
+        "name": { "type": ["string", "null"] },
         "annotations": { "type": "object" }
       }
     },
@@ -69,8 +69,9 @@
       }
     },
     "logLevel": {
-      "description": "Operator log verbosity (--zap-log-level): empty = default (info). One of \"info\", \"debug\", \"error\", \"panic\", or an integer level (1 = debug/V1, 2 = V2 trace).",
+      "description": "Operator log verbosity (--zap-log-level): unset = default (info). One of \"info\", \"debug\", \"error\", \"panic\", or an integer level (1 = debug/V1, 2 = V2 trace).",
       "oneOf": [
+        { "type": "null" },
         { "type": "string", "enum": ["", "info", "debug", "error", "panic", "1", "2"] },
         { "type": "integer", "enum": [1, 2] }
       ]
@@ -87,14 +88,14 @@
             "port": { "type": "integer" }
           }
         },
-        "certSecretName": { "type": "string" },
+        "certSecretName": { "type": ["string", "null"] },
         "serviceMonitor": {
           "type": "object",
           "additionalProperties": false,
           "properties": {
             "enabled": { "type": "boolean" },
             "interval": { "type": "string" },
-            "scrapeTimeout": { "type": "string" },
+            "scrapeTimeout": { "type": ["string", "null"] },
             "labels": { "type": "object" },
             "tlsConfig": { "type": "object" },
             "bearerTokenFile": { "type": "string" }
@@ -140,6 +141,32 @@
       "description": "Topology spread constraints for the manager pod. See https://pkg.go.dev/k8s.io/api/core/v1#TopologySpreadConstraint",
       "type": "array"
     },
+    "dnsPolicy": {
+      "description": "DNS policy for the manager pod. Unset uses the Kubernetes default.",
+      "type": ["string", "null"],
+      "enum": [null, "", "ClusterFirst", "ClusterFirstWithHostNet", "Default", "None"]
+    },
+    "dnsConfig": {
+      "description": "DNS resolver settings for the manager pod. See https://pkg.go.dev/k8s.io/api/core/v1#PodDNSConfig",
+      "type": "object",
+      "additionalProperties": false,
+      "properties": {
+        "nameservers": { "type": "array", "items": { "type": "string" } },
+        "searches": { "type": "array", "items": { "type": "string" } },
+        "options": {
+          "type": "array",
+          "items": {
+            "type": "object",
+            "additionalProperties": false,
+            "required": ["name"],
+            "properties": {
+              "name": { "type": "string", "minLength": 1 },
+              "value": { "type": "string" }
+            }
+          }
+        }
+      }
+    },
     "podDisruptionBudget": {
       "description": "PodDisruptionBudget for the manager pod. See https://pkg.go.dev/k8s.io/api/policy/v1#PodDisruptionBudgetSpec",
       "type": "object",
@@ -161,8 +188,8 @@
           ]
         },
         "unhealthyPodEvictionPolicy": {
-          "type": "string",
-          "enum": ["", "IfHealthyBudget", "AlwaysAllow"]
+          "type": ["string", "null"],
+          "enum": [null, "", "IfHealthyBudget", "AlwaysAllow"]
         }
       }
     },
diff --git a/charts/superset-operator/values.yaml b/charts/superset-operator/values.yaml
index fddef08..d4d43c5 100644
--- a/charts/superset-operator/values.yaml
+++ b/charts/superset-operator/values.yaml
@@ -1,8 +1,8 @@
 image:
   # -- Docker image repository for the operator manager.
   repository: ghcr.io/apache/superset-kubernetes-operator
-  # -- Image tag. Defaults to the chart's appVersion when empty.
-  tag: ""  # defaults to .Chart.AppVersion
+  # -- Image tag. Defaults to the chart's appVersion when unset.
+  tag: ~
   # -- Image pull policy.
   pullPolicy: IfNotPresent
 
@@ -33,8 +33,8 @@ resizePolicy: []
 serviceAccount:
   # -- Whether to create a ServiceAccount for the operator.
   create: true
-  # -- Name of the ServiceAccount to use. Auto-generated from the release name when empty.
-  name: ""
+  # -- Name of the ServiceAccount to use. Auto-generated from the release name when unset.
+  name: ~
   # -- Annotations to add to the ServiceAccount.
   annotations: {}
 
@@ -42,9 +42,9 @@ leaderElection:
   # -- Enable leader election so only one replica is active at a time. Required when `replicas > 1`.
   enabled: true
 
-# -- Operator log verbosity (`--zap-log-level`). Leave empty for the default (`info`).
+# -- Operator log verbosity (`--zap-log-level`). Leave unset for the default (`info`).
 # Set to `debug` (alias `1`) for per-reconcile progress logs, or `2` for trace-level internals.
-logLevel: ""
+logLevel: ~
 
 metrics:
   # -- Enable the metrics endpoint.
@@ -54,15 +54,15 @@ metrics:
     port: 8443
   # -- Name of a Secret containing `tls.crt`, `tls.key`, and `ca.crt` to use for the metrics
   # server instead of the built-in self-signed certificate. Typically written by cert-manager.
-  certSecretName: ""
+  certSecretName: ~
   serviceMonitor:
     # -- Create a Prometheus Operator ServiceMonitor for the operator metrics endpoint.
     # Requires the prometheus-operator CRDs to be installed.
     enabled: false
     # -- How often Prometheus scrapes the metrics endpoint.
     interval: 30s
-    # -- Scrape timeout. Empty inherits Prometheus' default.
-    scrapeTimeout: ""
+    # -- Scrape timeout. Unset inherits Prometheus' default.
+    scrapeTimeout: ~
     # -- Extra labels on the ServiceMonitor so Prometheus Operator selects it
     # (e.g. `{release: prometheus}` for kube-prometheus-stack).
     labels: {}
@@ -119,6 +119,12 @@ affinity: {}
 # See [corev1.TopologySpreadConstraint](https://pkg.go.dev/k8s.io/api/core/v1#TopologySpreadConstraint).
 topologySpreadConstraints: []
 
+# -- DNS policy for the manager pod.
+dnsPolicy: ~
+# -- DNS config for the manager pod.
+# See [corev1.PodDNSConfig](https://pkg.go.dev/k8s.io/api/core/v1#PodDNSConfig).
+dnsConfig: {}
+
 podDisruptionBudget:
   # -- Create a PodDisruptionBudget for the manager pod. Only useful with `replicas >= 2`.
   enabled: false
@@ -126,8 +132,8 @@ podDisruptionBudget:
   minAvailable: ~
   # -- (intOrString) Maximum unavailable pods (integer or percentage). Defaults to `1` when neither is set.
   maxUnavailable: ~
-  # -- Unhealthy pod eviction policy (`IfHealthyBudget` or `AlwaysAllow`). Empty uses the Kubernetes default.
-  unhealthyPodEvictionPolicy: ""
+  # -- Unhealthy pod eviction policy (`IfHealthyBudget` or `AlwaysAllow`). Unset uses the Kubernetes default.
+  unhealthyPodEvictionPolicy: ~
 
 # -- Extra annotations to add to the manager pod.
 podAnnotations: {}
diff --git a/docs/reference/releases.md b/docs/reference/releases.md
index 7fa53ff..0d6db87 100644
--- a/docs/reference/releases.md
+++ b/docs/reference/releases.md
@@ -27,12 +27,14 @@ This page tracks notable changes in Apache Superset Kubernetes Operator releases
 
 - Structured metastore, Valkey, and lifecycle seed-source connection fields can now be sourced individually from Kubernetes Secret keys, allowing `Superset` resources to consume provisioner-owned endpoints and credentials without copying discovered values or assembling a separate connection URI ([#369](https://github.com/apache/superset-kubernetes-operator/pull/369)).
 - **Helm `podDisruptionBudget`.** The Helm chart now exposes a `podDisruptionBudget` value that renders a [PodDisruptionBudget](https://kubernetes.io/docs/concepts/workloads/pods/disruptions/#pod-disruption-budgets) for the operator manager pods. It is disabled by default and falls back to `maxUnavailable: 1` when no bound is set. Rendering fails for integer or percentage bounds that would leave zero allowed disruptions at the configured `replicas` (for example `minAvailable: 1` with one [...]
+- **Helm `dnsPolicy` and `dnsConfig`.** The Helm chart now exposes `dnsPolicy` and `dnsConfig` values for the operator manager pod ([#394](https://github.com/apache/superset-kubernetes-operator/pull/394), [@younsl](https://github.com/younsl)).
 
 ### Changed
 
 - **Helm chart requires leader election for multiple replicas.** Rendering now fails when `replicas` is greater than 1 and `leaderElection.enabled` is `false`. Replicas are active/standby behind the [leader election Lease](https://kubernetes.io/docs/concepts/architecture/leases/#leader-election); without it every replica reconciles the same `Superset` resources and races on Jobs, drains, and status. Existing releases with that combination fail `helm upgrade` until leader election is enab [...]
 - Valkey's default port is now applied at runtime, aligning it with the existing structured-metastore behavior, instead of being stored by CRD defaulting. Resources that omit both `valkey.port` and `valkey.portFrom` therefore no longer materialize `port: 6379`, while generated configuration continues to use port 6379. This permits `portFrom` without a defaulted literal conflicting at admission ([#369](https://github.com/apache/superset-kubernetes-operator/pull/369)).
 - Kubernetes support now covers the three newest `kind`-published minor versions instead of two. CI tests Kubernetes 1.37, 1.36, and 1.35 natively, with the experimental `next` lane disabled again ([#317](https://github.com/apache/superset-kubernetes-operator/pull/317)).
+- Optional string values in the Helm chart (`image.tag`, `serviceAccount.name`, `logLevel`, `metrics.certSecretName`, `metrics.serviceMonitor.scrapeTimeout`, `dnsPolicy`, `podDisruptionBudget.unhealthyPodEvictionPolicy`) now default to `~` (null) instead of `""`, matching the chart's other unset values. Rendered output is unchanged, and explicit empty strings are still accepted ([#394](https://github.com/apache/superset-kubernetes-operator/pull/394), [@younsl](https://github.com/younsl)).
 
 ### Fixed