Policies¶
A policy inspects one node's pending change and either passes it or fails it. Policies run at plan time, over the changeset, so a rule that would block an apply shows up in the plan that precedes it rather than half-way through the run.
from atlantide.policy import enforce
enforce("require-tags", keys=["env", "owner"])
enforce("deny-destroy-in-protected", stacks=["prod"])
enforce("require-secret-refs")
enforce() must be called during config evaluation — at the top of the file,
outside any Stack block, is the conventional place. Bindings travel with the
config: they are recorded in the IR and round-trip through a .atlas artifact,
so a plan built from an artifact enforces exactly what the config that produced
it did.
Levels¶
from atlantide.core import PolicyLevel
enforce("require-tags", keys=["env"], level=PolicyLevel.ADVISORY)
| Level | Effect of a violation |
|---|---|
MANDATORY (default) |
The plan is blocked. plan and apply exit 1. |
ADVISORY |
Reported as a warning; the run proceeds. |
Narrowing by type¶
types= restricts a binding to named resource types. A lone string is one type
name, not a sequence of characters:
enforce("require-tags", types="aws.S3Bucket", keys=["owner"])
enforce("require-tags", types=["aws.S3Bucket", "aws.SqsQueue"], keys=["owner"])
Bind the same policy twice with different arguments and each binding is
evaluated with its own — one deny-destroy-in-protected per environment works
as written.
What a policy sees¶
Only actionable changes are evaluated; a NOOP node is never passed to a
policy. For each one the policy receives the node id, the pending action, the
stack, the desired resource, and the binding's arguments. The desired resource
is None for a pure DELETE — there is nothing being declared — which is why
the resource-inspecting builtins pass such nodes rather than failing them.
Built-in policies¶
require-tags¶
Requires the tag keys named in keys, or — with no keys — only that the
resource is tagged at all.
enforce("require-tags") # any tags
enforce("require-tags", keys=["env", "owner"]) # these keys
A key present with an empty value counts as missing: a blank tag carries
nothing. Resource types with no tags field are skipped rather than failed.
require-secret-refs¶
Every field marked sensitive must hold a SecretRef handle, not a literal.
Takes no arguments.
Referencing a secret by name keeps the plaintext out of the config, the IR,
state and the .atlas artifact; assigning the value directly puts it in all
four. A field declared with secret() is typed SecretRef | None, so pydantic
already rejects a literal. This policy covers what the annotation does not — a
field declared mutable(..., sensitive=True) on a plain str, which a provider
author is free to write and which accepts plaintext silently.
deny-destroy-in-protected¶
Denies destructive actions — DELETE, and the destroy half of a REPLACE — in
the stacks named by stacks.
enforce("deny-destroy-in-protected", stacks=["prod", "shared"])
With no stacks it passes everything: which stacks are protected is the config
author's call, so there is no name to assume.
deny-destroy-in-prod is the same policy under an earlier name.
Policy vs. prevent_destroy
Lifecycle(prevent_destroy=True) guards one resource and is checked by the
planner; this policy guards every resource in a stack and is checked by the
policy pass. They are independent — a resource can be covered by both.
Argument errors¶
A binding that passes something a policy cannot use fails the plan with a
PolicyConfigError naming the policy and the parameter, rather than silently
matching nothing:
require-tags: `keys` must be a name or a list of them, got int
Binding a policy to a resource type¶
Provider authors can attach a policy to a resource type, so every instance carries it without each config having to remember:
from atlantide.policy import policy
@policy("require-tags")
class Gadget(AcmeResource):
...
The binding applies to that type alone, and is evaluated alongside whatever the
config binds with enforce(). Config itself cannot use this — Atlas-lang
forbids class — so it is an API for the Python side: providers, and the
libraries that publish components.
Where policies sit in the pipeline¶
Policy evaluation is the last step of stage 7, after the prevent_destroy
guard and after --target/--replace selection — so a policy sees the
changeset that would actually run, not the unnarrowed one. See
How it works.