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 ¶
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 ¶
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 ¶
Config is one file, describing one or more apps.
func Load ¶
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.
type DNS ¶
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.
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.