@nitra/cfr
v1.1.0
Published
OpenTofu + Flux utilities: validate rendered GitOps contracts and reconcile live GCP coverage through OpenTofu state.
Maintainers
Readme
@nitra/cfr
Read-only tools for the OpenTofu + Flux GitOps standard. Node.js >=18. Cloud infrastructure belongs to OpenTofu; Kubernetes desired state belongs to Flux. Version 1 has no Config Connector inventory, Kubernetes transport, namespace discovery or kubeconfig dependency.
Commands
npx @nitra/[email protected] --version
npx @nitra/[email protected] check ./k8s/main
kustomize build ./k8s/main | npx @nitra/[email protected] contract --json -
npx @nitra/[email protected] get-resources --project azovmemo --json
npx @nitra/[email protected] tofu-inventory --project azovmemo \
--tofu ./tofu/gke-foundation --strict --jsonFor reproducible CI, use an exact dependency and commit package-lock.json,
then npm ci and npx --no-install cfr .... The lockfile binds transitive
versions and npm integrity hashes; version-only npx examples are interactive.
The tool never imports resources, applies plans, changes clusters or publishes
Flux artifacts. Consumers must explicitly upgrade to this major version.
check
Check a Kustomize directory's resources: against its sibling YAML files.
The directory-only shorthand remains supported. YAML is parsed, not guessed
with line patterns. This checks local inclusion, not rendering or Flux policy.
npx @nitra/[email protected] check ./k8s/maincontract
Validate the complete supplied set of concrete rendered Kubernetes manifests.
Render templates/ResourceSets first and include referenced sources and Flux
dependencies. Multiple files and Kubernetes List objects are supported.
npx @nitra/[email protected] contract --json root.yaml controllers.yaml apps.yamlStatic checks include:
- No cloud-owner CRs, Config Connector controller manifests, RBAC or known
chart/OLM installation paths; no plaintext
Secret.data/stringData. - No duplicate resource identities or unresolved ResourceSet templates.
- OCI sources for roots/workloads; workload
ref.digestuses SHA-256. Aflux-systemroot may use a separate SemVer channel. An OCI source used asHelmRelease.chartRefmay use an exact chart tag, notlatest. - Root Kustomization:
prune:false,wait:false, no child health checks. Children selectwait:trueor targeted health checks, not both; explicit interval, timeout, path, prune and wait fields. - Container images and Kustomization image overrides use immutable SHA-256 digests. Helm release/target/storage identities and chart versions are explicit.
- Referenced sources/dependencies exist in the supplied bundle; no Flux dependency cycles.
JSON: {contractVersion, ok, checked, findings}. Findings contain resource
identities and rule codes, never manifest/Secret payloads. Exit 0 = pass,
1 = violations/read/parse error, 2 = invalid command usage.
This is a static subset of the shared contract, not full compliance certification.
It does not resolve remote artifacts, Helm values/image templates, arbitrary
custom installers or OpenTofu configuration. Add CRD/schema validation,
secret scanning, provenance/retention checks, exact-plan gates, and direct
application/data acceptance separately. A valid subset cannot prove that a
missing cluster object or source has been included. contractVersion:1 names
the validator profile, not a pin to a particular external contract Git revision.
Flux documents that wait:true ignores healthChecks; see the
Kustomization reference.
get-resources
Scan one explicit GCP project using Application Default Credentials (ADC).
No gcloud/kubectl executable is required. Output contains resource identities
and diagnostic reasons, not application secret values or Secret Manager payloads.
npx @nitra/[email protected] get-resources --project azovmemo --json
npx @nitra/[email protected] get-resources --project azovmemo --include-systemJSON: {project, resources, diagnostics}. Human output groups resource identities.
GCP-managed defaults/controller resources are filtered with explicit diagnostics;
--include-system includes system resources. This is inventory, not an adoption
instruction. Dynamic GKE disks, LB/NEG and similar resources must retain their
controller owner, not be blindly imported into OpenTofu.
tofu-inventory
Compare live GCP identities against actual state from every supplied OpenTofu
root, using tofu -chdir=DIR show -json (not HCL text matching). Initialize
the correct backend first; do not supply a plan or state file from another
project/backend. Cloud reads use ADC, and OpenTofu must be on PATH.
npx @nitra/[email protected] tofu-inventory --project azovmemo \
--tofu ./tofu/gke-foundation --tofu ./tofu/other-root \
--show-covered --strict --jsonJSON remains an array of classifications/diagnostics. Important statuses:
| Status | Meaning |
|---|---|
| covered_opentofu | Live identity matches a supported managed resource in actual state |
| uncovered | Live resource has no matching OpenTofu owner |
| orphan_opentofu | State resource has no matching live identity |
| controller_managed | Exact successful RestorePlan/VolumeRestore evidence proves derived disk ownership |
| ignored | Explicit system/runtime/controller scope diagnostic |
| unsupported_live_resource | Unmapped live infrastructure type; blocks strict mode |
| unsupported_controller_resource | Unmapped/invalid OpenTofu state identity; blocks strict mode |
--strict fails on uncovered/orphan resources and unsupported live/state types.
Overlapping owners across state roots are errors, never resolved by precedence.
API/auth failures fail the command; no successful empty inventory fallback.
Read-only API coverage spans IAM/WIF and policy bindings, GKE, registry, storage,
DNS/network/Compute, Cloud Run/Scheduler/Eventarc/PubSub, secrets/KMS,
backup/restore and additional supported cloud assets. IAM conditions are part
of identity. Asset Inventory lag is reconciled against direct APIs for selected
known cases; a green report is not a provider plan or exhaustive runtime proof.
Ignored (system-managed / data) resources
--strict should not demand OpenTofu ownership for things GCP, GKE or a
controller create and maintain, or that are data. These are reported as
ignored with a reason (individual entries for tagged resources, asset_scope
counts for assets; both visible with --json, entries also with --show-covered).
A resource that IS in OpenTofu state stays covered_opentofu. --include-system
turns all built-in rules off. Rules and rationale live in lib/ignore-rules.mjs.
| Ignored | Why |
|---|---|
| secretmanager.googleapis.com/SecretVersion | payload = data; the Secret itself is still checked (SecretManagerSecret) |
| Disks with a kubernetes.io/created-for/pvc/* label/description | lifecycle owned by the PersistentVolume (restore-derived disks keep their RestorePlan logic) |
| ComputeInstanceSettings with empty metadata | per-zone default object |
| Pub/Sub container-analysis-{notes,occurrences}-v1[beta1] | created by Container Analysis |
| Firewalls gke-<cluster>-<8hex>-[ipv6-]{all,vms,inkubelet,exkubelet,master} | created by GKE |
| DNS response policies/rules named gke-* (policy: name or all rules gke-*) | created by GKE Cloud DNS |
| Dataplex entry groups @<service> | Google system groups |
| Project, ProjectBillingInfo | bootstrap/organisation level, not project-scoped IaC |
| Compute project default network tier, Cloud Armor tier | project-wide defaults on every project |
| Alert policy "GKE Security Bulletin Affecting Your Cluster" | created by GKE; matched by exact name AND the container.googleapis.com/notifications / SecurityBulletinEvent log filter (the policy has no creator label; other alerts stay visible) |
| Route default-route-<hex>: network default, 0.0.0.0/0, next hop default-internet-gateway | default internet route; any other shape stays visible |
| Soft-deleted custom roles (absent from IAM roles.list) | Cloud Asset still returns deleted roles; shown as one aggregated ignored entry |
| Predefined iam.googleapis.com/Role (//iam.googleapis.com/roles/*) | Google-managed; custom roles stay unsupported/visible |
Never ignored: service accounts and keys, IAM members/policies, buckets, secrets,
WIF pools/providers, custom roles, enabled APIs, addresses, Artifact Registry,
snapshots, alert policies, Certificate Manager, Cloud Armor, notification channels,
user networking. For project-specific exceptions use the explicit, repeatable
--ignore KIND_OR_ASSET_TYPE:ID_REGEX (only uncovered / unsupported-live
findings; the reason shows user --ignore ...).
Also mapped (state <-> live): google_compute_security_policy /
google_compute_region_security_policy, google_monitoring_notification_channel,
google_certificate_manager_certificate, google_certificate_manager_dns_authorization,
google_project_iam_custom_role (project custom roles only; organisation roles stay
unsupported). Unowned ones are reported as uncovered, never silently hidden.
Orphan IAM members are re-checked before being reported: Cloud Asset's IAM search is
eventually consistent, so for each orphan candidate cfr reads the owner's policy
(project / service account / bucket / secret) with a read-only getIamPolicy (one read per
target, at most 25). If the binding exists it is covered_opentofu with
verified_via: getIamPolicy; on a denied/failed read it stays orphan and a warning is
printed to stderr. --no-verify-iam disables this.
Secret-level IAM (google_secret_manager_secret_{iam_member,iam_binding,iam_policy})
is mapped; Cloud Asset reports it by project number, cfr normalises that to the
project ID taken from the scanned assets. A stale orphan caused by Cloud Asset index lag is resolved by the getIamPolicy check above.
covered_opentofu does not establish absence of a second external reconciler.
During migration, separately prove KCC abandon/recreate-path removal before
enabling OpenTofu writes. No automatic ownership transfer is implemented here.
portability
OpenTofu coverage is not readiness to move the workload to another Google Cloud
project. cfr portability shows ahead of time what will hinder a move. It is
read-only and reads only metadata and counters (never secret payloads, objects
or image contents).
npx @nitra/cfr portability --project PROJECT --dir tofu --dir flux [--tofu tofu/root] [--json] [--strict]--dir is repeatable (default .); *.md (for literal checks), docs/,
.terraform/, node_modules/ and .git/ are skipped. The project number comes
from Cloud Asset. Findings carry severity (error|warn|info), check,
location (file:line or live:...), message, hint. --json emits
{findings, summary}; exit is 0, or 1 with --strict when any error exists.
| Check | Severity | What |
|---|---|---|
| hardcoded-project-id | error | literal project ID, grouped by context (bucket/resource name, pkg.dev/PROJECT/, SA email, PROJECT.svc.id.goog, projects/PROJECT, bare ID) |
| hardcoded-project-number | error | literal project number |
| globally-unique-names | warn | bucket names without a variable part; literal dns_name (the Artifact Registry host is project-scoped by path, so it is reported by the ID check) |
| data-not-in-iac | info | live counters + migration advice: SecretVersion per secret, buckets, Artifact Registry DOCKER images, PVC disks, snapshots, user-managed SA keys, Cloud SQL / BigQuery / Pub/Sub subscriptions / Firestore / Spanner / Redis when present |
| uncovered-or-unsupported | warn | needs --tofu: uncovered, orphan and unsupported resources (not ignored) grouped by type |
| out-of-git-steps | warn | heuristic: kubectl create secret, kubectl apply -k, helm install, gcloud (.sh/.yaml), manual/bootstrap wording in comments, scripts and README |
| external-trust | info | live WIF providers' issuer/audiences and static issuer_uri in .tf |
| static-ips-and-dns | info | external static addresses (IPs change, DNS needs updating) |
The human report ends with blockers (error): N; warnings: N; data to migrate: ....
Heuristics can have false positives; messages never echo source lines.
Upgrade from 0.x
- Remove
kcc-inventoryjobs. The removed command returns exit 2 with guidance. - Replace
get-resources <namespace>/--allwith one explicitget-resources --project PROJECTinvocation per project. - Update raw inventory JSON consumers to the new envelope.
- Pass every authoritative state root to
tofu-inventory; no KCC or ownership catalog flags can satisfy coverage. - Fix unknown/missing arguments: version 1 rejects them with exit 2.
- Add the rendered
contractcheck as a separate gate; it does not replacecheck, schema validation, inventory or runtime acceptance.
Do not enable final strict migration gates by silently excluding old cloud CRs from reports. Record temporary failures/exceptions while transferring ownership, then remove KCC resources/controllers/addons in the actual migration workflow.
Development and release
npm ci
npm test
npm pack --dry-runGitHub CI tests Node 18/20/22. Tag v1.0.0 must match package.json.
The existing GitHub-hosted publish workflow uses npm trusted OIDC publishing,
with pinned actions and npm 11.6.2; no long-lived npm publish token.
See npm trusted publishing.
MIT license. Historical behavior is recorded in CHANGELOG.md.
