feat: Add global OTEL extension, backend service, and release automation to Argo template - #53
Merged
Merged
Conversation
Contributor
There was a problem hiding this comment.
Pull request overview
This pull request updates the Argo CD Helm values template and accompanying documentation to enable a global Argo CD UI extension (OTEL) and deploy an in-cluster backend service that the extension calls.
Changes:
- Enables Argo CD’s proxying for extension backends and adds extension configuration in
configs.cm. - Adds Argo CD RBAC rules to allow invoking the
otel-extension. - Deploys an
otel-extension-apiDeployment and Service viaextraObjects, and documents the new placeholders inREADME.md.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| README.md | Documents new placeholders and explains that the OTEL extension is configured globally. |
| argocd.yaml.tpl | Enables the extension feature, configures the extension + RBAC, and adds Kubernetes resources for the extension backend API. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Copilot
AI
force-pushed
the
feature/global-otel-backend
branch
from
June 15, 2026 05:10
a3a1322 to
4ba2510
Compare
…n ArgoCD templates
Makes the Argo CD OTEL UI extension unconditional and removes the duplicate backend this module was deploying. Always on: drops var.otel_enabled. Safe because the frontend renders nothing when it has no links -- a cluster whose backend is not up shows no panel at all rather than an error box. That behaviour only exists from v0.1.3 onward, so the version default must never go below it. Backend ownership: platform-helm-chart-platform owns the backend and deploys it into glueops-core-argocd-extension-backend. This module was deploying a second Deployment/Service of the same image into glueops-core, so enabling otel would have run two copies. Removed, along with the now-unused otel_backend_tag and tempo_base_url. Backend URL: was glueops-core, which does not resolve (verified NXDOMAIN in-cluster). Both the Service name and its namespace are hardcoded constants in the platform chart, so this URL is identical on every cluster -- there is deliberately nothing per-cluster to substitute. Installer image: the chart defaults it to quay.io directly, unlike every other image on the platform. It runs as an initContainer on argocd-server, so a failed pull takes the Argo CD UI down rather than just disabling the extension. Pinned to the gpkg mirror. Verified by rendering the module output and parsing it: valid YAML, no unreplaced placeholders, tenant RBAC policies preserved (otel policies are appended, not substituted), and no backend objects in extraObjects. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The previous comment said 'do NOT pin below v0.1.3' while the default was v0.1.3-rc1, which sorts below v0.1.3 -- the rule contradicted the value it was guarding. State the actual constraint (a release containing PR #25's hide-when-empty behaviour) and why a prerelease is pinned for now. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
rc1 blanked the panel behind "Loading links..." on every Argo CD reconcile: Argo CD remounts status-panel extensions when the Application object updates, and with timeout.reconciliation: 10s that is roughly every 10 seconds. Each remount reset the panel and refetched /api/links, once per reconcile per open application. rc2 caches the links per application and seeds state from that cache, so a remount renders immediately with no flash and no refetch. It keeps rc1's render-nothing-when-empty behaviour, so it is still safe to ship to every cluster. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Prepares this branch for merge. As it stood, merging would have started
publishing a second, unused backend image on every release, forever.
.github/workflows/release.yml triggered on `release: [published]` and pushed
ghcr.io/glueops/argocd-otel-extension-api, built from backend/ in this repo.
That image is a duplicate: the real backend is argocd-extension-backend-api,
built from GlueOps/argo-cd-extention-backend and deployed by the platform chart
as the glueops-argocd-extension-backend Application. main.tf here says as much
-- "This module must never deploy a second copy of it." The duplicate is
referenced nowhere in the delivery path, and clusters run
argocd-extension-backend-api. Once release-please starts cutting releases from
main, that workflow would fire on each one.
Removes the workflow and backend/. Leaves .github/release.yml alone -- a
different file, org-managed via internal-GlueOps/github-shared-files-sync and
present on main too.
README documented an interface that no longer exists: otel_enabled ("The
default is false, so tenants must opt in explicitly" -- the opposite of current
behaviour), otel_backend_tag, tempo_base_url, and a placeholder_otel_enabled
that is not in the template. The example also omitted argocd_rbac_policies and
carried a credential-shaped client_secret. Rewritten against the eight
variables main.tf actually declares.
No rendering logic changed: main.tf and argocd.yaml.tpl are untouched and the
module output is byte-identical (13256 bytes, sha 57f5c2ab).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ackend # Conflicts: # argocd.yaml.tpl
…g them in HCL The extension is unconditionally installed, so the three injected blocks carried no interpolation at all -- they were static YAML assembled as HCL string lists and pushed in with replace(). Writing them directly in the template removes ~56 lines of main.tf and, more importantly, removes the failure mode: the anchors were *comment lines* at 4, 2 and 6 spaces, and because rendering is plain string replacement the indentation of a comment decided whether the injected YAML landed in the right place. argocd.yaml.tpl now parses as valid YAML as downloaded (verified with yq), so the documented "wget it and use it with helm install" path works without substituting anything structural. Only placeholder_otel_extension_version and placeholder_otel_extension_semver remain, and both are plain scalars. Verified byte-for-byte: rendering the module before and after produces identical YAML content (the only added lines are the explanatory comments, which moved from HCL into the template and now ship in the rendered file), and yq reports the two documents semantically identical. Zero unsubstituted placeholders in the output. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The locals chain this PR introduced was justified when OTEL needed five substitutions on top of the seven already there -- twelve levels of nested replace() is unreadable. Moving the static blocks into argocd.yaml.tpl cut that to two, so the nest is nine instead of seven and the rewrite no longer pays for itself. It also meant this PR touched how every unrelated placeholder renders, which is review surface the change does not need. main.tf now keeps the nested-replace shape already on main and appends two calls for the extension version and semver. The two version checks move from output preconditions to variable validation blocks. That is where they belong -- they validate an input, not an output -- and it leaves the output block the same shape as main's. Verified they still fire: passing " " fails with "otel_extension_version must be non-empty". Rendered output is unchanged from before this PR's cleanup: YAML content byte-identical with comments stripped, and yq reports the documents semantically identical. Zero unsubstituted placeholders. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
tofu fmt had normalised the provider block's alignment, and the comment above otel_extension_version still described v0.1.3-rc1 as unreleased and told the reader to bump the default once PR #25 merged -- both done. Neither line is what this PR is about. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Argo CD denies extension invocation unless a policy allows it, and the policy named otel-extension specifically. Adding a second extension would have meant an RBAC edit reaching every cluster -- exactly the per-cluster churn we are removing elsewhere. The installed set is controlled by server.extensionList in this same file, so the wildcard does not widen what can be invoked, only what can be invoked without another edit. The explanation sits above policy.csv as a YAML comment rather than inside the block scalar: everything under policy.csv is Casbin policy text, and while Casbin does skip "#" lines, RBAC is not worth resting on that. Verified with yq that the rendered policy value contains only policy lines. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
v0.1.3 is the first non-prerelease of the panel and the first build that renders the links it fetches. Every earlier build gated each category on status === 'ok', and deployment-config is effectively the only category the backend ever marks ok, so the panel showed a single Config Repo button and hid working Loki, Tempo, Prometheus and dashboard links. Verified end to end: the release tarball downloads, contains resources/otel-extension/extensions.js (7725 bytes), and the shipped bundle carries the fix and no longer carries the removed gate. Rendering this module produces the matching EXTENSION_URL, with zero unsubstituted placeholders. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Security and correctness hardening, no visible change to the panel: safeHref() gates every rendered href (javascript:/data: XSS, and //host, /\host and tab/newline open-redirect smuggling), requestTimeoutMs rejects NaN and fractional values that make setTimeout fire immediately, categories filter to renderable links before deciding whether to render, React keys cannot collide, and the theme MutationObserver no longer watches document.body's subtree. Verified: the release URL returns HTTP 200, the shipped bundle carries the hardening markers (8058 bytes, up from 7725 in v0.1.3) and still carries the links cache from #69, and rendering this module produces the matching EXTENSION_URL with zero unsubstituted placeholders. Unlike v0.1.3, this tag is an ancestor of main. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The status panel no longer appears on clusters without the extension backend. It previously always rendered its bordered panel with the GlueOps logo, and usually without even an explanatory line, because a transport failure resolved as an empty category list and took the success path. v0.1.5 returns null unless there are links to show, and stops calling /api/links once a failure has marked the backend absent. This is what makes the always-on rollout safe: the extension can ship fleet-wide ahead of, or without, its backend and simply not appear. Behaviour change: an application whose categories are all empty now shows no panel at all, where earlier releases showed a logo box. Verified: the release URL returns HTTP 200 and the shipped bundle no longer contains "Loading links", "Observability unavailable" or "Application context not available" -- all three present in v0.1.4 -- while retaining the links cache and the safeHref/timeout hardening. Rendering this module produces the matching EXTENSION_URL with zero unsubstituted placeholders. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
venkatamutyala
requested changes
Sep 10, 2026
Answers both review questions on #53 with evidence rather than assertion. The role cannot be wildcarded. Argo CD's Casbin matcher resolves the subject with g(r.sub, p.sub) -- a group lookup -- while resource/action/object go through globMatch, so `*` in the subject position is a literal name matching nobody. Verified with `argocd admin settings rbac can` against v3.2.12: with `p, role:*, ...` both role:readonly and role:admin answer No; with a bare `p, *, ...` a real user also answers No; with the current two lines both answer Yes, as does a user mapped in via `g,`. The lines stay as they are. A bad EXTENSION_URL does NOT break the deployment, which is the opposite of what the comment here claimed. Tested against argocd-extension-installer:v0.0.9 in a throwaway pod: a nonexistent domain (curl exit 6) and a 404 from a bad tag (curl exit 22) both leave the initContainer exiting 0, the pod Succeeded, and the main container starting with an empty /tmp/extensions. The installer's EXIT trap runs `rm -rf` on its temp dir and then reads $?, which reports the rm rather than the curl, masking the real code. The old comment conflated two failure modes. An IMAGE PULL failure genuinely is fatal -- the initContainer never starts, so argocd-server never starts -- which is what the gpkg mirror pin protects against. A DOWNLOAD failure is not, and nothing alerts on it: the extension is just silently absent. Rendered output is unchanged apart from these comments; policy.csv still contains only policy lines and there are zero unsubstituted placeholders. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
venkatamutyala
requested changes
Sep 10, 2026
Reverts the object wildcard. Adding an extension should be a conscious RBAC decision rather than something a wildcard grants silently, so a second extension now needs a line here. Only Argo CD built-in roles are referenced, deliberately. This template is identical on every cluster while the roles above it come from each tenant's own argocd_rbac_policies -- venus defines a single group->admin mapping, another tenant may define several -- so naming a custom role such as role:developers would leave a dangling reference wherever that tenant does not define it. The consequence is that a user holding neither built-in role cannot invoke the extension, and since v0.1.3 the panel renders nothing rather than an error, so nothing on screen points at RBAC. The agreed fix is `policy.default: role:readonly`, rolled out with the OTel stack. Verified with `argocd admin settings rbac can --default-role role:readonly` on v3.2.12: an arbitrary user goes No -> Yes for the extension and for `get applications`, and stays No for `delete applications`. Worth being explicit that this widens read access to Argo CD generally, not only to this panel -- on venus today a non-super_admin has no access at all. Also recorded that the role itself cannot be wildcarded: Argo CD resolves the subject with g(r.sub, p.sub) while resource/action/object use globMatch, so `p, role:*` and a bare `p, *` both answer No for every subject tested. Rendered output verified: policy.csv contains only policy lines, zero unsubstituted placeholders. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
It was introduced by this branch solely to ignore backend/node_modules/, and backend/ was removed by the same PR when the duplicate backend publisher was dropped -- the real backend lives in GlueOps/argo-cd-extention-backend and is deployed by the platform chart. That leaves a file whose only line ignores a path that exists on neither this branch nor main. The repo is Terraform plus a template and has nothing to ignore, and main never carried a .gitignore, so removing it restores that. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
argocd-extension-installer:v0.0.9 assigns ext_version once at line 100 of install.sh and never references it again -- across all 124 lines that is the only mention. The tarball URL determines everything. Confirmed empirically rather than by reading: installed the real v0.1.5 tarball in a throwaway pod with the variable omitted and got the same file at the same path, md5 56b8f0b9f5e53cad2088624abcd4e763, 8038 bytes -- byte-identical to the released bundle and to what is running on venus -- with exit 0 and no warning. The log shows `ext_version=` empty and no different code path taken. Removing it also removes the second placeholder. main.tf now has one OTEL substitution instead of two, and the otel_extension_semver local and its trimprefix are gone. The two placeholders only ever differed by the leading "v": the URL needs the literal GitHub tag (v0.1.5), EXTENSION_VERSION took the bare semver (0.1.5). Rendered output verified: the diff against the previous render is exactly the two deleted lines, nothing else, and zero unsubstituted placeholders remain. The finding is recorded next to the env var, including the caveat that a newer installer could start using it -- the image is pinned in the same block, so whoever bumps it should re-check. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The manual-install note still told readers that both placeholder_otel_extension_version and placeholder_otel_extension_semver are substituted. The semver placeholder was removed with EXTENSION_VERSION, so the template now carries eight placeholders and only one of them is OTEL's. The version floor said "v0.1.3 or newer", which understates it now that the extension is installed on every cluster unconditionally. v0.1.3 and v0.1.4 draw an empty bordered panel on a cluster with no backend; only v0.1.5 renders nothing. Since always-on is the design, v0.1.5 is the real floor, and the note now says so and explains what each earlier build does instead. Verified: all eight placeholders in argocd.yaml.tpl are substituted exactly once by main.tf, none orphaned, and the rendered output is unchanged by this commit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The comment-to-code ratio on the new OTEL blocks was too high. Cut the
long explanatory blocks to a line or two each, or dropped them where the
key name already says it:
- server.enable.proxy.extension and EXTENSION_URL: dropped
- extension.config, RBAC policy, installer image pin: reduced
- otel_extension_version: dropped the version history, since v0.1.5 is
the first release anyone will use
Only comment lines and the otel_extension_version description string
changed; no behaviour change.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
venkatamutyala
approved these changes
Sep 11, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR enables the ArgoCD OTEL UI extension globally, deploys a purpose-built in-cluster backend service (
otel-extension-api) that the extension calls, and adds release automation to publish the backend container image to GHCR.Changes Made
argocd.yaml.tplserver.enable.proxy.extension)extraObjectsmain.tfotel_enabled,otel_extension_version,otel_backend_tag, andtempo_base_urlinput variables (OTEL opt-in defaults tofalse)otel_enabled = trueoutput preconditionguards for OTEL inputs with whitespace trimmingrequired_version = ">= 1.2.0"Terraform constraintbackend/src/server.js— Node.js/Express server proxying/prometheus/*→PROMETHEUS_BASE_URLand/tempo/*→TEMPO_BASE_URL, with a/healthzhealth checkpackage.json— Express + http-proxy-middleware dependenciesDockerfile— Node 20 Alpine container imageREADME.md— Documents all endpoints and environment variables.github/workflows/release.ymlghcr.io/glueops/argocd-otel-extension-apito GHCR on every published release usingdocker/build-push-actionREADME.md