project

package
v1.23.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 21, 2026 License: MIT Imports: 22 Imported by: 0

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:

  1. Driftfile-hardcoded literals — a value written directly in the Driftfile. Absolute; never looked up. (Handled by the parser: a non-`$` value is kept verbatim.)
  2. Terminal environment — variables already exported in the shell session.
  3. `--secret KEY=value` / `--env <name>` override flags.
  4. 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

Constants

This section is empty.

Variables

This section is empty.

Functions

func CountAtomicFunctions

func CountAtomicFunctions(m *Manifest) (int, error)

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

func CountScheduledFunctions(m *Manifest) (int, error)

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

func GetCmd() *cobra.Command

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

func ParseProjectName(path string) (string, error)

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

func SlugifyRoute(route string) string

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 AtomicLimits struct {
	MaxNumberOfFunctions            int
	MaxFunctionRuntimeInSeconds     int
	MaxNumberOfDeploymentsInHistory int
	MaxNumberOfHoursForLogRetention int
	MaxNumberOfRequestsPerMinute    int
	MaxNumberOfScheduledJobs        int
	MaxFunctionMemoryBytes          int
}

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 BackboneBlobsLimits struct {
	MaxCount           int
	MaxSizeInBytesEach int
	MaxStorageBytes    int // total blob storage (the billing driver)
}

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 BackboneNoSQLLimits struct {
	MaxCollections  int
	MaxStorageBytes int
}

type BackboneQueuesLimits

type BackboneQueuesLimits struct {
	MaxQueues    int
	MaxDepthEach int
}

type BackboneRealtimeLimits added in v1.6.0

type BackboneRealtimeLimits struct {
	MaxConcurrentConnections int
}

type BackboneSQLLimits added in v1.6.0

type BackboneSQLLimits struct {
	MaxDatabases    int
	MaxStorageBytes int
}

type BackboneSecretsLimits

type BackboneSecretsLimits struct {
	MaxCount           int
	MaxSizeInBytesEach int
}

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

type CanvasEntry struct {
	Dir   string `yaml:"dir"`
	Route string `yaml:"route"`
}

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

func ParseHooks(path string) (Hooks, error)

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

func FetchLiveSlice(name string) (*LiveSlice, error)

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

func ParseDriftfile(path string) (*Manifest, error)

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

func (m *Manifest) ResolvePath(rel string) string

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

func (m *Manifest) SelectEnvironment(env string, explicit bool) (string, error)

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.

func (*SQLEntry) UnmarshalYAML added in v1.6.0

func (s *SQLEntry) UnmarshalYAML(node *yaml.Node) error

UnmarshalYAML accepts either a bare-string (database name → empty database) or a map (the long form with name/schema/seed). This mirrors nosql so `sql: [ledger]` and the long form both work.

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

func ParseTests(path string) (Tests, error)

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.

type Verdict

type Verdict int

Verdict is the four-way classifier described above.

const (
	VerdictCreate Verdict = iota
	VerdictMatch
	VerdictGrow
	VerdictAbort
)

func (Verdict) String

func (v Verdict) String() string

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL