feat: Add global OTEL extension, backend service, and release automation to Argo template - #53
feat: Add global OTEL extension, backend service, and release automation to Argo template#53yesterdaysrebel wants to merge 30 commits into
Conversation
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.
a3a1322 to
4ba2510
Compare
…n ArgoCD templates
Implemented in the latest commits. Added:
|
| variable "otel_backend_tag" { | ||
| type = string | ||
| description = "Image tag (SHA or semver) for ghcr.io/glueops/argocd-extension-backend-api" | ||
| default = "v0.1.1" | ||
| } |
| otel_extension_config = var.otel_enabled ? join("\n", [ | ||
| " extension.config: |", | ||
| " extensions:", | ||
| " - name: otel-extension", | ||
| " backend:", | ||
| " services:", | ||
| " - url: http://argocd-extension-backend-api.glueops-core.svc.cluster.local:8000", | ||
| ]) : "" |
| " name: argocd-extension-backend-api", | ||
| " namespace: glueops-core", | ||
| " labels:", | ||
| " app.kubernetes.io/name: argocd-extension-backend-api", |
| " selector:", | ||
| " matchLabels:", | ||
| " app.kubernetes.io/name: argocd-extension-backend-api", |
| " metadata:", | ||
| " labels:", | ||
| " app.kubernetes.io/name: argocd-extension-backend-api", |
| " containers:", | ||
| " - name: argocd-extension-backend-api", | ||
| " image: \"ghcr.io/glueops/argocd-extension-backend-api:placeholder_otel_backend_tag\"", | ||
| " imagePullPolicy: IfNotPresent", |
| " name: argocd-extension-backend-api", | ||
| " namespace: glueops-core", | ||
| " labels:", | ||
| " app.kubernetes.io/name: argocd-extension-backend-api", |
| " selector:", | ||
| " app.kubernetes.io/name: argocd-extension-backend-api", |
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>
| # everything under policy.csv is Casbin policy text, not YAML. | ||
| policy.csv: | | ||
| placeholder_argocd_rbac_policies | ||
| p, role:readonly, extensions, invoke, *, allow |
There was a problem hiding this comment.
can thesed roles be wildcarded? role:* or does that break it?
| - name: otel-extension | ||
| env: | ||
| - name: EXTENSION_URL | ||
| value: "https://github.com/GlueOps/argo-cd-ui-extention/releases/download/placeholder_otel_extension_version/extension.tar.gz" |
There was a problem hiding this comment.
If this fails to download will it break deployment? Can you throw in an invalid url maybe a nonexistent domain and a bad file/name/version?
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>
| @@ -0,0 +1 @@ | |||
| backend/node_modules/ | |||
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