SPAM Cluster Agent Metadata
- Go 98.8%
- Dockerfile 1.2%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
* feat: annotation + env fallback chain for cluster identity
ROR's clusterinterregator requires all seven Vitistack annotations on a
node before it reports a cluster ID; clusters that carry only
vitistack.io/clusterid fall through to the unknown provider and SCAM
reported the kube-system namespace UID instead of the real ROR UUID.
Extract resolveClusterID/Name/Environment with explicit priority chains
(ROR → vitistack.io/* → cluster.x-k8s.io/cluster-name → ror.io/name →
helm-injected env vars → kube-system UID). Sentinel checks now cover
unknown-cluster and unknown-environment in addition to unknown-undefined
and unknown-cluster-id.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat: stamp build version + OCI labels via docker/metadata-action
Previously the binary had no self-knowledge of which build it was, and the
Docker image carried bespoke `build.*` labels that downstream tooling
doesn't read. Image digests gave runtime traceability, but logs and the
startup banner offered no way to correlate cluster-emitted events back to
a build.
Changes:
- CI uses docker/metadata-action to produce OCI-standard labels
(org.opencontainers.image.*) and canonical tags (branch, short SHA,
latest on default branch, PR refs). Bespoke build.* labels are dropped.
- Build args VERSION (from metadata-action) and COMMIT (github.sha) are
passed into the Dockerfile and stamped into the Go binary via
`-ldflags -X main.version -X main.commit`.
- scam exposes `--version`, prints version+commit in the startup banner,
and adds them as default log attrs so every pushed event carries them.
- helm-push consumes the build job's `version` output, so chart
`image.tag` becomes `<version>@<digest>` instead of `<date>@<digest>`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat: switch cluster identity to ROR Self() + env/UID fallback
Drops the clusterinterregator + node-annotation walks. New chain
per field:
- Name: ROR V2 Self() (when ROR_API_ENDPOINT + ROR_API_KEY
are set) → CLUSTER_NAME env
- ID: CLUSTER_ID env → kube-system namespace UID
- Environment: ENVIRONMENT env
Self() is best-effort — any failure falls through silently, so
clusters without a ROR apikey behave exactly as before.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(chart): ror.enabled gate with apikey from Secret
When ror.enabled is true the deployment gets:
- ROR_API_ENDPOINT (default https://api.ror.nhn.no, overridable
via ror.endpoint)
- ROR_API_KEY either from a literal ror.apiKey override or from
secretKeyRef to ror.apiKeySecret (defaults: rorapikey / APIKEY,
matching ror-agent's provisioning convention).
Off by default — clusters without a ROR apikey keep working on
the env / kube-system UID chain.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ci: publish helm-dev chart from non-main push events
New helm-push-dev job mirrors helm-push but pushes to a separate
OCI artifact (scam/helm-dev) with prerelease version
0.0.0-<branch-slug>.<shortsha>, pinned to the SHA-tagged image
built in the same run. PRs are skipped (no write secrets).
Lets dev branches be helm-installed for testing without polluting
the prod helm artifact's version sequence.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: correct rorversion import path
* fix: pass context to Self().Get()
* feat: read ROR apikey via k8s API instead of secretKeyRef
Lets SCAM pull the apikey from a Secret in a different namespace
(e.g. nhn-ror/ror-apikey) via the in-cluster client — RBAC-gated,
which is the only way to read cross-namespace secrets in K8s
(kubelet's secretKeyRef resolution is locked to the pod's own
namespace, regardless of RBAC).
Apikey resolution order:
1. ROR_API_KEY env var (literal value)
2. K8s Secret pointed at by ROR_API_KEY_SECRET_NAMESPACE +
ROR_API_KEY_SECRET_NAME + ROR_API_KEY_SECRET_KEY
Best-effort: any failure falls through to the env/UID identity
chain.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(chart): cross-namespace RoleBinding for ROR apikey Secret
Switch from secretKeyRef (which can't go cross-namespace) to env-var
pointers (ROR_API_KEY_SECRET_NAMESPACE/_NAME/_KEY) read by the app
via the in-cluster client. When ror.apiKeySecret.namespace differs
from the release namespace, the chart now provisions a Role +
RoleBinding in the apikey namespace, scoped via resourceNames to
just that one Secret.
Default Secret name flipped from 'rorapikey' to 'ror-apikey' to
match the actual provisioning convention (kebab-case).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: use ROR Self() slug as cluster_id when available
In NHN's ROR the cluster identifier IS the slug (e.g.
t-tek-003-n2ua) — there's no separate UUID. Previously we put
that slug only in 'cluster' and fell back to the kube-system
namespace UID for cluster_id, which is a local-only fingerprint
no other system knows about.
New priority for ID: CLUSTER_ID env → ROR Self() slug →
kube-system UID. Clusters without a ROR apikey still get the
UID fallback unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat: structured logs through ROR self lookup chain
Adds Info/Warn lines at every branch point so we can tell from
kubectl logs exactly which step fails:
- endpoint resolution
- apikey source (env vs secret)
- secret read (with namespace/name and which keys the secret
actually exposes when the requested key is missing)
- Self() call + result type/name
Never logs the apikey value — only its length and source.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(chart): always render apikey Role; rename to ror-self-reader
The previous template only created the Role+RoleBinding when the
apikey namespace differed from the release namespace. But the
release's ClusterRole doesn't grant secrets, so when both live in
the same namespace (the actual NHN topology — scam runs in nhn-ror
alongside the ror-apikey Secret) the SA still gets forbidden.
Drop the cross-namespace conditional and always render the Role,
in whatever namespace holds the apikey. Rename suffix to
ror-self-reader so it's clearly distinct from any prod resources
during this test rollout.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat: fetch cluster object from ROR to fill environment
After V2 Self() returns the cluster slug, hit GET /v1/clusters/<slug>
with X-API-KEY and decode `environment` (and `clusterName` for the
log line). The V2 Self response shape doesn't carry environment, and
the SDK's typed Resources() route requires guessing at a
GroupVersionKind for Cluster — a hand-rolled HTTP call against /v1
matches what SPAM already does and keeps the surface area small.
Environment priority becomes: ROR cluster object → ENVIRONMENT env.
Best-effort throughout — any HTTP/decoding failure logs a warn and
falls through to the env var.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor: use clusterName for cluster label, drop info-level traces
Two changes after the ROR self lookup chain is proven end-to-end:
1. Banner field 'cluster' now uses the cluster object's clusterName
(display name, e.g. 't-tek-003') instead of the slug. The slug
stays as cluster_id, which is what SPAM correlates by.
2. Drop the per-step Info traces that were there to debug the chain.
Warn-level lines remain on every failure path, so a regression
stays diagnosable from kubectl logs. Also drops the now-unused
'source' return value on resolveRorApiKey.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(chart): cluster-scoped RBAC for apikey read
Swap the Role/RoleBinding (which had to be created in the apikey's
own namespace) for a ClusterRole/ClusterRoleBinding so the chart
never touches that namespace. resourceNames still scopes the grant
to the configured Secret name only — broader than namespaced
(matches that name in any namespace), narrower than blanket
'get secrets'.
Trade documented in values.yaml so the choice is explicit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor: tighten ROR identity lookup
Apply code-review fixes: enforce timeouts on every ROR/k8s call so a
hung dependency can't block startup, make fetchRorIdentity pure HTTP
(apikey resolution lives in main now), and defer the kube-system UID
fetch behind a thunk so it only runs when CLUSTER_ID and the ROR slug
are both unset.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor: kube-system UID as cluster_id, ROR as metadata
ROR slug is binding metadata, not cluster identity — it can be
re-pointed, renamed, or absent. cluster_id is now always the
kube-system Namespace UID, which is stable for the cluster's
lifespan and lines up with how the rest of the ecosystem joins
cluster data (OTel k8s.cluster.uid, etc).
ROR slug/name/env still emitted on every line, but as a nested
ror_metadata group so SPAM can use it for ROR-ACL lookups while
joining internally on cluster_id. resolveClusterID priority is
now CLUSTER_ID env → kube-system UID; ROR slug is no longer in
the chain. Banner gains a ror_slug line so the ROR binding is
still operator-visible (cluster_id is now an opaque UUID).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ci: prefer PACKAGES_TOKEN for registry login
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(chart): default ror apikey namespace to nhn-ror
The cluster-scoped apikey lives centrally in `nhn-ror`; the prior
`""` default fell back to the release namespace, so every install
had to override it or hit "secrets ror-apikey not found" with the
chart-provisioned reader-role still pointing at the wrong place.
Keep `""` as the documented fallback for co-located deployments.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(chart): enable ror identity lookup by default
The chart targets NHN's Motstandskraft where every cluster has an
apikey in nhn-ror; making operators opt in to it on every install
just leaves cluster/environment/ror_metadata empty by default and
defeats the new identity scheme. Soft-fails to kube-system UID and
CLUSTER_NAME env when no apikey is reachable, so clusters without
a ROR binding still get a usable cluster_id without further config.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
| .cluster/scam | ||
| .devcontainer | ||
| .docs | ||
| .github/workflows | ||
| cmd/scam | ||
| deploy | ||
| internal/collector | ||
| .dockerignore | ||
| .gitignore | ||
| Dockerfile | ||
| go.mod | ||
| go.sum | ||
| LICENSE | ||
| README.md | ||
ArgoCD deployment
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: scam
namespace: argocd
spec:
destination:
namespace: nhn-scam
server: https://kubernetes.default.svc
project: default
source:
chart: helm
repoURL: ghcr.io/norskhelsenett/scam
targetRevision: '*'
syncPolicy:
automated:
enabled: true
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true