A handful of small k8s/GitOps CLI utilities, one npx/bunx away — no
install. Three commands so far:
check(default) — verify a Kustomize directory'sresources:list matches what's actually on diskkcc-inventory— diff a GCP Config Connector namespace against the live project to find driftget-resources— the raw resource listkcc-inventorydiffs, without the diff
Kustomize's resources: field is an explicit list, not a glob. Add a
YAML manifest to a directory managed by a Flux
Kustomization without listing it in resources:, and
kustomize-controller silently skips it — no error, no warning, the
object just never reaches the cluster.
This command catches that drift before it ships: it compares every
*.yaml/*.yml file physically present in a directory against the
resources: list in its kustomization.yaml, in both directions.
npx @nitra/cfr [dir-or-kustomization.yaml ...]
npx @nitra/cfr check [dir-or-kustomization.yaml ...] # same, explicitNo arguments checks .. Point it at one or more directories (or direct
paths to a kustomization.yaml/kustomization.yml):
npx @nitra/cfr flux/clusters/production✗ flux/clusters/production/kustomization.yaml
on disk but missing from resources: (Flux will not apply them):
- new-app.yaml
Exits 0 when every target is consistent, 1 otherwise — wire it into CI
on any path that touches a Kustomize directory with an explicit
resources: list:
# .github/workflows/cfr.yml
on:
push:
paths: ['flux/clusters/production/**']
pull_request:
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- run: npx @nitra/cfr flux/clusters/productionFor each target directory:
- every
*.yaml/*.ymlfile in the directory (except thekustomization.yamlitself) must appear inresources: - every
resources:entry that is a plain local filename (no/, no URL scheme) must exist on disk
Entries containing / (subdirectories, components) or a URL scheme
(remote bases) are out of scope and skipped — this tool only guards
against the specific footgun of a loose file sitting next to
kustomization.yaml that nobody remembered to list.
A real incident: a PR added two manifests to a Flux cluster directory but
missed adding them to resources:. The PR merged clean, CI was green,
git log showed the files — and Flux applied nothing. No error surfaced
anywhere; the only symptom was the feature silently not existing in the
cluster. This command turns that into a CI failure at PR time instead.
Config Connector (KCC) lets a Kubernetes namespace declare a GCP project's resources as CRs, with a controller reconciling git against reality. It won't tell you about the reverse direction: a resource created straight in GCP — by hand, by another tool, by a Terraform run nobody ported — that KCC has never heard of, and never will until someone points it out.
kcc-inventory is that someone. Per namespace (any namespace carrying the
annotation cnrm.cloud.google.com/project-id), it compares what's live in
the GCP project against the union of KCC declarations, OpenTofu state, and
explicit ownership catalogs. Besides IAM (including Workload Identity pools
and providers), GKE,
Artifact Registry, buckets, addresses and Cloud DNS, it includes Cloud Run
(RunService, RunJob), CloudSchedulerJob, EventarcTrigger, Pub/Sub,
Secret Manager, VPC Access, KMS, and the Cloud Run HTTP(S) load-balancer
chain (network, subnetwork, backend service, serverless NEG, URL map,
target HTTPS proxy, global forwarding rule, and SSL certificates).
Location-scoped resources use the canonical location/name ID, preventing
resources with the same name in different regions from being merged. IAM
bindings on a RunService are normalized to the same identity.
Read-only — it reports, it doesn't touch anything.
npx @nitra/cfr kcc-inventory <namespace>
npx @nitra/cfr kcc-inventory --all # every KCC namespace
npx @nitra/cfr kcc-inventory <namespace|--all> --json
npx @nitra/cfr kcc-inventory <namespace|--all> --include-system # don't filter GCP-managed noise
npx @nitra/cfr kcc-inventory nitraai \
--ownership platform/kcc/inventory-ownership.json \
--tofu platform/workload-identity/tofu
npx @nitra/cfr kcc-inventory nitraai --show-covered### namespace nitraai -> проєкт nitraai ###
== StorageBucket (проєкт nitraai) ==
UNCOVERED — old-backups-bucket
--ownership PATH and --tofu DIR are repeatable. Ownership paths use the
version-1 {resources: [...]} catalog. OpenTofu roots are read from actual
state via tofu -chdir=DIR show -json; parsing .tf configuration alone
would incorrectly mark resources that were never imported or applied as
covered. Consequently, tofu must be available on PATH only when
--tofu is used.
Overlapping declarations are an error: a canonical
(project, kind, id) resource cannot simultaneously belong to KCC,
OpenTofu, or an ownership exception.
No gcloud or kubectl CLI needed on PATH — the GCP side (Cloud Asset
Inventory, IAM, Compute Engine) and the cluster side both talk REST
directly, authenticated with Application Default
Credentials
(gcloud auth application-default login locally, a service account key
via GOOGLE_APPLICATION_CREDENTIALS, or the ambient credentials on
GCE/GKE/Cloud Build).
Cluster connection details still come from your kubeconfig (KUBECONFIG,
default ~/.kube/config) — that part isn't going anywhere, it's the only
place the cluster's API server address and CA certificate live. Set
KUBE_CONTEXT to target a specific context explicitly instead of relying
on current-context. The GCP access token is used directly as the
cluster bearer token (the same trick gke-gcloud-auth-plugin performs
under kubectl), so this only works against GKE clusters — a
kubeconfig using client certs, a static token, or a non-GCP exec plugin
(EKS, AKS, ...) won't authenticate.
By default, GCP-managed system noise is filtered out — Google-owned
service accounts, Artifact Registry shims, the GCP default network and its
subnets, Cloud DNS zone-apex NS/SOA records, resources whose
gkegw<generation>- name shows they are created by the GKE Gateway
controller, GKE-managed node pools and DNS zones, and legacy bucket ACL
entries. Pass --include-system to see it anyway. Gateway-generated backend
services, URL maps, and HTTPS proxies are derived from Gateway API objects;
do not adopt them with KCC.
- UNCOVERED — live in GCP but absent from KCC, OpenTofu state, and ownership catalogs.
- ORPHAN_KCC, ORPHAN_OPENTOFU, ORPHAN_OWNERSHIP — a controller declaration exists, but its live GCP resource does not.
- COVERED_KCC, COVERED_OPENTOFU, COVERED_OWNERSHIP — shown in
JSON and, for human output, only when
--show-coveredis passed.
kcc-inventory calls searchAllResources/searchAllIamPolicies on the
Cloud Asset API — one or two paginated calls per project instead of a
list call per resource kind. That index can lag: it has been observed
returning DNSRecordSet and ComputeAddress entries for resources
already deleted in GCP. Both are cross-checked against a direct Compute
Engine call before being reported, and any stale entry found this way is
counted and noted separately — never silently folded into DRIFT.
kcc-inventory is a diff on top of a fact-finding step: for each KCC
namespace, list what's live in GCP and what's declared under KCC. That
step is get-resources — same scan, same filtering, no drift/orphan
comparison. Useful on its own for piping into jq, feeding a different
tool, or just seeing everything a namespace touches without wading
through a diff.
npx @nitra/cfr get-resources <namespace>
npx @nitra/cfr get-resources --all
npx @nitra/cfr get-resources <namespace|--all> --json
npx @nitra/cfr get-resources <namespace|--all> --include-system### namespace nitraai -> проєкт nitraai ###
== StorageBucket ==
gcp: 7n-forgejo-lfs
gcp: old-backups-bucket
kcc: 7n-forgejo-lfs
--json emits {resources: [{namespace, project, kind, id, source}, ...], diagnostics: [...]} — source is "gcp" or "kcc", diagnostics
carries the same stale-cache/GKE-managed notes described above.
Same requirements as kcc-inventory — no gcloud/kubectl needed, GKE
only, --include-system to see GCP-managed noise.
See CHANGELOG.md.
MIT