Skip to main content

Policies for Affected Changes

Use on: atmos.affected to reject changes to files, component instances, stack configuration, or inherited metadata. The gate runs before affected results are filtered or consumed.

When policies run​

Place rules in your policy directory. They run on the shared affected analysis used by atmos describe affected, atmos list affected, and atmos terraform plan --affected. A denial or evaluation error stops the command before it returns an affected list or schedules work. --stack, tag/label filters, output queries, and --error-mode=warn do not narrow or bypass the gate.

Commands that do not request affected analysis do not run these rules. For example, a direct atmos helm deploy app -s prod uses applicable lifecycle policies, not an implicit Git comparison. Affected rules accept action: deny; they do not execute lifecycle hooks.

Protect a label​

This rule allows new labels and new instances, but rejects changes or removals of an existing owner label, including deletion of its entire instance or parent metadata object.

policy.d/protect-owner.yaml
apiVersion: atmos/v1
kind: Policy
metadata:
name: protect-owner
description: Preserve existing instance ownership during repository changes.
spec:
policies:
preserve-owner:
on: atmos.affected
rule: >-
input.instances.exists(i,
has(i.before) && has(i.before.metadata) && i.before.metadata != null &&
has(i.before.metadata.labels) && i.before.metadata.labels != null &&
has(i.before.metadata.labels.owner) &&
(!has(i.after) || !has(i.after.metadata) || i.after.metadata == null ||
!has(i.after.metadata.labels) || i.after.metadata.labels == null ||
!has(i.after.metadata.labels.owner) ||
i.before.metadata.labels.owner != i.after.metadata.labels.owner))
action: deny
reason: Existing owner labels cannot be changed or removed.
atmos describe affected --base origin/main

Affected rules inspect before and after objects explicitly. They do not accept the single-instance match field. Never merge the two versions before testing a protected label or tag.

Input contract​

FieldContents
filesGit file changes: path, action, ambiguous; includes binary and mode changes
instancesChanged instances, identified by identity.stack, identity.component_type, and identity.name
componentsChanged groups of instances sharing a component implementation name and type
stacksChanged groups of instances sharing a logical stack name
changesStructured instance-field changes, with JSON Pointer path, action, and explicit presence flags
completetrue only after collection succeeds within the configured discovery scope
reasonsCollection diagnostics; empty on successful collection

Object changes have before, after, action (add, modify, remove), reasons, and ambiguous. Absent before/after values differ from JSON null. Stack/component versions contain an instances object keyed by serialized instance identities; they include unchanged members, not just affected members. Deleting one instance does not imply that its shared component was deleted.

Structured changes recurse through objects. Arrays and whole-object additions/removals are atomic; an entire instance addition/removal uses the empty JSON Pointer. Check before_present and after_present to distinguish absence from explicit null. JSON Pointer escapes / as ~1 and ~ as ~0.

Component-directory changes mark referencing instances with reason component_files even when their configuration is unchanged. All Git changes remain available in files, including changes outside component directories. The inventory is not a transitive dependency execution graph. Moves are conservatively represented as removal/addition, without inferring identity preservation. File additions/removals are marked ambiguous because rename identity is not established.

Reject policy-definition removals:

input.files.exists(f, f.path.startsWith('policy.d/') && f.action == 'remove')

Reject deletion of any configured instance:

input.instances.exists(i, i.action == 'remove')

Collection and enforcement​

  • Both Git worktrees must be clean. Commit changes before evaluating; staged, unstaged, and untracked changes block rather than mixing working-tree configuration with a commit file list.
  • Collection scans both checkouts without command output filters. It includes disabled and abstract instances and fails on missing imports, unsupported component types, and collapsed instance identities.
  • Values are merged static configuration. Component templates and YAML functions remain unevaluated; this is not a comparison of live Terraform outputs or rendered resource manifests.
  • Both snapshots use the active discovery configuration: included/excluded paths and naming settings. complete means complete within that scope, not every historical scope a revision might have defined.
  • Protect discovery configuration and its imports alongside policies, workflows, and CODEOWNERS. Use a trusted governing policy directory in CI. A candidate checkout must not control the enforcement configuration; a rule cannot protect itself if it is removed before loading.
  • Require a repository-level CI job that performs affected analysis. Component matrix jobs alone cannot enforce a rule against an instance removed from the matrix.

Git comparison requires no provider API permission. CI metadata uses the same provider-native snapshot as plan and lifecycle policies. The gate does not print inventory values in evaluation errors.