config

package
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Aug 10, 2026 License: MIT Imports: 6 Imported by: 0

Documentation

Overview

Package config is the declared shape of an app's cloud footprint.

One file describes every surface an app occupies — the Render service and its environment, the R2 bucket that holds its uploads, the Pages project that serves its frontend, the Neon database behind it — and upkeep's job is to make the cloud match it, or say plainly which part it cannot.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func LooksSecret

func LooksSecret(key string) bool

LooksSecret is exported because the importer must agree with this exactly: if it emitted a literal for a key validation refuses, it would write a file the tool cannot read back.

func Validate

func Validate(cfg Config) error

Validate rejects a config that would produce a confusing failure later.

Everything here is a mistake that is cheap to make and expensive to diagnose from the other end: a variable with no source reaches Render as an empty string, a CORS rule with no origin blocks every browser, an app with no surfaces produces an empty plan that looks like success.

Types

type App

type App struct {
	Name    string   `yaml:"name"`
	Render  *Render  `yaml:"render"`
	R2      *R2      `yaml:"r2"`
	Pages   *Pages   `yaml:"pages"`
	Neon    *Neon    `yaml:"neon"`
	DNS     *DNS     `yaml:"dns"`
	Auth    *Auth    `yaml:"auth"`
	Fly     *Fly     `yaml:"fly"`
	Workers *Workers `yaml:"workers"`
}

App is everything one deployed product occupies.

type Auth

type Auth struct {
	// FrontendProvider is what the frontend was BUILT with — it is baked into
	// the bundle and cannot be read back from anywhere, so declaring it is the
	// only way to check the two halves agree. One of local, neon, oidc,
	// supabase, fake.
	FrontendProvider string `yaml:"frontendProvider"`
	// FrontendAuthURL is where the browser sends its session calls. Probed,
	// because the usual failure is a URL that lost its path and now 404s —
	// which a client reads as "no session".
	FrontendAuthURL string `yaml:"frontendAuthUrl"`
	// SiteOrigins must each appear in the service's CORS allowlist. The match
	// is exact, so a hostname that is nearly right is entirely wrong.
	SiteOrigins []string `yaml:"siteOrigins"`
}

Auth is the one surface that checks variables against each other rather than against a provider. Authentication fails when two values disagree, not when one is missing, and nothing else notices.

upkeep never changes any of it: a wrong automatic fix here locks out everybody, including whoever ran the tool.

type CORSRule

type CORSRule struct {
	Origins []string `yaml:"origins"`
	Methods []string `yaml:"methods"`
	// Headers must list every header the client sends. For a presigned upload
	// that means every header in the signature — a signed header the preflight
	// refuses never leaves the page.
	Headers       []string `yaml:"headers"`
	ExposeHeaders []string `yaml:"exposeHeaders"`
	MaxAgeSeconds int      `yaml:"maxAgeSeconds"`
}

CORSRule mirrors R2's own shape rather than S3's, because that is what the API accepts and a translation layer is one more thing to get wrong.

type Config

type Config struct {
	Version int   `yaml:"version"`
	Apps    []App `yaml:"apps"`
}

Config is one file, describing one or more apps.

func Load

func Load(path string, getenv func(string) string) (Config, error)

Load reads a config file, expands ${VAR} references, and validates it.

A path of "-" reads standard input, so `import` and `plan` compose:

upkeep import -name app -render srv-… | upkeep plan -config -

getenv is injected so tests do not touch the process environment. Nil means os.Getenv.

func LoadFrom

func LoadFrom(path string, stdin io.Reader, getenv func(string) string) (Config, error)

LoadFrom is Load with the standard input it should read for "-".

func Parse

func Parse(raw []byte, getenv func(string) string) (Config, error)

Parse is Load without the filesystem.

type DNS

type DNS struct {
	ZoneID  string      `yaml:"zoneId"`
	Records []DNSRecord `yaml:"records"`
}

DNS is the handful of records a website needs in order to exist. Not a zone manager: records upkeep does not declare are reported and never removed, because a DNS record is the one thing here that can take a site off the internet by being deleted.

type DNSRecord

type DNSRecord struct {
	Type    string `yaml:"type"`
	Name    string `yaml:"name"`
	Content string `yaml:"content"`
	// TTL in seconds. Zero means Cloudflare's automatic.
	TTL int `yaml:"ttl"`
	// Proxied routes through Cloudflare. Unset leaves the zone's own choice
	// alone rather than asserting one.
	Proxied *bool `yaml:"proxied"`
}

type EnvVar

type EnvVar struct {
	Key string `yaml:"key"`
	// Value is a literal, for things that are not secret.
	Value string `yaml:"value"`
	// ValueEnv names a local environment variable holding the value.
	ValueEnv string `yaml:"valueEnv"`
	// ValueFrom names a reference in a secret manager, e.g.
	// op://vault/item/field. For a team, where the value should not be
	// exported into anyone's shell in the first place.
	ValueFrom string `yaml:"valueFrom"`
	// Manual marks a value nobody can generate: real bank details, a
	// credential only a dashboard can mint. upkeep reports it missing and
	// never invents one.
	Manual bool `yaml:"manual"`
	// Why explains a manual variable to whoever reads the plan. It is the
	// difference between "something is missing" and "here is what to do".
	Why string `yaml:"why"`
}

EnvVar is one variable. Exactly one of Value or ValueEnv is set.

The split is the point. A literal is checked into the repo because it is not a secret — an endpoint, a bucket name, a public hostname. A secret names the local environment variable it comes from, so the value passes through memory and never appears in the config, the plan, or a terminal.

func (EnvVar) Secret

func (e EnvVar) Secret() bool

Secret reports whether this variable's value must never be printed.

type Fly added in v0.2.0

type Fly struct {
	App string `yaml:"app"`
	// Secrets uses the same shape as a Render variable, so a value that must
	// not be committed names its source rather than carrying it.
	Secrets []EnvVar `yaml:"secrets"`
}

Fly is an app on Fly.io — the second place a backend can run, and the test of whether the provider seam is a seam.

It manages secrets rather than environment variables, and the difference is not cosmetic: Fly never returns a secret's value, only its name and a digest. upkeep can therefore report a secret as missing and set one, but cannot tell you an existing one is wrong. That limit is the provider's, and the plans say only what was actually checked.

type Neon

type Neon struct {
	ProjectID string `yaml:"projectId"`
	Branch    string `yaml:"branch"`
	// DatabaseURLEnv names the local variable holding the connection string,
	// when the Render service's DATABASE_URL should match it.
	DatabaseURLEnv string `yaml:"databaseUrlEnv"`
}

Neon is the database. upkeep reads it and reports drift; it does not create or drop projects, because a database is not a thing to converge silently.

type Pages

type Pages struct {
	AccountID string `yaml:"accountId"`
	Project   string `yaml:"project"`
	// ProductionBranch decides which deployment is the real one. It matters
	// more than it looks: a preview URL is a different origin, and an exact
	// CORS or allowlist match refuses it.
	ProductionBranch string `yaml:"productionBranch"`
	// Domains is every hostname that should serve this project. upkeep
	// reports ones it does not know about rather than deleting them.
	Domains []string `yaml:"domains"`
}

Pages is the frontend project.

type R2

type R2 struct {
	AccountID string `yaml:"accountId"`
	Bucket    string `yaml:"bucket"`
	// Public serves objects on the bucket's r2.dev hostname.
	Public bool `yaml:"public"`
	// CORS is what a browser is allowed to do directly against the bucket.
	// Empty means upkeep leaves whatever is there alone.
	CORS []CORSRule `yaml:"cors"`
}

R2 is a bucket and the two settings that decide whether a browser can use it.

type Render

type Render struct {
	ServiceID string `yaml:"serviceId"`
	// Env is the service's environment. Absent keys are left alone unless the
	// run is pruning — a service carries variables the platform put there.
	Env []EnvVar `yaml:"env"`
	// Image, when set, is what a deploy ships. Empty means upkeep never
	// triggers one and only reconciles the environment.
	Image string `yaml:"image"`
}

Render is the backend service: which one, and what its environment must be.

type Workers added in v0.2.0

type Workers struct {
	AccountID string `yaml:"accountId"`
	Script    string `yaml:"script"`
	// ZoneID is required only when routes are declared.
	ZoneID string `yaml:"zoneId"`
	// Secrets uses the same shape as everywhere else, so a value that must not
	// be committed names its source rather than carrying it. Cloudflare returns
	// a secret's NAME and never its value, so upkeep reports one missing and
	// says nothing about one that exists.
	Secrets []EnvVar `yaml:"secrets"`
	// Routes the script should answer. Routes upkeep did not declare are
	// reported and never removed: a live route has traffic behind it.
	Routes []string `yaml:"routes"`
}

Workers is a Cloudflare Worker: the two things about one that break quietly.

A secret the script expects and the account does not have — the Worker deploys, serves most requests, and one code path throws. And a route it should answer on and does not, so traffic reaches the zone's normal handling and the site "works" with one path belonging to someone else.

The script itself is not managed. Uploading a Worker is `wrangler deploy`, which does bundling, source maps and migrations; reimplementing that to own one more field would be a worse tool, not a more complete one.

Jump to

Keyboard shortcuts

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