Impact of a change
mirops scan -f reads what you're about to apply and judges it against the mirror report — never the cluster. With --enforce it blocks (exit 1) only on what the change breaks; everything else is context.
export MIROPS_SOURCE=s3://my-reports/prod/default.mirops
helm template payments ./chart | mirops scan -f - -n payments --enforce
What it reads
| Input | Example |
|---|---|
A manifest file — YAML (one or many documents) or JSON (an object or a List) | mirops scan -f k8s/deployment.yaml |
A directory — every .yaml, .yml and .json under it, subfolders included | mirops scan -f k8s/ |
| Standard input | kustomize build overlays/prod | mirops scan -f - |
A Terraform plan in JSON — kubernetes_* resources, kubernetes_manifest, kubectl_manifest | terraform show -json plan.out | mirops scan -f - |
From a Terraform plan it reads the kubernetes_* resources it models — deployment, stateful_set, daemon_set, job, cron_job, pod, replication_controller, service, ingress, persistent_volume_claim, config_map, secret, namespace, service_account, with or without the _v1 / _v2 suffix — plus any manifest in kubernetes_manifest and kubectl_manifest. A plan action maps to create, update, delete or replace (delete and recreate). It reads only Kubernetes changes: other providers are counted as not Kubernetes, no-op resources are skipped, and a helm_release is flagged not evaluable — it carries no manifests, so render the chart instead. In a directory it lists the files it ignored — other extensions, empty files, hidden directories (.git, .terraform):
IMPACT — demo-shop
read YAML: 2 Kubernetes change(s) → 2 evaluated
ignored 5 file(s):
.terraform — hidden directory
README.md — not .yaml, .yml or .json
empty.yaml — empty
main.tf — Terraform source (only a plan says what will change)
notes.txt — not .yaml, .yml or .json
It reads identity, action and references only — never Secret data, and it doesn't validate syntax or schema.
This static illustration provides a quick visual summary of the same flow:
What it blocks
With --enforce, exit 1:
| The change… | Example |
|---|---|
| deletes a PVC, Service, ConfigMap or Secret that something still uses | ✗ BLOCK deleting service/checkout — ingress/shop routes to it |
| replaces one of those (a Terraform destroy-and-recreate) while something uses it | ✗ BLOCK service/checkout is replaced (deleted and recreated), and ingress/shop uses it |
mounts a PVC that is Pending, Lost, or doesn't exist | ✗ BLOCK deployment/reports-worker mounts pvc/payments-db-data, which is Pending |
| routes an Ingress to a Service that doesn't exist | ✗ BLOCK ingress/shop routes to service/cart, which isn't in the cluster or this change |
| depends on something the same change deletes | a Deployment that mounts a PVC the plan deletes |
A dependency the change itself brings — a new app with its own PVC, Service and Ingress — passes.
What it shows but never blocks
- ℹ Context — the current state of what the change touches. A deploy that touches a component that is
Downpasses, because the deploy may be the fix:ℹ deployment/payments-api is Down right now — this change may be the fix. - ℹ Blast radius — what depends on what the change touches:
ℹ deployment/checkout — ingress/shop, service/checkout depend on it. - ? Not verified — what the mirror report can't vouch for yet: whether a ConfigMap or Secret exists, whether a new Service's selector matches pods, and the references of an object in a namespace the mirror doesn't have (new, empty, or outside the mirror's scope).
Each line starts with its mark: ✗ BLOCK fails the pipeline under --enforce; ℹ and ? never do.
Filtering by namespace
Set MIROPS_NAMESPACE (or -n) to the namespaces a repo deploys to — one, a comma-separated list, or all. A repo is then judged only on its own namespaces, never on what another team broke.
| The change… | Without -n | With -n payments |
|---|---|---|
touches payments and orders | both are judged | only payments; orders is counted in other namespaces and never blocks |
has objects without a namespace (helm template output) | not judged — "pass the one namespace it deploys to with -n" | judged in payments (with a single -n) |
| creates a namespace | judged | judged if it's one of -n |
A -n namespace that is neither in the mirror nor in the change exits 2 — most likely a typo, and judging nothing would pass the gate unchecked. -n also prints those namespaces' current state, which never blocks.
What it refuses
These aren't what will be applied. They exit 2 with the command that renders them:
| Input | Message |
|---|---|
values.yaml, Chart.yaml | Helm chart source — render it first: helm template <release> <chart> | mirops scan -f - |
A template with {{ … }} | render it first, e.g. helm template |
kustomization.yaml | Kustomize source — render it first: kustomize build <dir> | mirops scan -f - |
A .tf file | Terraform source — terraform show -json plan.out | mirops scan -f - |
A binary plan (plan.out) | convert it with terraform show -json |
A fresh mirror
The verdict is only as good as the mirror. --max-mirror-age (default 1h, MIROPS_MAX_MIRROR_AGE) refuses a report older than that with exit 2 — the operator may have stopped rebuilding it, and judging against it could pass something that is broken now.
Next: use it in CI/CD.