Skip to content

fix(opensearch-operator): align DNS_BASE with the platform cluster domain - #3357

Closed
Alekperov Fuad (fuad00) wants to merge 1 commit into
cozystack:mainfrom
fuad00:fix/opensearch-operator-dns-base
Closed

Alekperov Fuad (fuad00) wants to merge 1 commit into
cozystack:mainfrom
fuad00:fix/opensearch-operator-dns-base

Conversation

@fuad00

@fuad00 Alekperov Fuad (fuad00) commented Jul 19, 2026 •

Copy link
Copy Markdown

Problem

The OpenSearch operator builds in-cluster service FQDNs from its DNS_BASE env var:

  • the securityadmin bootstrap job connects to <svc>.<ns>.svc.<DNS_BASE>
  • the OpenSearch Dashboards Deployment points OPENSEARCH_HOSTS at the same suffix

The vendored upstream chart defaults DNS_BASE to cluster.local, and the vendored subchart has no way to learn the actual cluster domain: _cluster is a plain value that Helm does not propagate into subcharts, and the wrapper chart's values.yaml is not templated.

On a cozy.local cluster those names never resolve:

  • the *-securityconfig-update job loops on Waiting to connect to the cluster (getaddrinfo ENOTFOUND ...svc.cluster.local), so the .opendistro_security index is never created. OpenSearch nodes then fail readiness with Not yet initialized (you may need to run securityadmin) and the StatefulSet never reaches Ready.
  • OpenSearch Dashboards crash-loop on [ConnectionError]: getaddrinfo ENOTFOUND ...svc.cluster.local.

Fix

Rather than hardcode cozy.local (which would mirror the same failure onto clusters that keep cluster.local, e.g. Cozystack layered on an existing kubeadm/k3s/RKE2), make the operator follow the platform's configurable cluster domain:

  • Platform emits global.clusterDomain (from networking.clusterDomain) in the cozystack-values Secret. Unlike _cluster, Helm's reserved global key propagates into subcharts.
  • Operator patch (extended in leaderElection.diff) reads DNS_BASE from .Values.global.clusterDomain, falling back to the upstream manager.dnsBase default (cluster.local) when unset, so a bare helm install is unchanged.

Testing

  • helm template renders DNS_BASE: cozy.local on the default install, cluster.local when global.clusterDomain=cluster.local, and cluster.local with no global (bare install).
  • The helm-unittest suite (tests/dns_base_test.yaml) pins that the operator follows the platform domain (an arbitrary example.test propagates) and falls back to the upstream default — not that it equals any specific string. helm unittest . → 2 suites / 4 tests pass.
fix(opensearch-operator): resolve DNS_BASE from the platform cluster domain

Summary by CodeRabbit

  • Bug Fixes

    • OpenSearch Operator now uses the platform-configured cluster domain for in-cluster service DNS resolution.
    • Added a fallback to cluster.local when no custom cluster domain is configured.
    • Improves reliability for security bootstrap and OpenSearch Dashboards configuration in custom-domain environments.
  • Tests

    • Added coverage verifying both custom cluster domains and the default fallback behavior.

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request addresses a service resolution issue where the OpenSearch operator was failing to connect to internal services due to a mismatch between the default cluster.local domain and the platform's cozy.local domain. By overriding the DNS_BASE configuration, the operator can now correctly resolve FQDNs for bootstrap jobs and dashboard connections, preventing initialization failures and crash-loops.

Highlights

  • DNS Configuration: Updated the DNS_BASE environment variable in the OpenSearch operator to use cozy.local instead of the default cluster.local to ensure proper service resolution within the Cozystack environment.
  • Testing: Added a new helm-unittest case to verify that DNS_BASE is correctly set to cozy.local in the deployment template.
New Features

🧠 You can now enable Memory (public preview) to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment Gemini (@gemini-code-assist) Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on Gemini (@gemini-code-assist) comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution. ↩

@github-actions github-actions Bot added size/M This PR changes 30-99 lines, ignoring generated files area/uncategorized PR auto-labeler could not map title scope to a known area/*; please review kind/bug Categorizes issue or PR as related to a bug labels Jul 19, 2026
@dosubot dosubot Bot added the area/database Issues or PRs related to managed databases (postgres, mariadb, redis, etcd, kafka, clickhouse) label Jul 19, 2026
@coderabbitai

coderabbitai Bot commented Jul 19, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The platform now propagates networking.clusterDomain as global.clusterDomain to the OpenSearch operator. The controller manager uses it for DNS_BASE, falls back to cluster.local, and adds Helm tests for both behaviors.

Changes

OpenSearch DNS configuration

Layer / File(s) Summary
Propagate cluster domain to operator DNS_BASE
packages/core/platform/templates/apps.yaml, packages/system/opensearch-operator/charts/..., packages/system/opensearch-operator/patches/leaderElection.diff
The platform emits global.clusterDomain; the controller manager uses it for DNS_BASE and falls back to manager.dnsBase.
Validate rendered DNS_BASE behavior
packages/system/opensearch-operator/tests/dns_base_test.yaml
Helm tests verify the configured cluster domain and the cluster.local fallback.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Platform
  participant HelmValues
  participant ControllerManager
  Platform->>HelmValues: Emit global.clusterDomain
  HelmValues->>ControllerManager: Render DNS_BASE
  ControllerManager-->>ControllerManager: Fall back to cluster.local when unset
Loading

Suggested reviewers: androndo

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly captures the main change: aligning the OpenSearch operator DNS_BASE with the platform cluster domain.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request configures the dnsBase parameter to cozy.local in the opensearch-operator values and adds a corresponding test suite to ensure DNS_BASE matches the platform's cluster domain instead of the upstream default cluster.local. The reviewer noted that the PR description is missing the required release-note block as specified in the repository guidelines.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +11 to +21
# The operator builds in-cluster service FQDNs from DNS_BASE — the
# securityadmin bootstrap job (`-h <svc>.<ns>.svc.<DNS_BASE>`) and the
# OpenSearch Dashboards OPENSEARCH_HOSTS. The upstream chart defaults
# DNS_BASE to cluster.local, but Cozystack's cluster domain is
# cozy.local, so those names fail to resolve: the securityconfig job
# can never reach the cluster (no `.opendistro_security` index → nodes
# stay NotReady) and Dashboards crash-loops on ENOTFOUND. Align
# DNS_BASE with the platform's default cluster domain, matching what
# the opensearch app chart already uses everywhere
# (`.Values._cluster.cluster-domain | default "cozy.local"`).
dnsBase: cozy.local

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The PR body is missing the required release-note block. According to the repository guidelines, every PR must contain a release note block in the following format:

```release-note
type(scope): human-readable changelog entry

Please update the PR description to include this block.

<details>
<summary>References</summary>

1. PR body must contain a release note block: ```release-note ... ```. Flag any PR whose body has no release-note block. <sup>([link](https://github.com/cozystack/cozystack/blob/main/.gemini/styleguide.md))</sup>
</details>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The bug you are fixing is real, and I want to start by confirming that properly, because the obvious objection is "if DNS_BASE were wrong, CI would be red".

CI is not red because there is no opensearch end-to-end suite at all. The chainsaw directory has 21 suites and opensearch is not among them, and the string does not appear anywhere under the CI configuration. The operator is deployed, but DNS_BASE has only two consumers and both are per-cluster: the -h argument of the securityconfig-update Job, and OPENSEARCH_HOSTS for Dashboards. No OpenSearchCluster is ever created in CI, so neither is reached. The wait loop in the securityconfig job also has no attempt limit, which is exactly the symptom you describe. So the fix is warranted and the absence of a red check proves nothing.

The override is also in the right place: it lands in the parent values.yaml, leaves charts/ alone, and survives make update.

What blocks it is the other direction of the same bug. Replacing cluster.local with a hardcoded cozy.local fixes the default install and breaks a non-default one. networking.clusterDomain is configurable, and a Cozystack layered on an existing kubeadm, k3s or RKE2 cluster keeps cluster.local, where OpenSearch works today precisely because the upstream default happens to match. After this change those installs get the identical failure, mirrored. The new test then pins that behaviour, which is the part I would most like to avoid: a test asserting the hardcoded value makes the regression look intentional to whoever reads it next.

To be fair to you, and this is why I am not simply saying no: four sibling packages already hardcode the same domain in the same way, and the same defect is already tracked in #2348 for the mariadb operator. So this PR is not out of line with the codebase, it is consistent with a pattern that is itself the problem. I also confirmed the constraint you were up against: _cluster is not visible to subcharts and values.yaml is not templated, so there is no values-only fix here. That rules out the easy answer rather than the goal.

The direction that actually resolves it is to emit global.clusterDomain from the platform templates, since Helm propagates global into subcharts, and extend the existing patch so the vendored template falls back to it. A test case with a non-cozy.local domain would then pin the property you want, which is that the operator follows the platform, rather than pinning a specific string.

If a maintainer would rather take this as-is and fold it into #2348 with the four siblings, that is a defensible call and I would not stand in the way. My preference is to fix it once here, because you have already done the investigation.

Two mechanical things regardless of which way that goes. The commit has no Signed-off-by, which blocks merge, and the PR is missing the release-note block the template asks for. Also worth knowing: none of the workflows have actually run on this, they are all sitting in action_required because it is from a fork, so the green you and I both saw is absence of results rather than success. I ran the chart's unit tests locally and they pass, and the template does render the intended value.

…domain

The operator builds in-cluster service FQDNs from its DNS_BASE env var: the
securityadmin bootstrap job connects to `<svc>.<ns>.svc.<DNS_BASE>` and the
OpenSearch Dashboards Deployment points OPENSEARCH_HOSTS at the same suffix.
The upstream chart defaults DNS_BASE to cluster.local, and the vendored
subchart has no way to learn the real domain: `_cluster` is a plain value that
Helm does not propagate into subcharts, and the wrapper's values.yaml is not
templated. On a cozy.local cluster the securityconfig job then never reaches
the cluster (the `.opendistro_security` index is never created, so OpenSearch
nodes stay NotReady) and Dashboards crash-loop on ENOTFOUND.

Emit the configurable cluster domain as `global.clusterDomain` from the
platform values Secret -- Helm propagates `global` into every package's
subcharts -- and extend the vendored operator patch to read DNS_BASE from it,
falling back to the upstream `manager.dnsBase` default. This follows
networking.clusterDomain instead of hardcoding a value, so a cluster that
keeps cluster.local (e.g. Cozystack layered on an existing kubeadm/k3s/RKE2)
keeps working. The unit test pins that the operator tracks the platform
domain rather than any specific string.

Signed-off-by: fuad00 <[email protected]>
@fuad00
Alekperov Fuad (fuad00) force-pushed the fix/opensearch-operator-dns-base branch from 467665f to d37e295 Compare July 30, 2026 03:30
@fuad00

Copy link
Copy Markdown
Author

Thank you — this is one of the most useful reviews I've had, and you're right on every point. I went with fixing it properly here rather than folding into #2348.

What changed:

  • The platform now emits global.clusterDomain (from networking.clusterDomain) in the cozystack-values Secret, alongside _cluster. Since Helm propagates the reserved global key into subcharts, the vendored operator can finally see the domain — which _cluster couldn't reach, exactly as you noted.
  • The operator patch (extended in leaderElection.diff) now reads DNS_BASE from .Values.global.clusterDomain, falling back to the upstream manager.dnsBase default. The domain is no longer hardcoded — it tracks the platform, so a cluster that keeps cluster.local (kubeadm/k3s/RKE2-layered) renders cluster.local.
  • The unit test no longer pins a literal: it sets an arbitrary global.clusterDomain: example.test and asserts DNS_BASE follows it, plus a case asserting the upstream cluster.local fallback when no domain is set. So it pins the property (follows the platform), not a string.

Verified locally, since the fork workflows are stuck in action_required:

  • helm template → cozy.local on the default install, cluster.local with global.clusterDomain=cluster.local, and cluster.local with no global.
  • helm unittest . → 2 suites / 4 tests pass.

Both mechanical items are handled too: the commit is now Signed-off-by (DCO), and the release-note block is in the PR description.

One nice side effect: now that the platform exposes global.clusterDomain, the sibling packages that hardcode the domain — and the mariadb operator in #2348 — can move onto the same mechanism in follow-ups.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
packages/system/opensearch-operator/tests/dns_base_test.yaml (1)

21-24: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Cover the platform-to-operator propagation boundary.

This test sets global.clusterDomain directly, so it cannot catch a regression that stops networking.clusterDomain from being emitted as global.clusterDomain. Add a platform-template or integration assertion for that mapping.

The PR objective spans both propagation and consumption, so both boundaries should be covered.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/system/opensearch-operator/tests/dns_base_test.yaml` around lines 21
- 24, Add a platform-template or integration assertion that verifies
networking.clusterDomain is emitted as global.clusterDomain before the DNS
operator consumes it. Keep the existing dns_base_test.yaml consumption coverage,
and ensure tests cover both the platform-to-operator propagation and operator
behavior boundaries.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@packages/system/opensearch-operator/charts/opensearch-operator/templates/opensearch-operator-controller-manager-deployment.yaml`:
- Line 79: Revert the direct template change in the generated deployment
manifest and restore the vendored chart file to its upstream verbatim content.
Apply or preserve the DNS_BASE customization through
packages/system/opensearch-operator/patches/leaderElection.diff instead, keeping
the generated chart unmodified so make update does not overwrite the fix.

In `@packages/system/opensearch-operator/patches/leaderElection.diff`:
- Around line 34-35: Quote the rendered DNS_BASE value in the Helm template by
applying the quote function to the existing global.clusterDomain fallback
expression, preserving .Values.manager.dnsBase as its default. Ensure the
resulting EnvVar.value is always emitted as a string.

---

Nitpick comments:
In `@packages/system/opensearch-operator/tests/dns_base_test.yaml`:
- Around line 21-24: Add a platform-template or integration assertion that
verifies networking.clusterDomain is emitted as global.clusterDomain before the
DNS operator consumes it. Keep the existing dns_base_test.yaml consumption
coverage, and ensure tests cover both the platform-to-operator propagation and
operator behavior boundaries.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 6cd4836b-2ce1-421b-986e-75b20761afd5

📥 Commits

Reviewing files that changed from the base of the PR and between 467665f and d37e295.

📒 Files selected for processing (4)
  • packages/core/platform/templates/apps.yaml
  • packages/system/opensearch-operator/charts/opensearch-operator/templates/opensearch-operator-controller-manager-deployment.yaml
  • packages/system/opensearch-operator/patches/leaderElection.diff
  • packages/system/opensearch-operator/tests/dns_base_test.yaml

env:
- name: DNS_BASE
value: {{ .Values.manager.dnsBase }}
value: {{ .Values.global.clusterDomain | default .Values.manager.dnsBase }}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Keep the vendored chart template verbatim.

This file is regenerated from upstream; editing it directly means make update can overwrite the DNS_BASE fix. Revert this generated-file change and keep the customization in packages/system/opensearch-operator/patches/leaderElection.diff.

As per coding guidelines, files under packages/**/charts/** must remain verbatim because make update regenerates them. Based on learnings, vendored Helm templates under packages/system/*/charts/**/templates/ are upstream-owned and should be changed through patches or overrides.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/system/opensearch-operator/charts/opensearch-operator/templates/opensearch-operator-controller-manager-deployment.yaml`
at line 79, Revert the direct template change in the generated deployment
manifest and restore the vendored chart file to its upstream verbatim content.
Apply or preserve the DNS_BASE customization through
packages/system/opensearch-operator/patches/leaderElection.diff instead, keeping
the generated chart unmodified so make update does not overwrite the fix.

Sources: Coding guidelines, Learnings

Comment on lines +34 to +35
- value: {{ .Values.manager.dnsBase }}
+ value: {{ .Values.global.clusterDomain | default .Values.manager.dnsBase }}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files | rg 'packages/system/opensearch-operator/patches/leaderElection\.diff|dnsBase|clusterDomain|EnvVar|manager\(' || true

echo
echo "== patch context =="
if [ -f packages/system/opensearch-operator/patches/leaderElection.diff ]; then
  nl -ba packages/system/opensearch-operator/patches/leaderElection.diff | sed -n '1,120p'
fi

echo
echo "== search occurrences =="
rg -n "value:.*DNS_BASE|DNS_BASE|clusterDomain|dnsBase" packages/system/opensearch-operator || true

echo
echo "== helm/quote behavior probe outside repo =="
python3 - <<'PY'
# Standalone YAML-like YAML parser for common string values,
# without importing/requiring any repository dependencies.
import re

def parse_yaml_like_value(s):
    s = s.strip()
    if not s: return ''
    if (s.startswith("'") and s.endswith("'")) or (s.startswith('"') and s.endswith('"')):
        if s.startswith("'"):
            return s[1:-1].replace("''", "'")
        return s[1:-1].replace('\\"', '"').replace('\\\\', '\\')
    if s in ('true', 'false', 'y', 'n', 'yes', 'no', 'on', 'off'):
        return True if s in ('true', 'yes', 'y', 'on') else False
    try:
        return int(s)
    except ValueError:
        try:
            return float(s)
        except ValueError:
            return s

samples = ["true", "123", "example.com", "local.", "none", "null"]
for s in samples:
    print(f"{s!r:12} -> {parse_yaml_like_value(s)!r} ({type(parse_yaml_like_value(s)).__name__})")
PY

Repository: cozystack/cozystack

Length of output: 295


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files | rg 'packages/system/opensearch-operator/patches/leaderElection\.diff|dnsBase|clusterDomain|EnvVar|manager\(' || true

echo
echo "== patch context =="
if [ -f packages/system/opensearch-operator/patches/leaderElection.diff ]; then
  nl -ba packages/system/opensearch-operator/patches/leaderElection.diff | sed -n '1,120p'
fi

echo
echo "== search occurrences =="
rg -n "value:.*DNS_BASE|DNS_BASE|clusterDomain|dnsBase" packages/system/opensearch-operator || true

echo
echo "== helm/quote behavior probe outside repo =="
python3 - <<'PY'
# Standalone YAML-like YAML parser for common strict scalar values,
# without importing/requiring any repository dependencies.
def parse_yaml_like_value(s):
    s = s.strip()
    if not s:
        return ''
    if (s.startswith("'") and s.endswith("'")) or (s.startswith('"') and s.endswith('"')):
        if s.startswith("'"):
            return s[1:-1].replace("''", "'")
        return s[1:-1].replace('\\"', '"').replace('\\\\', '\\')
    if s in ('true', 'false', 'y', 'n', 'yes', 'no', 'on', 'off'):
        return True if s in ('true', 'yes', 'y', 'on') else False
    try:
        return int(s)
    except ValueError:
        try:
            return float(s)
        except ValueError:
            return s

samples = ["true", "123", "example.com", "local.", "none", "null"]
for s in samples:
    print(f"{s!r:12} -> {parse_yaml_like_value(s)!r} ({type(parse_yaml_like_value(s)).__name__})")
PY

Repository: cozystack/cozystack

Length of output: 295


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files | grep -E 'packages/system/opensearch-operator/patches/leaderElection\.diff|dnsBase|clusterDomain|EnvVar|manager\(' || true

echo
echo "== patch header and lines 1-120 =="
if [ -f packages/system/opensearch-operator/patches/leaderElection.diff ]; then
  awk '{printf "%6d  %s\n", NR, $0}' packages/system/opensearch-operator/patches/leaderElection.diff | sed -n '1,140p'
fi

echo
echo "== search occurrences =="
grep -RInE 'value:.*DNS_BASE|DNS_BASE|clusterDomain|dnsBase|type: string|type: bool|type: integer' packages/system/opensearch-operator || true

echo
echo "== deterministic YAML scalar typing probe =="
python3 - <<'PY'
def as_yaml_scalar(s):
    s = s.strip()
    if not s:
        return ''
    if (s.startswith("'") and s.endswith("'")) or (s.startswith('"') and s.endswith('"')):
        return s
    if s in ('true', 'false', 'y', 'n', 'yes', 'no', 'on', 'off'):
        return True if s in ('true', 'yes', 'y', 'on') else False
    try:
        return int(s)
    except ValueError:
        try:
            return float(s)
        except ValueError:
            return s

samples = ["true", "false", "123", "0", "example.com", "local.", "none", "null", "false.example.com"]
for s in samples:
    v = as_yaml_scalar(s)
    print(f"{s!r:22} -> {v!r:12} ({type(v).__name__})")
PY

echo
echo "== deterministic Helm quote template probe =="
python3 - <<'PY'
def quote(s):
    if s == '':
        return '""'
    import re
    s = re.sub(r'(?<!\\)\\', r'\\\\', s)
    s = re.sub(r'(?<!\\)"', r'\"', s)
    return f'"{s}"'

samples = ["true", "123", "example.com", "local.", "false.example.com"]
for s in samples:
    print(f"quote({s!r}) -> {quote(s)}")
PY

Repository: cozystack/cozystack

Length of output: 50376


Quote the rendered DNS_BASE value.

Unquoted Helm output can render YAML-sensitive values like true or 123 as non-strings, but EnvVar.value must be a string in Kubernetes. Add | quote after the fallback expression.

Proposed fix
-          value: {{ .Values.global.clusterDomain | default .Values.manager.dnsBase }}
+          value: {{ .Values.global.clusterDomain | default .Values.manager.dnsBase | quote }}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- value: {{ .Values.manager.dnsBase }}
+ value: {{ .Values.global.clusterDomain | default .Values.manager.dnsBase }}
value: {{ .Values.global.clusterDomain | default .Values.manager.dnsBase | quote }}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/system/opensearch-operator/patches/leaderElection.diff` around lines
34 - 35, Quote the rendered DNS_BASE value in the Helm template by applying the
quote function to the existing global.clusterDomain fallback expression,
preserving .Values.manager.dnsBase as its default. Ensure the resulting
EnvVar.value is always emitted as a string.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NOT LGTM. The direction I asked for last round is in and it works, but the delivery channel takes down a different package, and the platform half of the wiring is untested.

I rebased d37e295 onto current main before reading it, since the branch forked 1232 commits back and I wanted the findings to be about today's tree. The rebase is clean; apps.yaml has moved a lot since July, but not where this lands. Everything below is measured against current main.

The things I blocked on last time are resolved. The test pins the property rather than a string, and it is not decorative: I reverted the template to .Values.manager.dnsBase, hardcoded cozy.local, swapped the default operands, and renamed the env var, and each of those turns the suite red while the unmutated tree stays green. The cozy.local rung is exactly what I refused last round. The chart edit reproduces too. I pulled upstream 2.8.0 fresh and ran the package's own patch --no-backup-if-mismatch -p1 < patches/leaderElection.diff: no fuzz, no offset, and the DNS_BASE line comes out identical to the committed one, so make update will not eat it. Sign-off and the release-note block are both there.

What blocks it now is that global is not a private channel to one subchart. internal/operator/package_reconciler.go:266-273 attaches the cozystack-values Secret as valuesFrom to every Package's HelmRelease unless its PackageSource sets operator.cozystack.io/skip-cozystack-values, and packages/core/platform/sources/bootbox.yaml does not. Then packages/system/bootbox/charts/smee/templates/role.yaml:3-5 guards on bare {{- if .Values.global }} and reads .Values.global.rbac.type inside. An empty map is falsy, so the guard is skipped today; a global carrying only clusterDomain is truthy and .Values.global.rbac is nil. helm template packages/system/bootbox --set global.clusterDomain=cozy.local fails with role.yaml:4:32 ... nil pointer evaluating interface {}.type, and the same render without the flag succeeds. role-binding.yaml:3 and deployment.yaml:6 carry the same guard. I rendered all 169 package charts both ways and this is the only outcome that changes: 133 render bare, and for the 36 that already need values the error text is byte-identical before and after. So any cluster with cozystack.bootbox in bundles.enabledPackages gets a HelmRelease that cannot render.

There is a mechanism for this that does not reach every package. Package.spec.components[<name>].values (api/v1alpha1/package_types.go:68-81, applied at package_reconciler.go:301-302) is already used in templates/bundles/system.yaml:25-36 to push platform values into the vendored kube-ovn and cilium subcharts. And helm template packages/system/opensearch-operator --set opensearch-operator.manager.dnsBase=example.test already renders DNS_BASE: example.test, so going that way needs no chart patch at all: the leaderElection.diff hunk and the vendored chart edit both come back out, and the "keep in sync with _cluster.cluster-domain" comment goes with them, which I would rather not carry anyway. Patching smee's guard instead would also work, but that puts a patch file in a package this PR has no other reason to touch, and leaves the next key added to global to find the next chart with the same shape.

The other blocker is the platform side of the test. The new suite sets global.clusterDomain itself, so it stays green if apps.yaml stops emitting the key and the original failure comes back. packages/core/platform/tests/ holds 36 suites and has a per-key convention for exactly this: apps_monitoring_enabled_wiring_test.yaml, apps_gateway_class_wiring_test.yaml and apps_load_balancer_class_wiring_test.yaml each matchRegex the rendered stringData["values.yaml"] for one key. This change adds a key to that Secret and adds nothing there. Whichever mechanism the fix lands on, it wants a wiring test there with the default and an override.

Ignore the red E2E Tests check. Its status text is "Pull Request workflow concluded 'failure'; e2e not run", and the run it points at has zero jobs, so nothing ever executed; the fork runs sat unapproved since July and got reaped. There is still no CI evidence either way on this head.

Three smaller things.

On a cluster that already has OpenSearchClusters, this does not take effect by itself. At 2.8.0 the manager reads DNS_BASE on each call and defaults to cluster.local, so a restart switches the domain, but the reconciler keeps an existing <cluster>-securityconfig-update Job while its securityconfig/checksum annotation matches, and the target hostname is not part of that checksum. backoffLimit is 0 and the wait loop has no attempt limit, so a Job that was already stuck stays stuck and has to be deleted by hand. That is worth a line in the release note, otherwise someone upgrades, sees nothing change, and reports the fix as broken. Certificates are a milder version of the same thing: they are only generated when the Secret GET fails, so existing SANs keep svc.cluster.local, which nothing in-cluster verifies but which stays stale until those Secrets go.

The value is unquoted, the same as upstream has it. EnvVar.value is a string and the PARALLEL_RECOVERY_ENABLED line right below quotes its own, so | quote would be consistent and costs nothing. It only bites on a domain that YAML reads as a non-string, which is not a valid domain, so this is tidiness rather than a defect.

The body has no Downstream repositories section. I walked the trigger map against the diff and nothing matches as far as I can see: the website row keys on packages/core/platform/values.yaml, which this PR does not touch, and the ansible row keys on changing what Cozystack requires from the distribution, which this does the opposite of. So "No downstream repository is affected", with those two lines as the reason.

One bot suggestion I would not act on: reverting the vendored chart file and keeping only the patch. charts/ is the post-patch artifact this repo commits, so both files moving together is correct, and I checked that the pair reproduces.

This is the DNS side of #4073 and it closes it for a fresh install. On a cluster already carrying the failure it is partial until that Job is deleted.

# each package's own templates, so vendored operator subcharts that build
# in-cluster FQDNs from it (e.g. opensearch-operator's DNS_BASE) cannot see
# it. Keep in sync with _cluster.cluster-domain.
clusterDomain: {{ .Values.networking.clusterDomain | quote }}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This key reaches every package whose PackageSource does not set operator.cozystack.io/skip-cozystack-values, not only this one subchart. packages/system/bootbox/charts/smee/templates/role.yaml:3-5 guards on bare {{- if .Values.global }} and then reads .Values.global.rbac.type, so a global that carries only clusterDomain flips that guard on and the render dies on a nil .Values.global.rbac. Details and the alternative in the review body.

@lexfrei

Copy link
Copy Markdown
Contributor

This is what I asked for in July, and I need it on main now: the opensearch TLS PR (#2682) cannot pass its e2e while the securityconfig Job dials cluster.local on a cozy.local cluster (#4073). I don't know when you will be back on this, so I'm taking your commit as is, with your authorship, into a branch on the main repo and adding one commit of my own on top: a platform test that pins networking.clusterDomain reaching global.clusterDomain in the cozystack-values Secret. Checked on today's main: rebases clean, make update reproduces the patched DNS_BASE line from the patch, package and platform suites green. The coderabbit thread about editing the vendored template is wrong, charts/ carries the patch result by design. I'll link the new PR here and close this one after it merges. If you'd rather rebase and add the test yourself in the next day or two, say so and I'll review that instead.

@lexfrei

Copy link
Copy Markdown
Contributor

New PR: #4185. It carries your commit with your authorship and both sign-offs; one clause of the message is reworded and marked as such in the message, because a per-component values route does exist and the global route is a choice, not the only way. On top of it: a platform test that pins the wiring, and a fix in bootbox, whose vendored smee chart dies on any non-empty global map. I will close this one when it merges.

Aleksei Sviridkin (lexfrei) added a commit that referenced this pull request Sep 10, 2026
…main (#4185)

## What this PR does

The opensearch-operator resolves every in-cluster name it dials as
`<svc>.<ns>.svc.<DNS_BASE>`, and nothing set `DNS_BASE`, so it stayed on
the vendored chart default `cluster.local` while the platform runs on a
configurable domain. On a cluster that is not on `cluster.local` the
securityconfig bootstrap job waits forever on a name that never
resolves, the security configuration is never applied, and OpenSearch
nodes stay unready. That is issue #4073.

The platform now carries the domain to the operator in the
`cozystack-values` Secret, under the subchart key the vendored chart
already reads:

```yaml
opensearch-operator:
  manager:
    dnsBase: "cozy.local"
```

`charts/opensearch-operator` reads that as `.Values.manager.dnsBase` and
hands it to the manager as `DNS_BASE`, so nothing under `charts/`
changes and the package needs no patch. The value renders from
`networking.clusterDomain` rather than a literal, so a cluster that
keeps `cluster.local` keeps working, and a package rendered without the
Secret keeps the upstream `cluster.local` default. This is the route the
KEDA cluster domain already travels through the same Secret.

The whole change is one key in
`packages/core/platform/templates/apps.yaml` plus two test suites: one
pinning that `networking.clusterDomain` reaches the Secret under that
key, one pinning that the key reaches the Deployment env in the package.

### Why this is not the `global` route

Earlier revisions of this branch, and #3357 before it, emitted the
domain as `global.clusterDomain` and patched the vendored template to
read it. `global` is the one key Helm copies into every subchart, which
is why it looked right. That is also the problem. The `cozystack-values`
Secret is `valuesFrom` on every HelmRelease the operator builds unless
the PackageSource opts out with
`operator.cozystack.io/skip-cozystack-values`
(`internal/operator/package_reconciler.go:265-273`), so a new key under
`global` lands in the values of nearly every release on the cluster.

One of them broke on it. The vendored smee chart in
`packages/system/bootbox` gates on the truthiness of the whole `global`
map and then indexes `global.rbac`, so a single unrelated key under
`global` ends its render. This still reproduces on `main`:

```console
$ helm template bootbox packages/system/bootbox
# exit 0
$ helm template bootbox packages/system/bootbox --set global.clusterDomain=cozy.local
Error: cozy-bootbox/charts/smee/templates/role.yaml:4:32
  executing "cozy-bootbox/charts/smee/templates/role.yaml" at <.Values.global.rbac.type>:
    nil pointer evaluating interface {}.type
```

The subchart key reaches the operator with no patch, no change to any
other release, and nothing to harden in bootbox. I measured that rather
than reasoning about it: rendering all 170 charts under `packages/*/*`
plain and with `--set opensearch-operator.manager.dnsBase=cozy.local`,
exactly one chart's output changes, opensearch-operator, by the one
`DNS_BASE` line this PR is about. Nine other charts differ between the
two runs by exactly as much as two identical runs of the same command
differ, which is the random material Helm regenerates per render, and
thirty-five fail in both modes for reasons unrelated to the flag, with
byte-identical errors either way.

The smee hardening is still worth having, because the next key anyone
puts under `global` hits the same gate, and that gate is still on the
default branch of tinkerbell/charts. It is not carried here. It is
available as a separate change against its own package.

### On having more than one way to say the cluster domain

Review feedback on the earlier revision asked for the opposite of this:
fold the `keda:` block into `global.clusterDomain` so the platform has
one answer rather than two added a fortnight apart for the same reason.
The instinct is right and I am not doing it here, because `global` is
the channel that carries the cost. A key under it lands in the values of
nearly every release, one vendored chart already ends its render on it,
and the next chart with the same gate breaks on the day someone adds a
second key. The subchart key has no reach: the chart that declares it
reads it and everything else ignores it, which is how the KEDA domain
already travels.

That does leave the repo with several ways to tell a chart its domain,
and that complaint stands. What it avoids is a shared channel whose
blast radius grows with every new user of it. Whether the platform
should grow one general mechanism here, and which one, is worth settling
on its own rather than inside a bug fix.

### Why the stuck Job is a manual step and not a migration

The release note asks operators to delete a wedged
`<release>-securityconfig-update` Job once. On an existing OpenSearch
installation that Job is already stuck, so this is a step for everyone
the fix is for, and `packages/core/platform/images/migrations/` is the
mechanism built for exactly that. It is not in this PR because the
migration deletes Jobs in tenant namespaces, which is a wider grant and
a wider blast radius than the one key this change adds, and the fix
works without it. If it should ride the same release, it is a separate
commit and I will add it.

### Upgrade behaviour on a running cluster

Read from the operator source at v2.8.0. `DNS_BASE` is read on every
call through `helpers.ClusterDnsBase`
(`pkg/helpers/constants.go:35-41`), so the manager restart is the whole
switch. On a cluster that already runs OpenSearch clusters:

- Certificates are not re-issued. Transport and http certs are generated
only when their Secret is missing (`pkg/reconcilers/tls.go:272-295`,
`490-518`), per-node transport certs only for a pod with no entry in the
Secret (`tls.go:373-377`), the admin cert only when missing, expiring
within five days or not signed by the CA (`tls.go:176-211`). Existing
SANs keep the old suffix. Nothing in-cluster verifies them: the
securityadmin job runs `curl -k` and `securityadmin.sh -nhnv`
(`pkg/reconcilers/securityconfig.go:34,40,46`), the operator client sets
`InsecureSkipVerify` (`opensearch-gateway/services/os_client.go:77`),
Dashboards run with `opensearch.ssl.verificationMode: none`
(`pkg/builders/dashboards.go:180`).
- OpenSearch node pods do not roll, because no StatefulSet field derives
from the DNS base. Dashboards roll, since `OPENSEARCH_HOSTS` changes
(`pkg/builders/dashboards.go:46-47`).
- A securityconfig Job already stuck in its wait loop is not recreated.
The reconciler keeps an existing `<release>-securityconfig-update` Job
while its checksum annotation matches the securityconfig Secret
(`securityconfig.go:140-146`), and the target hostname is not part of
that checksum. Delete that Job once by hand. The next reconcile
recreates it against the new name, and users and roles start applying
from there.

### Known and not addressed here

The upstream `manager.dnsBase` knob stays usable. The platform writes it
through `spec.valuesFrom`, and a per-package override in
`Package.spec.components[].values` lands in `spec.values`, which Flux
merges on top of the references, so a package that needs a different
domain can still say so and win.

The fallback differs from its neighbours and that is deliberate.
`packages/apps/opensearch` falls back to `cozy.local`,
`packages/system/keda` declares `cozy.local` as its package default, and
this package stays on the upstream `cluster.local`. Inside the bundle
all three receive the same value, so nothing diverges in practice, and
`cluster.local` is the right standalone default for a Cozystack
installed over an existing kubeadm or k3s cluster.

A cluster domain that YAML reads as a number does not survive the round
trip. The platform quotes the value into the Secret, so
`networking.clusterDomain: 123` arrives as the string `123`, but the
upstream template renders it as a bare `value: 123` in the container
env, and an `EnvVar` value has to be a string. Quoting it means patching
the vendored template, which is the thing this approach removes, and an
all-numeric cluster domain is not a usable DNS name to begin with.
Before this change the key was never set from the platform, so the path
is new here even though nothing reaches it.

The vendored `opensearch-operator` Deployment template does not
reproduce from `make update` and has not for a while: a fresh pull plus
the patch differs in three places, and one of them is a live defect. The
committed `nindent 10` on `manager.extraEnv` renders invalid YAML, so
that knob cannot be used at all, while upstream's `nindent 8` works.
Nothing in the tree sets `extraEnv`, so it is latent, and the drift
predates this branch. This PR does not touch that patch at all and does
not carry a fix for it.

Nothing in CI renders a package chart with the values the platform
injects, which is why the smee breakage on the earlier approach had to
be found by hand. That is a gap in the harness.

### Screenshots

Not a UI change.

### Downstream repositories

Walked the trigger map against the diff, which is one template key in
`packages/core/platform/templates/apps.yaml` and two test files.
`packages/core/platform/values.yaml` is untouched and gains no key, so
the hand-written platform-package table on the website is not affected.
No package, app schema, version enum, CRD, release prefix or naming
convention moves, so the Terraform provider is not reached. The
`networking.clusterDomain` key already exists and its default does not
change, so ansible-cozystack, which passes `--cluster-domain=cozy.local`
to k3s and sets only the networking CIDRs in its platform-package
template, needs nothing: the operator now follows the domain that repo
already configures. talm sets the Talos `cluster.network.dnsDomain` from
its own value and does not write the platform value either. Nothing
under `hack/`, no namespace and no package-tree layout moves, so ccp is
not reached.

- [x] No downstream repository is affected by this change
- [ ] [cozystack/website](https://github.com/cozystack/website) -
follow-up:
- [ ]
[cozystack/terraform-provider-cozystack](https://github.com/cozystack/terraform-provider-cozystack)
- follow-up:
- [ ]
[cozystack/ansible-cozystack](https://github.com/cozystack/ansible-cozystack)
- follow-up:
- [ ] [cozystack/ccp](https://github.com/cozystack/ccp) - follow-up:
- [ ] [cozystack/talm](https://github.com/cozystack/talm) - follow-up:
- [ ] [cozystack/cozyhr](https://github.com/cozystack/cozyhr) -
follow-up:
- [ ] [cozystack/cozy-proxy](https://github.com/cozystack/cozy-proxy) -
follow-up:
- [ ]
[cozystack/cozystack-telemetry-server](https://github.com/cozystack/cozystack-telemetry-server)
- follow-up:
- [ ]
[cozystack/external-apps-example](https://github.com/cozystack/external-apps-example)
- follow-up:
- [ ] [cozystack/examples](https://github.com/cozystack/examples) -
follow-up:
- [ ] [cozystack/community](https://github.com/cozystack/community) -
follow-up:

### Release note

```release-note
fix(opensearch-operator): the operator's DNS_BASE now follows the platform cluster domain (`networking.clusterDomain`) instead of the upstream `cluster.local` default, so the securityconfig job and OpenSearch Dashboards reach the cluster on installations that use `cozy.local`. On upgrade, an existing `<release>-securityconfig-update` Job that is already stuck waiting on the old hostname is not recreated by the operator: delete it once by hand and the next reconcile recreates it against the new hostname. Existing certificates keep their SANs and keep working, nothing in-cluster verifies them.
```
@lexfrei

Copy link
Copy Markdown
Contributor

#4185 is merged (e89617d), so I'm closing this one.

The merged shape is not your commit though. The route moved from global.clusterDomain to a per-subchart key, opensearch-operator.manager.dnsBase in the same shared Secret, because the vendored smee chart in bootbox checks the whole global map for truthiness and then indexes global.rbac under it, so any non-empty global key kills its render on every cluster with bootbox enabled. Your diagnosis and your first fix are what moved this, and the merged commit body says so. Thanks for the patch and for the patience with the rework.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/database Issues or PRs related to managed databases (postgres, mariadb, redis, etcd, kafka, clickhouse) area/uncategorized PR auto-labeler could not map title scope to a known area/*; please review kind/bug Categorizes issue or PR as related to a bug size/M This PR changes 30-99 lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants