oci-nuke¶
oci-nuke deletes every resource in an Oracle Cloud Infrastructure compartment subtree. It is
for tearing down sandbox, demo and CI tenancies — the OCI counterpart to aws-nuke,
gcp-nuke and azure-nuke, built on libnuke and the
official oci-go-sdk.
Read this page before you read an install command
This tool exists to destroy things. Its one non-negotiable property is that it must never delete something the operator did not intend to delete. Coverage breadth is worth nothing if the safety model leaks — so the safety model is documented first, and the install instructions come after it on purpose.
Safety Model¶
The tool must never delete something the operator did not intend to delete. This is the project's core value, and it is enforced structurally, not by convention:
- Dry-run is the default. Every
run(or its aliasnuke) reports what it would delete without deleting anything.--no-dry-runis the only way to make the tool actually remove a resource. There is no other flag, config setting, or environment variable that bypasses this. - The tenancy gate is a hard failure. Before any resource is listed,
oci-nukemakes a live call to the OCI Identity API (GetTenancy) to verify the credentials in use actually belong to the tenancy named in the config file. A mismatch exits non-zero immediately — the gate does not degrade to a warning, and it runs before the first listing call, not after. - The compartment blocklist is OCID-keyed, never name-keyed, and non-negotiable. OCI compartment names are unique only within their parent, so matching by name is a real bypass vector (a differently-scoped compartment can share a name with a protected one). Every blocklist entry and every tenancy comparison is by OCID.
- A destructive run requires the operator to type the target compartment's display name.
Before
--no-dry-rundeletes anything,oci-nukeprints the live-resolved display name of the target compartment and requires it to be typed back exactly, mirroringaws-nuke's own account-alias confirmation.--forceis the only way to skip this, and doing so is always recorded in the run output as aforce=truelog line. - Filters are resolved per compartment, never shared across a run. A subtree can span many compartments with different protection needs; each one gets its own resolved filter set, so a filter configured for one compartment can never silently apply to another.
Every guarantee above is enforced by a dedicated, still-passing test:
- Dry-run is the default —
TestRunPipeline_EmptyPlanSucceeds(pkg/commands/run) exercises the full pipeline with--no-dry-rununset and asserts nothing is deleted. - The tenancy gate is a hard failure —
TestRunPipeline_TenancyMismatchBlocksBeforeListing(pkg/commands/run) asserts a tenancy mismatch short-circuits before any compartment is listed. - The blocklist is OCID-keyed and pruned during traversal, never as a post-filter —
TestRunPipeline_BlocklistedAncestorRefusedandTestRunPipeline_ResolvesSubtreeAndSkipsBlocklistedDescendant(pkg/commands/run), pluspkg/scope's ownTree.Resolve/Tree.AncestorBlocklistedunit tests. - The tenancy root can never be a target —
TestRunPipeline_TenancyRootTargetRefused(pkg/commands/run). - Per-resource scope re-verification cannot be disabled —
TestRegister_PanicsWhenResourceDoesNotImplementCompartmentScopedandTestRegister_WrapsListerSoRegistryGetListerAppliesScoping(pkg/ocinuke) assert every registered resource type is both required to implement the compartment accessor and wrapped byScopedListerat registration time, with no opt-out. --forceusage on a destructive run is always logged, never silent —TestRunPipeline_ForceIsLoggedWhenUsed(pkg/commands/run) asserts aforce=truelog entry is emitted on every--forcerun. See "Protection filters" below for the full--force/--force-sleepbehavior and the confirmation prompt's own full-line read (fixing a real whitespace-truncation bug in an earlier draft).- Protection filters (protect-by-tag, minimum age) are off by default and documented as strongly recommended — see "Protection filters" below.
- Every skip carries a machine-readable reason (
out-of-scope,blocklisted,protected-by-tag,too-young) —pkg/scope.RefusalReason/pkg/scope.SkipEvent. - Filters are resolved independently per compartment, never shared across a run —
TestBuildNukes_AppliesPerCompartmentFilters(pkg/commands/run).
Protection filters¶
--force / --force-sleep. A destructive run (--no-dry-run) normally stops and requires
the operator to type the target compartment's live display name before proceeding. --force
skips that prompt entirely — intended for CI, never for interactive use — and every --force run
logs a force=true line recording that the confirmation was bypassed. --force-sleep (default
and minimum: 3 seconds) is how long oci-nuke waits after a --force run starts before
continuing, giving a human watching CI output a last chance to cancel.
oci-nuke run --config config.yaml --compartment-id <ocid> --no-dry-run --force --force-sleep 10
Protect-by-tag and minimum age. Both are ordinary filters under the existing filters/
presets config keys — no separate schema section — using the reserved __global__ filter key,
which applies to every resource type uniformly:
filters:
ocid1.compartment.oc1..demo:
filters:
__global__:
- property: "freeform_tags.Persistent"
type: exact
value: "true"
- property: "time-created"
type: dateOlderThan
value: "24h"
The first entry protects any resource tagged Persistent=true (freeform or defined tags both
work the same way). The second is the minimum-age filter, and its polarity is easy to get
backwards: dateOlderThan with value: "24h" protects anything YOUNGER than 24 hours, not
anything older. Worked example, straight from the filter engine's own arithmetic
(fieldTime + duration, then compared against now):
- A resource created 1 hour ago against
dateOlderThan: "24h":1h-ago + 24hlands 23 hours in the future, which is after now — the filter matches, and the resource is protected. - A resource created 30 days ago against the same
dateOlderThan: "24h":30d-ago + 24hstill lands 29 days in the past, which is before now — the filter does not match, and the resource is eligible for deletion.
In other words: dateOlderThan: "24h" reads as "protect anything not yet 24 hours old," not "only
delete things older than 24 hours." Get this backwards in a config and a "protect anything
deployed in the last hour" filter can silently protect nothing.
Both protections are off by default, and both apply per compartment via the same
ResolveFilters(compartmentID) merge every other filter goes through — see guarantee 9 above.
Deleting the compartment itself¶
Deleting the target compartment itself, after everything inside it is gone, is off by
default. Enable it via --delete-compartments or settings.compartment.delete: true in the
config file — whichever source enables it wins: this is a pure union, never an exclusive
choice, so the flag can never turn off a config key that already turned the behavior on.
oci-nuke run --config config.yaml --compartment-id <ocid> --no-dry-run --delete-compartments
A compartment delete needs real time: OCI's own DeleteCompartment work request is asynchronous,
and OKE/load-balancer/NAT-gateway cleanup inside it can take minutes. Enabling this requires
--max-wait-retries * <run-sleep> to be at least 15 minutes — the default --max-wait-retries
(180, at the fixed 5-second run-sleep interval) already satisfies this floor; lowering it below
the floor while --delete-compartments is set refuses the run before any API call.
The target compartment and its whole in-scope subtree are deleted bottom-up, deepest-first — every descendant compartment's own removal completes (or exhausts its retry budget) before the next level up is even attempted, and the operator's original target is always last. This means a deeply nested tree's total wall-clock ceiling is the sum, not the max, of every level's own retry budget — each level can spend its entire budget before the next level starts, since sibling-subtree deletion is deliberately sequential in this first implementation. Size CI or automation timeouts accordingly for deeply nested targets: three nesting levels at the 15-minute floor is up to 45 minutes, not 15.
A compartment is only deleted once it is actually empty. While the run is still removing things
in it, its delete waits, without calling OCI. If anything will outlive the run — a vault, key or
secret only scheduled for deletion, a protected or too-young resource, backup residue, a config
filter's catch, a child compartment that stays — it is not attempted at all and is reported as
compartment-not-empty, naming what holds it; the dry-run plan says so too, so nobody approves a
delete that cannot happen. If OCI refuses the delete of a compartment the run had nothing in
(something in an unscanned region, or a type oci-nuke does not know), that refusal is final for
the run rather than retried for the whole wait budget. Each of these is picked up again by a later
run. A compartment with a blocklisted descendant is never attempted either.
--delete-compartments includes the target compartment itself. To clean a parent's empty
children but keep the parent, filter the parent's own Compartment out by id under its
filters block.
See docs/adr/0001-build-on-libnuke.md for why this
project builds on ekristen/libnuke rather than
adopting or forking an existing OCI-specific deletion script, and
docs/adr/0002-cobra-over-urfave-cli.md for the CLI
framework decision.
Where to go next¶
- What cannot be deleted — platform limits no configuration changes.
- Getting started — install a signed archive or the container image.
- Configuration — the full
oci-nuke.yamlreference, generated from the same JSON Schemaconfig validateuses. - Resource types — every registered type, generated from the registry.
- Contributing — the resource scaffold and the contribution flow.