Home / Stories / Beyond Flux on GKE
Story · Platforms In development

A new estate gets measured before anything is proposed.

Acme runs one platform repository that drives six clusters: two OpenShift, three AKS, one more OpenShift spoke, with a hub cluster reconciling the rest. Before the assistant is allowed to write a single line there, it takes a read-only census: which files land on which cluster, who owns each object, and how much of the estate its agents can actually analyse today.

Starts fromA local clone of your platform repository at a pinned commit, plus (optionally) read-only access you grant during the session.
Looks atEvery Flux Kustomization and HelmRelease, kustomize overlays and patches, kubeConfig references, and the hub's Flux inventory.
You getA coverage report: placements per file, ownership per object, topology findings, and an honest "not analysable yet" count.
01 · Which platform is this?

Flavour and platform come from authoritative signals.

The cluster-deployment scan asks the API server what it serves. The config.openshift.io Infrastructure object means OpenShift, and its status.platform says vSphere, bare metal or Azure. harvesterhci.io will mean Harvester (roadmap). RKE2 and k3s are read from node markers, AKS from its node-pool labels.

Absence is not evidence. A cluster with no cloud labels is unknown, not "bare metal", and a vSphere cluster running another vendor's CSI driver is still vSphere.

02 · A runtime that cannot write

Read-only is a property of the code, not a flag.

The reach census runs in a separate, sealed runtime. Its import graph does not contain the merge-request writer, the proposal store, the chat notifier, the LLM engine or any credential loader, and a test walks that graph on every change, with a planted violation it must catch.

It reads a local clone at an immutable commit through plain git plumbing. It never fetches, checks out or touches the working tree, and no model sees your YAML during the census.

03 · Hub and spokes

A file's cluster is decided by its parent, not its path.

In a hub-and-spoke Flux layout, the hub reconciles everything and reaches each spoke through a kubeConfig secret. Plain objects under a Kustomization with spec.kubeConfig land on the spoke. A HelmRelease patched to carry a kubeConfig lives on the hub while its chart installs into the spoke.

The graph resolves every file to a controller, object and workload cluster, or to "unresolved". It never quietly assumes "local".

04 · Topology findings

The mistakes that only a topology can show.

Deterministic rules run over the graph: a HelmRelease inside a spoke-applied layer (fatal: spokes have no HelmRelease CRD), a path no Kustomization covers, a ${VAR} nobody declares, a directory Flux will apply recursively because it has no kustomization.yaml, a Namespace that a hub-render layer quietly creates on the hub.

Where you already run your own render-based check, the graph is compared against it, Kustomization by Kustomization. On the first real estate the two agreed on every Kustomization.

05 · Who owns each object?

Ownership is per object, read from the hub's inventory.

Real estates are mixed. In one namespace, Flux may own the Namespace, RBAC and Routes while the Deployments are applied by a CI script. A namespace allowlist is only a ceiling.

With read-only access you grant during the session, the census reads each Flux Kustomization's status.inventory on the controller cluster and marks every object as in-inventory, not, or abstain. Objects Flux does not own are listed as such, never assumed to be managed by git.

06 · How much can we actually analyse?

An unanalysed repository must never read as clean.

The coverage meter counts, per Flux root, files placed, files unresolved, kustomize render inputs, and kinds the shipped agents do not branch on yet: OpenShift Route, EgressIP, MachineConfig, OLM subscriptions, KubeVirt VirtualMachine.

Those are reported as "seen, not analysed", with the count, so a quiet result is never mistaken for a healthy one. The number also tells us which platform pack to build next.

07 · Argo CD Roadmap

The same questions, asked of a different controller.

Planned: Argo CD Application and ApplicationSet graphs as a second reconciliation source. The census already separates "which cluster does this file land on" and "who owns this object" from the Flux specifics, which is the seam an Argo CD reader plugs into.

08 · Below the cluster Roadmap

Terraform and OpenTofu: measured state versus declared state.

Planned: the same loop applied to cloud infrastructure code. Read the declared resources and the provider's actual state, and propose the minimal reviewed diff where they disagree, managing only what the repository declares.

Nothing on this step exists yet. It is where the platform coverage is heading.

cluster flavour · platform detection
  • hub-prod config.openshift.io served → openshift · Infrastructure: vsphere
  • spoke-a kubernetes.azure.com/* labels → azure
  • spoke-b kubernetes.azure.com/* labels → azure
  • edge-1 rke2 node marker → rke2
  • vm-1 harvesterhci.io served → harvester-hci (roadmap)
  • lab-3 no authoritative signal → platform unknown
platform never inferred from absence, installed operators or CSI drivers
Inside the reach runtime
  • git reader: rev-parse, ls-tree, cat-file
  • Flux graph + kustomize closure
  • deterministic rules
  • read-only Kubernetes reader (session only)
  • local report writer
Not importable from it
  • merge-request / PR writer
  • proposal store
  • chat notifier
  • LLM engine
  • app config + credentials
emission firewall test
 import closure excludes the writer spine
 positive control: planted import caught
 git verbs limited to read plumbing
 poisoned writers untouched by a full census run
  names in the report use aliases, never real ones
hub-prod · controller
local12 files
ns
rb
rt
HelmRelease objectshub
hr
hr
hr
spoke-a · via kubeConfig
remote_directplain CRs
cm
sa
remote_hub_rendercharts
spoke-b · via kubeConfig
remote_directplain CRs
cm
unresolvednever "local"
?
clusters/spoke-a/addons.yaml
kind: Kustomization
spec:
  path: ./infrastructure/spoke-a
  patches:
    - target: { kind: HelmRelease }
      patch: - op: add  path: /spec/kubeConfig  # object on hub, chart on spoke
reach.flux.* · deterministic findings
  • helmrelease_under_kubeconfig 0 · fatal class, none present
  • uncovered_path 11 files no Kustomization reaches
  • autogenerated_kustomization 4 dirs applied recursively
  • substitution_undeclared 2 · ${CLUSTER_NAME} with no value
  • hub_render_layer_non_helm_object 1 · Namespace lands on the hub, not spoke-a
oracle: your render check vs the graph · 19/19 Kustomizations agree
Flux Kustomizationon hub-prod
status.inventoryread-only, explicit context
Ownershipper object, per placement
namespace shop-prod · who owns what
  • Namespace shop-prod in Flux inventory
  • RoleBinding, Route ×2, ExternalSecret in Flux inventory
  • Deployment orders-api, payments applied by CI · not in inventory
  • ConfigMap ui-templates on one hub only · flagged for you
coverage.md · acme-platform @ pinned commit
analysable 72% seen, not analysed unresolved
  • LocalVolume · EgressIP · MachineConfig OpenShift · no agent branches on these yet
  • WebApplicationFirewallPolicy · Gateway API AKS add-ons
  • Subscription · OperatorGroup OLM
  • kustomize patches and config docs render inputs · excluded from the denominator
Illustrative numbers, shaped like the first real census.
FluxKustomization · HelmRelease
Placement + ownershipcontroller-neutral
Argo CDApplication · ApplicationSet
planned · apps/orders-api.yamlroadmap
apiVersion: argoproj.io/v1alpha1
kind: Application
spec:
  source: { path: apps/orders-api/overlays/prod }
  destination: { name: spoke-a, namespace: shop }  # → placement
planned · infra/network.tfroadmap
resource "google_compute_firewall" "ingress_https" {
  source_ranges = ["0.0.0.0/0"]
-  ports         = ["443", "8443"]
+  ports         = ["443"]  # 8443 unused for 90 days
}

The report you get.

Not a merge request: the census cannot open one. It writes a local report that we walk through with you. Illustrative, shaped like the real output.

Report

Reach census · acme-platform

6 clusters · 1 hub · 1 pinned commit · read-only · aliased names
reach censusread-onlyno LLMoracle 19/19
CoveragePlacementsOwnershipFindings
Summary

analysable 312 / 438 files (71%) · 151 render inputs · 11 unresolved. The commit read matches the revision both hubs currently reconcile.

Flux rootFilesAnalysableUnresolved
clusters/hub-prod118962
infrastructure/spoke-a74510
tenants/1421129
apps/125683

Seen, not analysed: 106 objects of OpenShift and AKS-native kinds. A platform pack would lift reach by at most that many.

PlacementMeaningFiles
localapplied on the cluster that reconciles it373
remote_directapplied to a spoke via spec.kubeConfig82
remote_hub_renderHelmRelease on the hub, chart on the spoke20
multi-placementshared base reconciled by several clusters181
uncoveredno Kustomization reaches the path19
Per object, from each hub's Flux inventory
  • 790 in-inventory verdicts across both hubs.
  • 46 declared in git but not in any inventory (16 files): overlay namespace rewrites we do not model yet, plus 6 genuine candidates listed for you.
  • 0 abstentions. Shared bases get one verdict per placement.
For you to decide
  • A Namespace created on the hub by a hub-render layer, three times.
  • A ConfigMap present in one hub's git path and absent from the other.
  • Privileged developer RBAC declared on both hubs.
# findings.jsonl (excerpt)
{"rule": "reach.flux.uncovered_path", "path": "apps/legacy-report/prod/", "count": 19}
{"rule": "reach.flux.autogenerated_kustomization", "path": "tenants/team-b/", "count": 6}
{"rule": "reach.flux.substitution_undeclared", "var": "${CLUSTER_NAME}", "count": 3}
{"rule": "reach.flux.hub_render_layer_non_helm_object", "kind": "Namespace", "lands_on": "hub-prod"}
{"rule": "reach.flux.helmrelease_under_kubeconfig", "count": 0}
R
oracle comparison your own render-based spoke check vs the graph

24 of 24 Kustomizations classified the same way; every hub-render HelmRelease resolved to the right spoke. Divergences would be reported as graph defects.

What keeps it honest.

A new estate is read long before it is written to, and "we could not tell" is always a visible answer.

Cannot emit, by constructionThe census runtime has no import path to any writer, store, notifier or LLM. A test with a planted violation proves the check works.
Pinned, not fetchedIt reads a clone at an immutable commit you pulled. The commit and its age are stamped on the report, so staleness is visible.
Live access is session-onlyOwnership reads use an explicit read-only context you grant while present. The unattended bot identity never gains access to your clusters.
Unknown is not cleanUnresolved placements and un-analysed kinds are counted as such. They never quietly fall into "local" or "no findings".
Authority over guessesPlatform comes from the Infrastructure object or cloud labels. Absence of a signal gives "unknown", never a default.
Aliases in every outputReports carry aliases for your clusters and repositories. The mapping stays in a local, uncommitted file.

Where this stands.

Live
  • Sealed reach census of a local clone: coverage, placements, topology findings
  • Hub-and-spoke Flux resolution, checked against a customer's own render check
  • Import firewall with positive control
  • Flavour and platform detection (OpenShift, RKE2, k3s, AKS)
In development
  • Per-object ownership from live Flux inventory (operator-present runs)
  • OpenShift cluster-shape collector
  • Kustomize overlay namespace rewrites in ownership matching
Roadmap
  • Harvester: detection and cluster-shape collector
  • OpenShift and KubeVirt rule packs (Route, EgressIP, VirtualMachine…)
  • Census analysis on a local, self-hosted model
  • Letting regular agents propose MRs on a censused estate
  • Argo CD Applications and ApplicationSets
  • Terraform / OpenTofu cloud IaC
← Previous storyLearning from your feedback