Documentation
¶
Overview ¶
alerts.go — Driftfile reconcile of per-function alerts. The alert-spec name on the slice is `<function>-<index>` so multiple alerts on the same function get stable names; reconcile is idempotent (existing names are replaced wholesale; absent names are removed). v1: errors trigger only, webhook notify only — matches the slice's per-user-alerting primitive.
domains.go — Driftfile reconcile of `slice.domains[]`. Adds any host that's declared in the manifest but missing on the slice; removes any host that's live on the slice but absent from the manifest. After add, the user still has to flip DNS and run `drift slice domain verify <host>` — that step requires the CNAME / TXT to be live and is not something we can do automatically from the deploy path.
egress.go — Driftfile reconcile of the per-slice outbound egress allowlist. Compares `slice.atomic.egress` from the Driftfile against the live mode + declared hosts on the slice, and triggers a refresh if anything changed. The operator does the DNS resolution and pushes the rendered IP/port list into the slice's NetworkPolicy via charter — the CLI's only job is to decide whether a refresh is needed.
Reconcile semantics:
- Driftfile has no egress block (or `mode` is empty / "open"), and live is also open → no-op.
- Driftfile is open, live is allowlist → POST refresh; operator re-renders the chart with `mode: open` and the slice goes back to "any public host."
- Driftfile is allowlist, live is open OR list differs → POST refresh; operator re-resolves and pushes the new IP set.
- Driftfile is allowlist, live matches exactly → no-op.
env.go — the Driftfile variable origin hierarchy.
`$VAR` / `${VAR}` references in a Driftfile (and `$ENVREF` secret values) resolve from the process environment. We layer additional sources beneath the real environment so a deploy "just works" without a manual `source .env`, while never letting a convenience source override something set on purpose.
Precedence, highest first:
- Driftfile-hardcoded literals — a value written directly in the Driftfile. Absolute; never looked up. (Handled by the parser: a non-`$` value is kept verbatim.)
- Terminal environment — variables already exported in the shell session.
- `--secret KEY=value` / `--env <name>` override flags.
- The `.env` file sitting next to the Driftfile.
We realise tiers 2–4 by gap-filling the process environment in order: the real environment is left untouched, override flags fill only what it didn't set, and the `.env` file fills only what remains. The parser then reads `os.LookupEnv` and naturally sees the highest-precedence value.
sql.go — Driftfile reconcile of the SQL primitive. For each declared `sql:` entry the CLI uploads the schema + seed SQL files to the slice's admin endpoints. Idempotent — schemas are expected to be `CREATE … IF NOT EXISTS`, seeds are only applied when the database has no user tables yet (the slice handles this).
Removal: a database that's live on the slice but not in the Driftfile is dropped. The same shape as `applyDomains` / `applyAlerts` so the deploy chain reads consistently.
Index ¶
- func CountAtomicFunctions(m *Manifest) (int, error)
- func CountScheduledFunctions(m *Manifest) (int, error)
- func CreateSlice(name, tier string, cfg SliceConfig, billingMonths int) error
- func GetCmd() *cobra.Command
- func ParseProjectName(path string) (string, error)
- func RenderDiff(d DiffResult) string
- func ResizeSlice(name string, cfg SliceConfig, billingMonths int) error
- func SlugifyRoute(route string) string
- type AlertEntry
- type AtomicEntry
- type AtomicLimits
- type AtomicSection
- type BackboneBlobsLimits
- type BackboneLimits
- type BackboneLocksLimits
- type BackboneNoSQLLimits
- type BackboneQueuesLimits
- type BackboneRealtimeLimits
- type BackboneSQLLimits
- type BackboneSecretsLimits
- type BackboneSection
- type CacheEntry
- type CanvasEntry
- type CanvasLimits
- type CanvasSection
- type DiffResult
- type DomainEntry
- type EgressSection
- type FieldDelta
- type Hooks
- type LineItem
- type LiveSlice
- type Manifest
- type NoSQLEntry
- type ParseErrors
- type SQLEntry
- type Slice
- type SliceConfig
- type Tests
- type Verdict
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func CountAtomicFunctions ¶
CountAtomicFunctions returns the total number of `@atomic`-decorated callables the deploy will ship. It MIRRORS the deploy branch: when the Element layout is in play (a Default element or any multi-function element) it counts across discovered elements; otherwise it counts across the folders listed in the Driftfile (the legacy path, which honors custom `dir:` overrides). Keeping it in lockstep with applyAtomic is what stops a flat app from provisioning zero function slots.
An Atomic function IS a decorated callable; un-annotated helpers don't count.
func CountScheduledFunctions ¶ added in v1.6.0
CountScheduledFunctions returns how many `@atomic cron=` (scheduled) callables exist — the authoritative scheduled-job count used to size the slice. Like CountAtomicFunctions, it mirrors the deploy branch.
func CreateSlice ¶
func CreateSlice(name, tier string, cfg SliceConfig, billingMonths int) error
CreateSlice POSTs /ops/slice/create. tier is "hacker" for free slices (server overrides Config with HackerConfig) or "custom" for everything else.
func GetCmd ¶
GetCmd returns the `drift project` command group: deploy the project described by ./Driftfile to a slice, or preview the diff first.
Vocabulary the platform uses:
- Primitives — Atomic, Backbone, Canvas (the *what*).
- Slice — the rented infrastructure that holds primitives (the *where*).
- Project — a Driftfile bundling primitives + slice (the *what + where*, deployed as one unit).
func ParseProjectName ¶ added in v1.11.0
ParseProjectName cheaply decodes ONLY the top-level `name` — no validation, no `${VAR}`/`$ENVREF` resolution — so commands that just need the project's identity (e.g. `drift project stop`/`logs` finding the container) work without the project's secrets being set in the environment.
func RenderDiff ¶
func RenderDiff(d DiffResult) string
RenderDiff produces the user-facing block for `drift deploy --plan` and the cost-confirm prompt. Wording matches the spec's reconcile- rule examples exactly.
func ResizeSlice ¶
func ResizeSlice(name string, cfg SliceConfig, billingMonths int) error
ResizeSlice POSTs /ops/slice/resize with the new SliceConfig. The platform-side endpoint already enforces "won't shrink below current usage", so even with a destructive flag, populated resources can't disappear silently.
func SlugifyRoute ¶
SlugifyRoute derives the per-site directory name from a route. The slug is what the slice uses to lay sites out under /data/canvas/<slug>/.
"/" -> "default" "/reviewer" -> "reviewer" "/admin/portal" -> "admin-portal"
Types ¶
type AlertEntry ¶
type AlertEntry struct {
On string `yaml:"on"` // "errors" (v1)
Threshold int `yaml:"threshold"` // count of errors in the window
Window string `yaml:"window"` // duration string e.g. "5m"
Notify string `yaml:"notify"` // "webhook=https://..." (v1)
}
AlertEntry declares one alert on a function. `On` is the trigger (`errors` for v1). `Threshold` and `Window` together define when the alert fires (e.g. >=1 error over a 5-minute window). `Notify` is the destination — `webhook=https://hooks.slack.com/...` for v1.
type AtomicEntry ¶
type AtomicEntry struct {
Name string `yaml:"name"`
Dir string `yaml:"dir"`
Element string `yaml:"element"`
Cron string `yaml:"cron"`
// Alerts is the per-function alerting list. v1: `errors`
// trigger only; `webhook` notify only.
Alerts []AlertEntry `yaml:"alerts,omitempty"`
}
func (*AtomicEntry) UnmarshalYAML ¶
func (a *AtomicEntry) UnmarshalYAML(node *yaml.Node) error
UnmarshalYAML accepts either a bare-string (function name) or a map (the long form with name/dir/element/cron).
type AtomicLimits ¶
type AtomicSection ¶
type AtomicSection struct {
FunctionMemory string `yaml:"function_memory"`
FunctionTimeout string `yaml:"function_timeout"`
RateLimit string `yaml:"rate_limit"`
DeployHistory int `yaml:"deploy_history"` // past deploys kept per function (rollback)
Functions []AtomicEntry `yaml:"functions"`
// Egress declares the slice's outbound network posture.
// Schema-only today; richer enforcement modes are planned.
Egress *EgressSection `yaml:"egress,omitempty"`
}
type BackboneBlobsLimits ¶
type BackboneLimits ¶
type BackboneLimits struct {
Secrets BackboneSecretsLimits `json:"secrets"`
Blobs BackboneBlobsLimits `json:"blobs"`
NoSQL BackboneNoSQLLimits `json:"nosql"`
SQL BackboneSQLLimits `json:"sql"`
Queues BackboneQueuesLimits `json:"queues"`
Realtime BackboneRealtimeLimits `json:"realtime"`
Locks BackboneLocksLimits `json:"locks"`
// BackupRetentionDays needs its json tag — the platform model tags this
// leaf "backup_retention_days" (unlike the other Go-named leaves), so
// without it the value marshals as "BackupRetentionDays" and the server
// silently reads 0.
BackupRetentionDays int `json:"backup_retention_days"`
}
type BackboneLocksLimits ¶
type BackboneLocksLimits struct {
MaxConcurrent int
}
type BackboneNoSQLLimits ¶
type BackboneQueuesLimits ¶
type BackboneRealtimeLimits ¶ added in v1.6.0
type BackboneRealtimeLimits struct {
MaxConcurrentConnections int
}
type BackboneSQLLimits ¶ added in v1.6.0
type BackboneSecretsLimits ¶
type BackboneSection ¶
type BackboneSection struct {
NoSQLStorage string `yaml:"nosql_storage"`
SQLStorage string `yaml:"sql_storage"` // storage per SQL database (e.g. "100MB")
BlobStorage string `yaml:"blob_storage"` // total blob storage (the billing driver, e.g. "1GB")
BlobMaxSize string `yaml:"blob_max_size"`
BlobMaxCount int `yaml:"blob_max_count"` // free safety quota (not a price driver)
QueueMaxDepth int `yaml:"queue_max_depth"`
SecretMaxSize string `yaml:"secret_max_size"` // max size of one secret value (e.g. "4KB")
Locks int `yaml:"locks"` // max concurrent Backbone locks
// RealtimeConnections caps simultaneous live realtime WebSocket
// connections across the slice (the live pub/sub primitive). Billed in
// 50-connection blocks; 0 (omitted) means realtime is off for this slice.
RealtimeConnections int `yaml:"realtime_connections"`
NoSQL []NoSQLEntry `yaml:"nosql"`
Queues []string `yaml:"queues"`
Cache map[string]CacheEntry `yaml:"cache"`
Secrets map[string]string `yaml:"secrets"`
// SQL declares per-slice SQLite databases. Each entry becomes a
// `.db` file.
SQL []SQLEntry `yaml:"sql,omitempty"`
}
type CacheEntry ¶
type CacheEntry struct {
File string `yaml:"file"`
Value string `yaml:"value"`
TTL int `yaml:"ttl"`
}
CacheEntry is the long-form expansion. Short-form `<key>: <path>` expands to {File: <path>}. Short-form `{value: ...}` expands to {Value: <inline-value>}.
func (*CacheEntry) UnmarshalYAML ¶
func (c *CacheEntry) UnmarshalYAML(node *yaml.Node) error
UnmarshalYAML for cache map values accepts either a bare-string (file path) or a map (the long form with value/ttl).
type CanvasEntry ¶
func (*CanvasEntry) UnmarshalYAML ¶
func (c *CanvasEntry) UnmarshalYAML(node *yaml.Node) error
UnmarshalYAML accepts either a bare-string (canvas directory) or a map (the long form with dir/route).
type CanvasLimits ¶
type CanvasLimits struct {
TotalMaxSizeInBytes int
}
type CanvasSection ¶
type CanvasSection struct {
CanvasSize string `yaml:"canvas_size"`
Sites []CanvasEntry `yaml:"sites"`
}
type DiffResult ¶
type DiffResult struct {
Verdict Verdict
SliceName string
IsNewSlice bool
Grows []FieldDelta // fields the manifest wants larger than live
Shrinks []FieldDelta // fields the manifest wants smaller than live (only set on Abort)
LiveCostCents int // monthly cost of the live slice (0 if Create)
WantedCostCents int // monthly cost of the manifest's declared shape
WantedItems []LineItem // itemised breakdown backing WantedCostCents (server-computed)
}
DiffResult is the structured output of Diff(). Render it with RenderDiff to get the user-facing prompt block.
func Diff ¶
func Diff(sliceName string, manifest SliceConfig, liveCfg *SliceConfig, liveCostCents, wantedCostCents int) DiffResult
Diff compares the manifest-derived SliceConfig against the live SliceConfig and returns the verdict + per-field deltas. liveCfg must be a pointer; nil means "the slice doesn't exist yet" → Create. Callers that have the server's itemised price breakdown (PriceConfig's second return value) should set the result's WantedItems field themselves afterward — Diff doesn't take it directly so existing callers (and tests) that don't care about display don't need to change.
type DomainEntry ¶
type DomainEntry struct {
Host string `yaml:"host"`
Verify string `yaml:"verify"` // "dns-txt" (default for v1)
Wildcard bool `yaml:"wildcard,omitempty"` // route every subdomain of Host to this slice
}
DomainEntry declares one custom hostname for the slice. Verify is the ownership-proof method; "dns-txt" is the only mode for v1.
type EgressSection ¶
type EgressSection struct {
Mode string `yaml:"mode"` // "open" | "allowlist"
Hosts []string `yaml:"hosts,omitempty"` // e.g. "api.stripe.com", "*.amazonaws.com", "smtp.sendgrid.net:587"
}
EgressSection — declares whether the slice's outbound traffic to the public internet is open (today's default) or restricted to a curated list of hostnames. Private-CIDR exclusion (RFC-1918, link-local incl. IMDS, CGNAT) is preserved unconditionally regardless of mode.
type FieldDelta ¶
type FieldDelta struct {
Path string // human-readable path, e.g. "atomic.functions" or "backbone.nosql_storage"
Live int // current value on the live slice (0 if Create)
Wanted int // value the manifest declares
IsBytes bool // render as a size string instead of a bare integer
IsTime bool // render as a duration (seconds) — applies to function_timeout
IsHours bool // render as a duration (hours) — applies to log_retention
IsDays bool // render as a duration (days) — applies to backup_retention
Omittable bool // Wanted==0 means "the Driftfile didn't declare this knob", not "shrink it to zero"
}
FieldDelta records one resource/envelope dimension that changed.
func (FieldDelta) Delta ¶
func (f FieldDelta) Delta() int
Delta returns Wanted - Live; positive means grow, negative means shrink.
type Hooks ¶ added in v1.9.0
type Hooks struct {
PreDeploy []string `yaml:"pre_deploy,omitempty"`
PostDeploy []string `yaml:"post_deploy,omitempty"`
}
Hooks are shell commands the CLI runs locally around a deploy: pre_deploy before anything ships (typically a build/lint), post_deploy after the slice is live (typically a smoke test). Commands run in declaration order via the shell, from the project root; a non-zero exit aborts. Deliberately NOT a pipeline engine — no test stages, env matrices, parallelism, caching, or remote execution. Cross-environment orchestration is the user's CI calling `drift project deploy` more than once, never a Driftfile concern.
func ParseHooks ¶ added in v1.9.0
ParseHooks decodes ONLY the `hooks:` block, with no validation and no file-existence checks. The deploy command calls it BEFORE the full ParseDriftfile so a `pre_deploy` build can produce artifacts (e.g. a canvas/ dist directory) that the full parse then validates. Hook command strings are left verbatim — `${VAR}` in a command is expanded by the shell at run time (the deploy environment is already exported), not at the YAML layer. A missing/garbled `hooks:` block yields empty hooks, never an error, so a half-built project can still run its build step.
type LineItem ¶ added in v1.17.0
type LineItem struct {
Key string `json:"key"`
Label string `json:"label"`
Quantity int `json:"quantity"`
UnitCents int `json:"unit_cents"` // 0 means "included"
SubtotalCent int `json:"subtotal_cents"` // authoritative
}
LineItem mirrors the platform's core/common/plan.LineItem wire shape — one priced (or informational, UnitCents==0) row in a price breakdown.
func PriceConfig ¶
func PriceConfig(cfg SliceConfig) (int, []LineItem, error)
PriceConfig POSTs /ops/slice/price with a SliceConfig and returns the monthly cost in cents plus the itemised breakdown the server already computes (core/common/plan.PriceConfig) — the same data the browser configurator shows, just never threaded through the CLI's HTTP client until now. Used by both --plan and the cost-confirm prompt; the platform's pricing function is the single source of truth, never the CLI.
type LiveSlice ¶
type LiveSlice struct {
Name string `json:"name"`
Tier string `json:"tier"`
Config SliceConfig `json:"config"`
MonthlyCostCents int `json:"monthly_cost_cents"`
}
LiveSlice is a CLI-local mirror of the fields we actually use from the platform's models.Slice. The wire endpoint returns more fields (createdAt, billing, provisioning) but we only need name + config.
func FetchLiveSlice ¶
FetchLiveSlice GETs /ops/slice/get?name=<name>. Returns nil if the slice doesn't exist (404), error for any other failure.
type Manifest ¶
type Manifest struct {
// Slice is the base resource shape, inlined at the Driftfile root: name,
// retention knobs, and the atomic/backbone/canvas/domains sections. Each
// environment instantiates a slice from this shape plus its overrides.
Slice Slice `yaml:",inline"`
// Environments maps an environment name to a partial slice whose *set*
// fields override the base when that environment is selected. The bare-list
// form (`environments: [prod, staging]`) expands to a map of empty bodies.
// Empty/absent = a single-environment project deployed under its bare name.
Environments map[string]Slice `yaml:"environments,omitempty"`
// Hooks are local shell commands run around a deploy (see Hooks).
Hooks Hooks `yaml:"hooks,omitempty"`
// Tests are local shell commands `drift project test` runs against a
// `project run`-started local instance (see Tests).
Tests Tests `yaml:"tests,omitempty"`
// contains filtered or unexported fields
}
Manifest is the parsed Driftfile, after shorthand expansion. The project's resource shape is inlined at the top level (the file *is* the project); `environments` and `hooks` are optional siblings. Downstream code reads m.Slice exactly as before — the only change from v1 is where those keys live in the file, not the struct shape they decode into.
func ParseDriftfile ¶
ParseDriftfile reads a Driftfile from disk, expands shorthands, resolves $ENVREF secrets, and validates everything against the spec. baseDir is the directory containing the Driftfile and is used as the resolution root for relative paths.
func (*Manifest) ResolvePath ¶
ResolvePath is the exported sibling of resolveBaseDir, used by the run driver after parse to find files referenced by the manifest.
func (*Manifest) SelectEnvironment ¶ added in v1.9.0
SelectEnvironment resolves the deploy target for the chosen environment: it validates `env` against the declared environments, deep-merges that environment's overrides onto the base slice, and derives the slice name. It mutates the manifest in place (m.Slice becomes the effective slice) and returns the resolved environment name (empty for a single-environment project). `explicit` is true when the user named an environment (positional arg or --env), which makes "no environments declared" an error rather than a silent fall-through.
Resolution:
- No environments declared: a bare-name single slice. An explicit env is an error (nothing to select).
- env == "": default to `prod`/`production` if present, else error asking the user to pick one.
- env names a declared environment: merge + derive name.
Naming: `prod`/`production` (and the no-environments case) deploy under the bare project name; every other environment deploys under `<name>-<env>`.
type NoSQLEntry ¶
type NoSQLEntry struct {
Name string `yaml:"name"`
Seed string `yaml:"seed"` // path to JSONL
// TTL: how long a document lives after its LAST write before the
// platform deletes it — resets on every update. Duration string
// (durationRe: <int>[smhd]); empty = kept forever. Per-collection,
// not per-document.
TTL string `yaml:"ttl"`
}
func (*NoSQLEntry) UnmarshalYAML ¶
func (n *NoSQLEntry) UnmarshalYAML(node *yaml.Node) error
UnmarshalYAML accepts either a bare-string (collection name) or a map (the long form with name/seed).
type ParseErrors ¶
type ParseErrors []string
ParseErrors aggregates every validation failure in one error so the user sees them all at once. Implements `error` so it can flow through the cobra RunE return.
func (ParseErrors) Error ¶
func (p ParseErrors) Error() string
type SQLEntry ¶
type SQLEntry struct {
Name string `yaml:"name"`
Schema string `yaml:"schema,omitempty"`
Seed string `yaml:"seed,omitempty"`
}
SQLEntry declares one SQL database. `Schema` is a path to a SQL file with idempotent DDL (`CREATE TABLE IF NOT EXISTS`); it runs on every deploy. `Seed` is a path to a SQL file that runs only when the database has no user tables yet.
type Slice ¶
type Slice struct {
Name string `yaml:"name"`
LogRetention string `yaml:"log_retention"`
BackupRetention string `yaml:"backup_retention"`
Atomic AtomicSection `yaml:"atomic"`
Backbone BackboneSection `yaml:"backbone"`
Canvas CanvasSection `yaml:"canvas"`
// Domains lists per-slice custom hostnames the slice should answer
// on (e.g. forms.gemeente.example). Schema-only today; the
// reconcile path is planned.
Domains []DomainEntry `yaml:"domains"`
}
type SliceConfig ¶
type SliceConfig struct {
Canvas CanvasLimits `json:"canvas"`
Atomic AtomicLimits `json:"atomic"`
Backbone BackboneLimits `json:"backbone"`
}
SliceConfig mirrors the JSON wire shape of drift-common/models.SliceConfig. Field names match the platform's encoded shape exactly: parent fields live under lowercase JSON keys (canvas, atomic, backbone, secrets, blobs, nosql, queues, locks); leaf fields use the Go field names because the platform's models package omits json tags on those.
func ManifestToSliceConfig ¶
func ManifestToSliceConfig(m *Manifest) (SliceConfig, error)
ManifestToSliceConfig builds a SliceConfig from a parsed Driftfile. Unset envelope knobs leave the corresponding field at zero, which the platform reads as "use the slice envelope's default for this field" (the same convention the configurator uses today).
Returns a translation-error block if any envelope knob fails to parse despite passing the upstream Driftfile validation. That would be an internal bug, not user error, but we surface it rather than swallowing it.
type Tests ¶ added in v1.20.0
type Tests struct {
E2E []string `yaml:"e2e,omitempty"`
}
Tests are shell commands `drift project test` runs once the project is up locally (the same instance `drift project run` starts) — e2e/integration checks against a real running instance, before anything ships to Drift. Commands run in declaration order via the shell, from the project root; a non-zero exit fails the run. The instance's local URL rides in as DRIFT_TEST_URL so a test command knows where to point (the port is picked at runtime, never fixed). Deliberately NOT a pipeline engine — same posture as Hooks: no stages, matrices, parallelism, or remote execution.
func ParseTests ¶ added in v1.20.0
ParseTests decodes ONLY the `tests:` block, with no validation and no file-existence checks — same cheap-parse posture as ParseHooks, so `drift project test` can check "are any tests even declared" before paying for a full build.