Documentation
¶
Overview ¶
Package model is the vocabulary: what can be stopped, what stopping it means, and — the part that matters most — what must never be touched.
This tool exists to be fired at two in the morning by someone who has just seen a bill. Everything here is shaped by two rules that follow from that:
Stop, never delete. A stopped thing costs nothing and comes back. A deleted thing may not, and nobody deletes carefully at two in the morning. The restore is the product. Stopping an account is easy and useless on its own. What makes this safe to fire is that the exact prior state is recorded durably first, so putting it back is mechanical rather than archaeological.
Index ¶
- Constants
- Variables
- func IsNeverTouch(s string) bool
- func PriorBool(prior map[string]any, key string) (bool, bool)
- func PriorFloat(prior map[string]any, key string) (float64, bool)
- func PriorInt(prior map[string]any, key string) (int, bool)
- func PriorInt32(prior map[string]any, key string) (int32, bool)
- func PriorString(prior map[string]any, key string) (string, bool)
- type Action
- type BlastRadius
- type Entry
- type Kind
- type Phase
- type Plan
- type Refusal
- type Resource
- type Snapshot
Constants ¶
const ( ResultChanged = "changed" ResultSkipped = "skipped" ResultFailed = "failed" ResultUnchanged = "unchanged" )
What happened to one entry. Recorded per entry rather than per run so a partial fire can be restored without re-running against resources that were never changed.
const GlobalRegion = "global"
GlobalRegion is the Region recorded for services that have none. CloudFront is the only one so far. It is a real value rather than an empty string so a state file says plainly where the resource lives, and so the executor can route it deliberately instead of by accident.
const RDSAutoRestart = 7 * 24 * time.Hour
RDSAutoRestart is the trap that makes stopping a database different from stopping anything else: AWS restarts a stopped RDS instance by itself after seven days. A kill switch that silently un-kills a week later is worse than no kill switch, because nobody is watching by then.
Variables ¶
var NeverTouch = []string{
"s3", "efs", "fsx", "dynamodb", "glacier", "backup",
"ebs-volume", "snapshot", "ami", "route53-zone", "iam", "kms",
"cloudtrail", "config", "secretsmanager", "ssm-parameter",
}
NeverTouch is the set of things this tool will not act on under any flag.
It is a deny list rather than an allow list on purpose. An allow list grows by accident: someone adds a resource kind, forgets the exclusion, and a cost tool deletes a bucket. These are the things whose loss is unrecoverable, so the rule is absolute and there is no option to override it.
Functions ¶
func IsNeverTouch ¶
IsNeverTouch reports whether a service or resource identifier names something in the protected set. Matched on the ARN service field and on the free-form kind, so it catches both "arn:aws:s3:::bucket" and a hand-built resource.
func PriorBool ¶ added in v1.10.0
PriorBool reads a boolean out of the recorded prior state.
Only a real bool counts. A CloudFront distribution's prior "enabled" decides whether restore switches it back on, and coercing a string or a number into that answer is how a distribution stays dark after a restore reports success.
func PriorFloat ¶ added in v1.10.0
PriorFloat reads a rate out of the recorded prior state.
API Gateway throttles are floats — 10.5 requests/second is legal — so this cannot go through PriorInt without rounding a limit the account was actually set to. A negative rate is refused: the API rejects it, and restoring to a value that fails is not a restore.
func PriorInt ¶
PriorInt reads an integer out of the recorded prior state.
JSON round-trips numbers as float64, so a desired count of 3 comes back as 3.0 and a naive type assertion to int fails — silently restoring zero, which would leave the account down after a "successful" restore.
func PriorInt32 ¶ added in v1.9.1
PriorInt32 reads an integer out of the recorded prior state and narrows it to the int32 the AWS APIs take, refusing rather than wrapping.
The prior comes back through JSON, so nothing constrains its range on the way in: a hand-edited or corrupted state file can hold any number at all. A bare int32(n) turns 4294967296 into 0 and 5000000000 into 705032704, so a restore would scale a service to zero, or to some arbitrary capacity, and report success. Refusing is the only safe answer — the caller cannot guess what was meant, and this tool's whole promise is that the restore path is exact.
Reports false for a missing key, a non-numeric value, a negative count, or anything above math.MaxInt32.
Types ¶
type Action ¶
type Action struct {
Resource Resource
Phase Phase
// Op describes the change in the words a person would use, because it is
// printed in the confirmation prompt and read under pressure.
Op string
// Warning is printed in red and requires acknowledgement. Empty for the
// ordinary case.
Warning string
}
Action is one reversible change.
type BlastRadius ¶
type BlastRadius struct {
Total int
ByKind map[Kind]int
TouchesDB bool
DataLoss bool // an instance-store instance is in the plan
// SavingsUSD is the hourly total across resources that have an estimate.
SavingsUSD float64
// SavingsByKind breaks that down, so a plan dominated by one kind is
// visible rather than hidden inside a single number.
SavingsByKind map[Kind]float64
// UnpricedByKind counts resources with no defensible estimate. Reported
// separately and never as zero: a zero saving is a claim that stopping the
// thing is free, which is a different statement from not knowing.
UnpricedByKind map[Kind]int
}
BlastRadius is how much of the account this touches — the number a person checks before typing yes.
type Entry ¶
type Entry struct {
Kind Kind `json:"kind"`
ID string `json:"id"`
ARN string `json:"arn,omitempty"`
Name string `json:"name,omitempty"`
Region string `json:"region"`
Phase Phase `json:"phase"`
Prior map[string]any `json:"prior"`
// Result records what actually happened, so a partial fire can be restored
// without re-running against resources that were never changed.
Result string `json:"result,omitempty"`
Error string `json:"error,omitempty"`
}
Entry is one resource in a snapshot: what it was, and what it takes to put it back. This is the record a restore reads, so it holds the prior state rather than a reference to where the prior state might be found.
type Kind ¶
type Kind string
Kind is what sort of thing a resource is. It decides how it is stopped, how it is restored, and what phase it belongs to.
const ( KindALBListener Kind = "alb-listener" KindLambda Kind = "lambda" KindECSService Kind = "ecs-service" KindASG Kind = "asg" KindEC2Instance Kind = "ec2-instance" KindNATGateway Kind = "nat-gateway" KindRDSInstance Kind = "rds-instance" KindRDSCluster Kind = "rds-cluster" // KindEKSNodegroup, KindCloudFront and KindAPIGatewayStage are three // services discovery could not see, so a trip left them running. Each is // stopped by writing a number the service already has a field for, which // is why all three restore exactly: there is nothing to recreate. KindEKSNodegroup Kind = "eks-nodegroup" KindCloudFront Kind = "cloudfront-distribution" KindAPIGatewayStage Kind = "apigateway-stage" )
The resource kinds this tool acts on. Nothing outside this list is touched, and every entry here is stopped by a change that can be undone from the recorded prior state.
type Phase ¶
type Phase int
Phase is the order things happen in, and the order is not cosmetic.
Ingress goes first. Drain the compute while traffic is still arriving and a target-tracking scaling policy will fight the teardown, health checks will alarm, and whatever is driving the spend keeps driving it against a shrinking fleet. Cut the traffic and the rest goes quietly.
The phases, in the order they run. See Phase for why the order matters.
type Plan ¶
type Plan struct {
ID string
CreatedAt time.Time
Account string
Regions []string
Actions []Action
Refusals []Refusal
// AckRequired lists warnings that must be acknowledged before firing.
AckRequired []string
}
Plan is what will happen, in order, with everything that will not.
func (Plan) BlastRadius ¶
func (p Plan) BlastRadius() BlastRadius
BlastRadius totals what the plan would do. Savings and unpriced resources are counted separately: an unpriced resource is unknown, not free.
type Refusal ¶
Refusal is a resource the planner deliberately left alone. These are as important as the actions: a plan that silently omits things is a plan nobody can check, and "why is that still running" is the first question asked.
type Resource ¶
type Resource struct {
ID string
ARN string
Kind Kind
Name string
Region string
Tags map[string]string
// Prior is the state that must be restored, captured at discovery. Kept as
// a map so a new resource kind does not need a new struct threaded through
// the state file, and so an unrecognised field survives a version change
// rather than being silently dropped on restore.
Prior map[string]any
// HasInstanceStore marks an instance whose local NVMe is erased by a stop.
// EBS survives; instance store does not, and the API gives no warning.
HasInstanceStore bool
// EstimatedHourlyUSD is what stopping it saves, where it can be known
// cheaply. Zero means unknown, not free.
EstimatedHourlyUSD float64
}
Resource is one thing found in the account.
type Snapshot ¶
type Snapshot struct {
PlanID string `json:"plan_id"`
CreatedAt time.Time `json:"created_at"`
Account string `json:"account"`
Regions []string `json:"regions"`
Entries []Entry `json:"entries"`
Fired *time.Time `json:"fired_at,omitempty"`
Restored *time.Time `json:"restored_at,omitempty"`
}
Snapshot is what makes the whole thing safe: the exact prior state of every resource the plan will touch, written durably *before* anything is changed.
Without it, restore is someone guessing what the desired count used to be.