Skip to content

feat: Add global OTEL extension, backend service, and release automation to Argo template - #53

Open
yesterdaysrebel wants to merge 30 commits into
mainfrom
feature/global-otel-backend
Open

feat: Add global OTEL extension, backend service, and release automation to Argo template#53
yesterdaysrebel wants to merge 30 commits into
mainfrom
feature/global-otel-backend

Conversation

@yesterdaysrebel

@yesterdaysrebel yesterdaysrebel commented Jun 15, 2026

Copy link
Copy Markdown

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.tpl

  • Enables ArgoCD extension proxy (server.enable.proxy.extension)
  • Adds YAML-valid commented placeholders for OTEL extension config, RBAC policies, server extension list, and backend extraObjects

main.tf

  • Adds otel_enabled, otel_extension_version, otel_backend_tag, and tempo_base_url input variables (OTEL opt-in defaults to false)
  • Renders OTEL extension config, RBAC, server extensions, and backend Deployment/Service into the template when otel_enabled = true
  • Adds output precondition guards for OTEL inputs with whitespace trimming
  • Adds required_version = ">= 1.2.0" Terraform constraint

backend/

  • src/server.js — Node.js/Express server proxying /prometheus/*PROMETHEUS_BASE_URL and /tempo/*TEMPO_BASE_URL, with a /healthz health check
  • package.json — Express + http-proxy-middleware dependencies
  • Dockerfile — Node 20 Alpine container image
  • README.md — Documents all endpoints and environment variables

.github/workflows/release.yml

  • Builds and pushes ghcr.io/glueops/argocd-otel-extension-api to GHCR on every published release using docker/build-push-action

README.md

  • Documents OTEL Terraform inputs and manual install guidance for non-Terraform users
  • Updates Terraform usage example with all required inputs and correct output reference

Copilot AI review requested due to automatic review settings June 15, 2026 05:03

Copilot AI 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.

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-api Deployment and Service via extraObjects, and documents the new placeholders in README.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 a3a1322 to 4ba2510 Compare June 15, 2026 05:10
@yesterdaysrebel yesterdaysrebel changed the title Add global OTEL extension and backend service to Argo template feat: Add global OTEL extension and backend service to Argo template Jun 15, 2026
Copilot AI review requested due to automatic review settings June 15, 2026 05:38

Copilot AI 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.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

Comment thread README.md Outdated
Comment thread argocd.yaml.tpl Outdated
Comment thread argocd.yaml.tpl Outdated

Copilot AI 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.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

Comment thread argocd.yaml.tpl
Comment thread main.tf Outdated
Comment thread README.md Outdated

Copilot AI 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.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.

Comment thread argocd.yaml.tpl Outdated
Comment thread argocd.yaml.tpl Outdated
Comment thread argocd.yaml.tpl Outdated
Comment thread argocd.yaml.tpl Outdated
Comment thread README.md Outdated

Copilot AI 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.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.

Comment thread main.tf Outdated
Comment thread main.tf Outdated
Comment thread main.tf Outdated
Comment thread main.tf Outdated
Copilot AI review requested due to automatic review settings June 19, 2026 05:44
@yesterdaysrebel
yesterdaysrebel removed the request for review from Copilot June 19, 2026 05:44

Copilot AI commented Jun 19, 2026

Copy link
Copy Markdown

@copilot ## Pull request overview
This PR switches the Argo CD otel-extension proxy backend from Grafana to a purpose-built in-cluster backend service (otel-extension-api), and adds build/releas...

Implemented in the latest commits. Added:

  • backend/src/server.js — Express server proxying /prometheus/*PROMETHEUS_BASE_URL and /tempo/*TEMPO_BASE_URL, with a /healthz health check. Proxy errors are logged internally but return a generic 502 to the client.
  • backend/package.json — Express + http-proxy-middleware (v3.0.7, patched for the CRLF injection CVE) with "engines": {"node": ">=20"}.
  • backend/Dockerfile — Node 20 Alpine image using npm ci --omit=dev.
  • backend/README.md — Documents endpoints and all environment variables.
  • .github/workflows/release.yml — Builds and pushes ghcr.io/glueops/argocd-otel-extension-api to GHCR on every published release.
  • .gitignore — Excludes backend/node_modules/.

Copilot AI changed the title feat: Add global OTEL extension and backend service to Argo template feat: Add global OTEL extension, backend service, and release automation to Argo template Jun 19, 2026
@GlueOps GlueOps deleted a comment from Copilot AI Jun 19, 2026
Copilot AI review requested due to automatic review settings June 23, 2026 05:29

Copilot AI 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.

Pull request overview

Copilot reviewed 9 out of 11 changed files in this pull request and generated 8 comments.

Files not reviewed (1)
  • backend/package-lock.json: Generated file

Comment thread main.tf Outdated
Comment on lines +73 to +77
variable "otel_backend_tag" {
type = string
description = "Image tag (SHA or semver) for ghcr.io/glueops/argocd-extension-backend-api"
default = "v0.1.1"
}
Comment thread main.tf Outdated
Comment on lines +91 to +98
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",
]) : ""
Comment thread main.tf Outdated
Comment on lines +119 to +122
" name: argocd-extension-backend-api",
" namespace: glueops-core",
" labels:",
" app.kubernetes.io/name: argocd-extension-backend-api",
Comment thread main.tf Outdated
Comment on lines +125 to +127
" selector:",
" matchLabels:",
" app.kubernetes.io/name: argocd-extension-backend-api",
Comment thread main.tf Outdated
Comment on lines +129 to +131
" metadata:",
" labels:",
" app.kubernetes.io/name: argocd-extension-backend-api",
Comment thread main.tf Outdated
Comment on lines +140 to +143
" containers:",
" - name: argocd-extension-backend-api",
" image: \"ghcr.io/glueops/argocd-extension-backend-api:placeholder_otel_backend_tag\"",
" imagePullPolicy: IfNotPresent",
Comment thread main.tf Outdated
Comment on lines +180 to +183
" name: argocd-extension-backend-api",
" namespace: glueops-core",
" labels:",
" app.kubernetes.io/name: argocd-extension-backend-api",
Comment thread main.tf Outdated
Comment on lines +186 to +187
" selector:",
" app.kubernetes.io/name: argocd-extension-backend-api",
yesterdaysrebel and others added 14 commits June 23, 2026 11:21
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>
…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>
Comment thread argocd.yaml.tpl
# everything under policy.csv is Casbin policy text, not YAML.
policy.csv: |
placeholder_argocd_rbac_policies
p, role:readonly, extensions, invoke, *, allow

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.

can thesed roles be wildcarded? role:* or does that break it?

Comment thread argocd.yaml.tpl
- 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"

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.

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>
Comment thread .gitignore
@@ -0,0 +1 @@
backend/node_modules/

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.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants