SPAM Cluster Agent Metadata
  • Go 98.8%
  • Dockerfile 1.2%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Jonasbg c598c0fa26
All checks were successful
Build and Deploy / build (push) Successful in 3m4s
Build and Deploy / helm-push-dev (push) Has been skipped
Build and Deploy / helm-push (push) Successful in 8s
Feat/ror self helm (#2)
* 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>
2026-05-27 14:55:49 +02:00
.cluster/scam Feat/ror self helm (#2) 2026-05-27 14:55:49 +02:00
.devcontainer feat: add devcontainer lock file and update Dockerfile with specific image digest 2026-04-16 17:35:08 +02:00
.docs feat: update image references to use ghcr.io for scam deployment and related configurations 2026-04-16 09:03:45 +02:00
.github/workflows Feat/ror self helm (#2) 2026-05-27 14:55:49 +02:00
cmd/scam Feat/ror self helm (#2) 2026-05-27 14:55:49 +02:00
deploy feat: watch EndpointSlices for external service IP visibility 2026-04-16 23:49:55 +02:00
internal/collector merge feat/snapshot-counter-ack into feat/snapshot-reconcile 2026-05-12 12:15:55 +02:00
.dockerignore feat: add spam-operator Kubernetes operator 2026-04-15 20:58:17 +02:00
.gitignore feat: restructure spam-operator to scam, including Helm chart, deployment, RBAC, and documentation updates 2026-04-16 08:59:05 +02:00
Dockerfile Feat/ror self helm (#2) 2026-05-27 14:55:49 +02:00
go.mod Feat/ror self helm (#2) 2026-05-27 14:55:49 +02:00
go.sum Feat/ror self helm (#2) 2026-05-27 14:55:49 +02:00
LICENSE chore: add MIT LICENSE 2026-04-24 09:15:59 +02:00
README.md Create README.md 2026-04-17 15:25:07 +02:00

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