Help us shape the future of Go! Take the 2026 Go Developer Survey

app

package
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Sep 27, 2026 License: MIT Imports: 36 Imported by: 0

Documentation

Overview

Package app is the engine's workload lifecycle: the layer that keeps the database and the cluster agreeing with each other.

The database is the source of truth for which apps exist; the cluster is the source of truth for how they are doing. Neither is asked the other's question, which is why listing apps never enumerates Deployments and status is never cached in a column that can go stale.

Index

Constants

View Source
const (
	BuildRunning   = "running"
	BuildSucceeded = "succeeded"
	BuildFailed    = "failed"
)

Build statuses.

View Source
const (
	OperationQueued    = "queued"
	OperationClaimed   = "claimed"
	OperationBuilding  = "building"
	OperationApplying  = "applying"
	OperationVerifying = "verifying"
	OperationSucceeded = "succeeded"
	OperationFailed    = "failed"
	OperationCancelled = "cancelled"
)
View Source
const (
	BaselinePending          = "pending"
	BaselineReady            = "ready"
	BaselinePendingImage     = "pending_image"
	BaselineNeverDeployed    = "never_deployed"
	BaselineImageUnavailable = "image_unavailable"
	BaselineBlocked          = "blocked"
)
View Source
const (
	DeployRunning    = "running"
	DeployActive     = "active"
	DeploySucceeded  = "succeeded"
	DeployFailed     = "failed"
	DeploySuperseded = "superseded"
)

Deployment statuses. Active and superseded remain readable for legacy rows; new attempts finish as succeeded or failed. Which release is serving comes only from apps.active_release_id.

View Source
const AppReconcileInterval = 2 * time.Second

AppReconcileInterval is short because a steady pass normally performs one read per app and no writes. The database convergence key, not this interval, decides whether Kubernetes is touched.

View Source
const DefaultBranch = "main"

DefaultBranch is what a repository is built from when nobody says.

A default rather than a required field: almost every repository uses it, and a form that refuses to submit without a value everyone would type the same way is a form asking a question it already knows the answer to.

View Source
const DefaultProjectName = "Default"

DefaultProjectName is what the default project is called on screen.

View Source
const DefaultProjectSlug = "default"

DefaultProjectSlug is the project apps land in when nobody has chosen one.

A team that never thinks about projects should never have to: there is always one, it is created on demand, and every app goes in it.

View Source
const PendingImage = "yacht.invalid/not-built-yet:pending"

PendingImage stands in until the first build produces one.

A real reference rather than an empty string, because the app row's image is not nullable and every read of it assumes something is there. This one is chosen to be obviously not a thing to run: if it ever reaches a cluster, the pull fails with the name on the screen rather than starting something.

View Source
const PushTriggerPrefix = "push:"

PushTriggerPrefix starts the trigger, and revision, of a push deployment.

View Source
const ReconcileInterval = time.Minute

ReconcileInterval is how often builds are settled against the cluster.

A minute rather than seconds: this exists to catch builds whose process went away, which is a rare event, and polling hard for it would cost a query per second forever to shorten a wait nobody is sitting through.

View Source
const RollbackTrigger = "rollback"

RollbackTrigger is the deployment trigger, and revision, a rollback records.

Variables

View Source
var (
	// ErrHookUnauthorized covers an unknown hook and a signature that does not
	// match alike. Telling the two apart would tell a caller which app ids
	// have a hook without their knowing its secret.
	ErrHookUnauthorized = errors.New("app: this push is not signed with the app's webhook secret")

	// ErrNotGitApp means the app has no repository for a push to be about.
	ErrNotGitApp = errors.New("app: only an app built from a repository can deploy on push")
)

Hook refusals the web layer turns into status codes.

View Source
var (
	// ErrReleaseActive means the release asked for is the one already running.
	ErrReleaseActive = errors.New("app: that release is the one running now")

	// ErrReleaseImageGone means the registry no longer has the release's image.
	// Nothing is changed: an operation that could only fail at the image pull
	// would leave the app's settings rolled back and its workload not.
	ErrReleaseImageGone = errors.New(
		"app: the image this release ran is no longer in the registry, so it cannot be restored")
)

Rollback refusals. Each is something a person can act on, so each is its own error rather than one "cannot roll back" with the reason in a string.

View Source
var ErrClusterObservation = errors.New("app: cluster observation unavailable")
View Source
var ErrNameTaken = errors.New("app: name already in use")

ErrNameTaken is returned when an owner already has an app with that name.

View Source
var ErrNoBaselineRelease = errors.New("app: no baseline release yet")

ErrNoBaselineRelease means a legacy app has no immutable runtime truth yet. Falling back to mutable desired fields would make the active pointer a lie.

View Source
var ErrNoBuilder = errors.New(
	"app: this install cannot build from a repository — no image registry is configured")

ErrNoBuilder means this install cannot build from source.

Its own error rather than a generic failure, because it is a configuration answer and not a fault: an install with no registry has nowhere to push, and telling somebody that is different from telling them their build broke.

View Source
var ErrNoManifestResolver = errors.New(
	"app: image manifests cannot be resolved on this install")

ErrNoManifestResolver means this install cannot turn a tag into an immutable image identity. Nothing may be applied in this state: doing so would create a release whose meaning can change underneath it.

View Source
var ErrNoSecretKey = errors.New(
	"app: no YACHT_SECRET_KEY is configured, so a secret cannot be stored safely")

ErrNoSecretKey means a secret was asked for with no key configured to seal it.

Refusing is the point. Storing it readable instead would give the person the protection they asked for in name only, and they would have no way to tell.

View Source
var ErrNotFound = errors.New("app: not found")

ErrNotFound is returned when an app does not exist for the given owner.

View Source
var ErrOperationClaimLost = errors.New("app: deployment operation claim was lost")
View Source
var ErrOperationInFlight = errors.New("app: a deployment operation is already in flight")

ErrOperationInFlight is the durable per-app admission decision. Callers may report it, but must not work around it with an application-level pre-check.

View Source
var ErrOperationRecoveryPending = errors.New("app: operation stage awaits reconciliation")

ErrOperationRecoveryPending is deliberately non-terminal. Build and app reconcilers observe cluster truth after a process stops inside a stage; the durable checkpoint lets them do that without guessing or rebuilding.

View Source
var ErrProjectNotFound = errors.New("app: no such project")

ErrProjectNotFound means the team has no project with that slug.

View Source
var ErrRepoUnreachable = errors.New("app: the repository could not be read")

ErrRepoUnreachable means the remote did not answer, or answered by refusing.

Its own error because the page treats it as "cannot help right now" rather than as the repository being wrong: a private repository, a typo and an outage all land here, and only one of them is worth a red field.

View Source
var ErrTemplateNotFound = errors.New("app: no such template")

ErrTemplateNotFound means no template goes by that slug.

View Source
var ErrTreeUnsupported = fmt.Errorf("app: only GitHub repositories can be browsed")

ErrTreeUnsupported means the host is not one whose tree can be read.

View Source
var ErrVariableNotFound = errors.New("app: no such variable")

ErrVariableNotFound means the app has no variable with that key.

View Source
var ErrVolumeAttached = errors.New("app: the volume is still attached to the app")

ErrVolumeAttached means the volume is still mounted by its app.

View Source
var ErrVolumeNotFound = errors.New("app: no such volume")

ErrVolumeNotFound means the app has no such volume.

View Source
var ErrVolumeShrink = errors.New("app: a volume can grow but never shrink")

ErrVolumeShrink means a resize asked for less than the volume already has.

Kubernetes cannot shrink a claim, and nothing else can either: the filesystem on it may be full. Refusing is the honest answer, and refusing before anything reaches the cluster keeps the database and the cluster agreeing.

Functions

func Namespace

func Namespace(ownerID, name string) string

Namespace derives a workload's namespace from its owner and name.

A fixed-width hash rather than a slug of the name. Slugging then truncating to fit Kubernetes' 63-character limit is how two differently-named apps end up sharing a namespace and deleting each other — the truncation silently removes the part that made them unique. A hash has no such failure mode, and the readable name lives in a label instead.

func Slugify

func Slugify(name string) string

Slugify turns a display name into an addressable slug.

func ValidateStackName

func ValidateStackName(stack string) error

ValidateStackName checks the prefix every app in a stack is named with.

Stricter than an app name by exactly one character: a stack name becomes "<stack>-<app>", so it has to leave room and it cannot end in the dash that joins them.

Types

type Activity

type Activity struct {
	Deployment
	AppName      string
	AppNamespace string
}

Activity is a deployment with enough context to render outside an app page.

type App

type App struct {
	ID        uuid.UUID
	OwnerID   string
	Name      string
	Namespace string
	Image     string
	Replicas  int32
	Port      int32

	// Source is what this app was created from, and Internal keeps it off the
	// public internet.
	Source   Source
	Internal bool

	// Repo is where the source comes from, for an app built rather than
	// pulled. Zero on an image-sourced app.
	Repo Repo

	// RunAsUser is the numeric uid a built image runs as, discovered by the
	// build. Zero means unknown, which is every app not built from source.
	RunAsUser int64

	// ConfigVersion changes whenever editable runtime state or a live overlay
	// changes. ActiveReleaseID is the sole database answer to what is serving;
	// deployment status records only how an attempt ended.
	ConfigVersion   int64
	ActiveReleaseID *uuid.UUID
	BaselineState   string
	BaselineError   string

	// HealthPath is an HTTP path reporting whether the app is serving. Empty
	// means no probe. Liveness lets the same path also restart the container.
	HealthPath string
	Liveness   bool

	// Variables are this app's environment. A secret's value is not carried
	// here — see Variable.
	Variables []Variable

	CPURequest    string
	CPULimit      string
	MemoryRequest string
	MemoryLimit   string

	CreatedAt time.Time
	UpdatedAt time.Time

	// ProjectID is the canvas this app belongs to.
	ProjectID uuid.UUID

	// X and Y are where somebody dragged the card to, nil when nobody has.
	// Nil rather than zero because (0,0) is a position a person can choose,
	// and an app pinned to the corner must not be re-laid-out on every render.
	X, Y *int32

	// Status is read live from the cluster. Zero value means the cluster did
	// not answer — which is different from "not running" and is rendered
	// differently.
	Status      orchestrator.AppStatus
	StatusKnown bool

	// LastDeploy is how the most recent deployment ended.
	//
	// A different question from Status, and the one a list of apps could not
	// answer: a failed deploy leaves the previous workload running, so the app
	// stays green while the change somebody made never took. Empty on an app
	// that has never been deployed.
	LastDeploy string

	// Host is the platform-issued hostname, empty when no app domain is
	// configured. Read from the domains table rather than stored on the app,
	// so there is one place a hostname lives.
	Host string

	// Volumes is the storage attached to this app, read alongside it.
	Volumes []Volume

	// HTTPSOnly and CNAMEOnly are the app's routing choices, written as
	// annotations on its Ingress.
	HTTPSOnly bool
	CNAMEOnly bool

	// TLS reports whether the platform serves Host over TLS. Populated on
	// read from configuration rather than stored, because it is a property of
	// the install and not of the app.
	TLS bool
}

App is a workload as the engine sees it: the stored record plus whatever the cluster currently reports.

func (App) Ref

func (a App) Ref() orchestrator.Ref

Ref converts an App to an orchestrator reference.

func (App) URLScheme

func (a App) URLScheme() string

URLScheme is the scheme an app can actually be reached on.

It follows HTTPSOnly rather than TLS, and the difference is the whole point. TLS says whether the platform has a certificate anybody will trust; HTTPSOnly decides which of the ingress controller's entrypoints serves this app at all. An app that is HTTPS-only on an install with no certificate is still only reachable over https — it simply presents the controller's default certificate when it gets there.

Reading TLS here is what produced a dashboard offering http:// links to apps that answer nothing on port 80, which is the exact failure this method exists to prevent.

func (App) UntrustedCert

func (a App) UntrustedCert() bool

UntrustedCert reports that this app is served over https with a certificate nobody will trust.

Worth saying rather than leaving to a browser warning: the warning names the certificate, not the reason, and the reason is that this install has no wildcard certificate configured.

type Blueprint

type Blueprint struct {
	Source Source

	// Label and Description are what the picker shows.
	Label       string
	Description string

	// Available reports whether this source can be deployed at all.
	// Unavailable is listed with its reason rather than hidden: a menu that
	// silently omits things reads as a product that does not have them.
	Available bool
	Because   string

	Image string
	Port  int32

	// Internal keeps the workload off the public internet. A database speaks
	// its own protocol on its own port, so an HTTP hostname pointed at it
	// would be a route to something that cannot answer.
	Internal bool

	// RunAsUser, FSGroup and ScratchPaths carry what the image needs in order
	// to run under a restricted security context. They are facts about a
	// specific image, which is exactly why only a source that names the image
	// can supply them.
	RunAsUser    int64
	FSGroup      int64
	ScratchPaths []string

	// Volume is storage created with the app rather than attached afterwards.
	// A database with no data directory is not a database somebody has to
	// finish configuring; it is one that has not been deployed.
	Volume *VolumeInput

	// Env and generated secrets. GeneratedSecrets name variables whose values
	// are minted at create time and never shown — a password nobody chose is
	// one nobody can have reused elsewhere.
	Env              map[string]string
	GeneratedSecrets []string

	// ConnectionTemplate builds the in-cluster address other apps use, with
	// %s for the generated password.
	ConnectionTemplate string
	ConnectionKey      string
}

Blueprint is everything a source decides on the app's behalf.

The point of the type is that a source is data rather than a branch: adding one is filling this in, and every part of the system that consumes a blueprint keeps working without knowing the new source exists.

func BlueprintFor

func BlueprintFor(src Source) (Blueprint, error)

BlueprintFor returns the blueprint for a source.

A template has no blueprint. It is not a thing an app can be created from — it creates apps — so asking for one is a wiring mistake rather than a state to handle, and it is refused here where every create path passes through.

func BlueprintForWith

func BlueprintForWith(src Source, c Capabilities) (Blueprint, error)

BlueprintForWith returns the blueprint for a source on this install.

func Blueprints

func Blueprints() []Blueprint

Blueprints returns every source with nothing optional available.

Kept for callers that only need the list — the labels, the descriptions, the order. Anything deciding whether a source can be used wants BlueprintsFor.

func BlueprintsFor

func BlueprintsFor(c Capabilities) []Blueprint

BlueprintsFor returns every source, in the order a picker should show them.

Unavailable ones are included. What a product does not do yet is worth stating plainly in the place somebody looks for it, rather than leaving them to conclude it was never considered.

type Build

type Build struct {
	ID           uuid.UUID
	AppID        uuid.UUID
	DeploymentID uuid.UUID

	RepoURL   string
	RepoRef   string
	CommitSHA string
	Image     string

	Status  string
	Message string
	Log     string

	StartedAt  time.Time
	FinishedAt *time.Time
}

Build is a record of one.

func (Build) Running

func (b Build) Running() bool

Running reports whether this build is still going.

type Builder

type Builder = orchestrator.Builder

Builder turns a repository into an image.

A seam for the reason the orchestrator is one: what runs the build is a Kubernetes Job here and could be something else, and nothing above this line should have to know which. It is also what lets the rest of the pipeline be tested without a cluster.

The request and result types live in the orchestrator package rather than here, so a Kubernetes implementation can satisfy this without importing the service that calls it.

type Capabilities

type Capabilities struct {
	// CanBuild reports whether there is an image registry to push to. Without
	// one a build has nowhere to put the result and the cluster nothing to
	// pull, so the source is listed and refused rather than offered.
	CanBuild bool
}

Capabilities is what this install can actually do.

Passed in rather than discovered here, because whether a source works is a fact about how this install is configured and this file is a description of what the sources are. The zero value claims nothing, which is the right default for a caller that has not asked: a picker that offers building on an install with nowhere to push produces a create that fails at the end.

type CreateInput

type CreateInput struct {
	// Source decides what the app is. Empty means an image the person chose,
	// which is what every app was before sources existed.
	Source Source

	// ProjectID is the canvas this app is drawn on. Zero leaves it unassigned,
	// and the next read of the team's projects adopts it into the default one —
	// which is what a create that never mentioned a project wants.
	ProjectID uuid.UUID

	// Repo is where to build from, for SourceGit. Ignored by every other
	// source: an image nobody builds has no repository to name.
	Repo Repo

	Name     string
	Image    string
	Replicas int32
	Port     int32
	Env      map[string]string

	CPURequest    string
	CPULimit      string
	MemoryRequest string
	MemoryLimit   string
}

CreateInput describes a new workload.

func (CreateInput) Validate

func (in CreateInput) Validate() error

Validate checks the input before anything is written or applied.

type DeployActivity

type DeployActivity struct {
	// Days is one entry per day in the window, oldest first, including days
	// with no deploys.
	Days []DeployDay

	// Totals over the whole window, so the panel can say what it is showing
	// without the reader adding up bars.
	Succeeded int
	Failed    int
	Cancelled int
	Running   int
}

DeployActivity is deploys per day over a window, for the overview chart.

func (DeployActivity) Total

func (a DeployActivity) Total() int

Total is every deploy in the window.

type DeployDay

type DeployDay struct {
	Day time.Time

	Succeeded int
	Failed    int
	Cancelled int

	// Running counts deploys still in flight, which includes pending. They are
	// shown because a day whose deploys are all still running looks identical
	// to a day with no deploys otherwise, and those mean opposite things.
	Running int
}

DeployDay is one day's deploys, split by how they ended.

func (DeployDay) Total

func (d DeployDay) Total() int

Total is every deploy started that day.

type DeployLogs

type DeployLogs struct {
	Deployment Deployment
	Logs       Logs

	// Live is true when this deployment is the one currently running, which is
	// the only case where there are containers left to read.
	Live bool
}

DeployLogs is what one deployment's log view can honestly show.

type Deployment

type Deployment struct {
	ID         uuid.UUID
	AppID      uuid.UUID
	ReleaseID  *uuid.UUID
	Image      string
	Revision   string
	Trigger    string
	ActorKind  string
	ActorID    string
	IsActive   bool
	Status     string
	Message    string
	StartedAt  time.Time
	FinishedAt *time.Time
}

Deployment is one recorded attempt to run a version of an app.

func (Deployment) Source

func (d Deployment) Source() string

Source describes where a deployment was triggered from.

Only "image" exists today. It is a method rather than a stored column because the answer is derived from the revision, and a column would need a migration the first time a build pipeline is added.

type HTTPBucket

type HTTPBucket struct {
	At time.Time `json:"at"`

	// To is where this column ends. Sent rather than inferred, because a bar
	// on a time axis has to be told how wide it is: without an interval the
	// chart spreads whatever bars exist across the whole width, so two busy
	// minutes drew as two bars half a chart wide each.
	To time.Time `json:"to"`

	OK   int `json:"ok"`
	Warn int `json:"warn"`
	Err  int `json:"err"`
}

HTTPBucket is one column of the request timeline.

Counted by status class rather than kept as raw requests, because the chart answers "when did this app get traffic, and when did it go wrong" — and a thousand identical 200s answer that no better than a number does.

type HTTPLogs

type HTTPLogs struct {
	Lines []orchestrator.HTTPLogLine

	// Hosts are the names these requests could have arrived on, shown so an
	// empty result can be told from one for an app with no hostname at all.
	Hosts []string

	// Note explains an empty result, and Hint is the configuration that would
	// fix it when the answer is a setting somebody else has to apply.
	Note string
	Hint string
}

HTTPLogs is what the ingress controller recorded for one app.

func (HTTPLogs) Buckets

func (h HTTPLogs) Buckets() []HTTPBucket

Buckets groups requests into a timeline.

The range comes from the requests themselves rather than a fixed window, because the controller's log is a moving window of unknown span: asking for the last hour when the log only reaches back four minutes draws fifty-six empty columns and squeezes everything real into the last four.

type Hook added in v0.2.0

type Hook struct {
	Enabled   bool
	CreatedAt time.Time

	// LastDelivery is what the last push did, in a sentence, and when.
	LastDelivery   string
	LastDeliveryAt time.Time

	// Pending is a push waiting for the deploy in flight to end.
	Pending bool
}

Hook is what the settings page shows about an app's webhook. The secret is not in it: it is shown once, when it is made, and never read back to a page.

type Images

type Images interface {
	// ImageFor returns the full reference a build should push to.
	ImageFor(ctx context.Context, ownerID, app, revision string) (string, error)

	// Configured reports whether there is a registry at all, and Insecure
	// whether it speaks plain HTTP.
	Configured(ctx context.Context) bool
	Insecure(ctx context.Context) bool

	// DockerConfig is the credential, in the form a builder and a kubelet
	// both read. Fetched per use rather than held, so it is unsealed only
	// when something is about to push or pull.
	DockerConfig(ctx context.Context) ([]byte, error)
}

Images says where a build's result goes.

Narrower than the whole registry: the app service needs to name an image and to know whether naming one is possible, and nothing else about the credential.

type InstallDomain

type InstallDomain struct {
	domain.Custom
	App string
}

InstallDomain is one custom domain, with the app it belongs to.

type Link struct {
	From string
	To   string
	Via  string
}

Link is one app's dependency on another, as the graph needs it.

type LogRequest

type LogRequest struct {
	// Pod is optional. Empty reads the app's first pod, which is what somebody
	// looking at a single-replica service wants and is the common case.
	Pod string

	// Previous reads the container that died rather than the one running.
	Previous bool

	Tail int64
}

LogRequest asks for one app's container output.

type Logs

type Logs struct {
	Pod      string
	Pods     []string
	Lines    []orchestrator.LogLine
	Previous bool

	// Note explains an empty result that is not a failure — a pod that has
	// never restarted has no previous container, and saying so beats an empty
	// pane that looks like a bug.
	Note string
}

Logs is an app's container output, and which pods it could have come from.

type ManifestAvailability

type ManifestAvailability interface {
	CheckDigest(ctx context.Context, ref string) error
}

ManifestAvailability asks whether an immutable reference remains pullable. It is separate from resolution because resolving a digest is intentionally a no-I/O pass-through, while rollback must make an authoritative preflight.

type Manifests

type Manifests interface {
	// ResolveDigest returns the digest behind a reference, or an error that
	// says which kind of no it is. A reference that already names a digest
	// comes back unchanged.
	//
	// Callers must distinguish registry.ErrManifestNotFound from every other
	// failure: an absent image will not appear by waiting and the attempt has
	// to fail, while a registry that could not be asked has said nothing about
	// whether the image is there.
	ResolveDigest(ctx context.Context, ref string) (string, error)
}

Manifests answers what digest is behind an image reference.

Alongside Images rather than part of it, because the two are asked at different moments by different things: Images is where a build's result goes, and this is what that result turned out to be. Something that only needs to name an image should not have to be able to interrogate a registry.

A tag can be pushed over and a digest cannot, so this is what lets a deployment record an image identity that stays the same image. It is also the recovery path: a build whose push succeeded before Yacht stopped left its answer in the registry, and this is how to go and read it.

type Networking

type Networking struct {
	// Managed is the platform-issued hostname, empty when the feature is off
	// or the app cannot have one.
	Managed string

	Custom []domain.Custom

	HTTPSOnly bool
	CNAMEOnly bool

	// Target is what a custom domain's CNAME must point at. Empty when the
	// install has not been told, which is the state the page has to explain
	// rather than show an input that cannot work.
	Target string

	// Issuing is true when the install gives custom domains certificates of
	// their own. False means a custom domain is served over plain HTTP, and
	// the page has to say so rather than promise a certificate.
	Issuing bool

	// Certs is what the cluster holds for each routed custom domain, by host.
	// A host that is missing could not be read, which is not the same as not
	// issued and must not be drawn as a failure.
	Certs map[string]orchestrator.Certificate

	// Namespace is where the app's objects live, so a command the page offers
	// for looking into one can be pasted and run as it is.
	Namespace string
}

Networking is an app's routing: the hostnames it answers on and how.

type Operation

type Operation struct {
	ID             uuid.UUID
	OwnerID        string
	AppID          uuid.UUID
	DeploymentID   uuid.UUID
	ReleaseID      *uuid.UUID
	RequiresBuild  bool
	Status         string
	Message        string
	CreatedAt      time.Time
	ClaimedAt      *time.Time
	FinishedAt     *time.Time
	ClaimToken     *uuid.UUID
	LeaseExpiresAt *time.Time
	CancelledAt    *time.Time
	Checkpoint     string
	StageStartedAt time.Time
}

Operation is the durable, fenced stage record behind one deployment attempt. Checkpoint remains at the last entered stage when a lease is reclaimed, so a later reconciler can observe cluster truth rather than repeat earlier work.

type Options

type Options struct {
	// Keeper seals secret variables. Left nil, secrets are refused rather than
	// stored readable — a protection somebody asked for and did not get is
	// worse than one they were told they could not have.
	Keeper *secret.Keeper

	// AppDomain is the platform domain apps get hostnames under. Empty
	// switches per-app hostnames off.
	AppDomain string

	// WildcardTLS serves those hostnames over TLS from the ingress
	// controller's default certificate.
	WildcardTLS bool

	// CertIssuer is the cluster-wide certificate issuer custom domains are
	// given certificates from. Empty means the install has none, and a custom
	// domain is served over plain HTTP.
	CertIssuer string

	// ReservedDomains are hostnames no app may claim, even under the app
	// domain. An app named "admin" would otherwise take admin.<app domain>
	// simply by being created first.
	ReservedDomains []string

	// Resolver proves a custom domain points here. Left nil, verification is
	// refused with a reason rather than silently failing.
	Resolver domain.Resolver

	// Builder turns a repository into an image, and Images says where that
	// image goes. Either left nil, building from a repository is listed as
	// unavailable with the reason rather than offered and failed.
	Builder Builder
	Images  Images

	// Manifests pins a mutable image reference before it can become a release.
	// Independent of Builder: image-sourced apps need it even on an install
	// that cannot build repositories itself.
	Manifests Manifests

	// MaxConcurrentBuilds bounds claimed build operations across the install.
	// Zero takes the production default so tests and embedders stay safe.
	MaxConcurrentBuilds int32

	// RolloutTimeout bounds verification after Kubernetes accepts an apply.
	// RolloutPollInterval exists so tests can exercise slow and failed rollouts
	// without sleeping on production intervals.
	RolloutTimeout      time.Duration
	RolloutPollInterval time.Duration
}

Options are the settings the service needs beyond its dependencies.

type Project

type Project struct {
	ID      uuid.UUID
	OwnerID string
	Slug    string
	Name    string

	// Apps is how many apps it holds. Populated by List, because a list of
	// projects that does not say how big each one is gives no reason to pick.
	Apps int64
}

Project groups the apps drawn on one canvas.

type Push added in v0.2.0

type Push struct {
	// Event is the host's own name for it: X-GitHub-Event, X-Gitea-Event or
	// X-Gitlab-Event. Empty when none was sent.
	Event string

	// Signature is X-Hub-Signature-256 (GitHub, Gitea, Forgejo, Bitbucket
	// Server) or X-Gitea-Signature: an HMAC-SHA256 of the body.
	Signature string

	// Token is X-Gitlab-Token, which GitLab sends as the secret itself.
	Token string

	Body []byte
}

Push is one delivery, as the web layer received it.

type Release

type Release struct {
	ID      uuid.UUID
	OwnerID string
	AppID   uuid.UUID

	ImageRef       string
	ImageDigest    string
	Source         Source
	SourceRevision string

	Replicas int32
	Port     int32

	CPURequest    string
	CPULimit      string
	MemoryRequest string
	MemoryLimit   string
	Internal      bool

	RunAsUser              int64
	FSGroup                int64
	ScratchPaths           []string
	WritableRootFilesystem bool

	HealthPath string
	Liveness   bool

	Env        map[string]string
	SecretKeys []string

	ConfigVersion int64
	CreatedAt     time.Time
	Origin        string
}

Release is the immutable, non-secret runtime state behind one deployment. Mutable overlays are deliberately absent; ReleaseOverlays supplies the current values when this becomes an AppSpec.

func (Release) AppSpec

func (r Release) AppSpec(overlays ReleaseOverlays) orchestrator.AppSpec

AppSpec reconstructs the exact orchestrator input from a release and the current overlays. The image is pinned even though ImageRef retains the tag a person recognizes in history.

type ReleaseOverlays

type ReleaseOverlays struct {
	Ref           orchestrator.Ref
	ConfigVersion int64
	RegistryAuth  []byte
	Secrets       map[string]string
	Volumes       []orchestrator.VolumeSpec
	Hosts         []string
	TLSHosts      []string
	IssuedHosts   []string
	CertIssuer    string
	HTTPSOnly     bool
	CNAMETarget   string
}

ReleaseOverlays are the fields intentionally kept current across rollback. They are named in one type so every AppSpec field belongs visibly either to immutable release history or to current external/security state.

type Repo

type Repo struct {
	// URL is an https:// or ssh:// Git URL.
	URL string

	// Branch is the ref built. Empty means DefaultBranch.
	Branch string

	// Subdir is the directory inside the repository to build from, for a
	// monorepo. Empty is the root.
	Subdir string
}

Repo is where an app's source comes from.

A value type rather than three columns threaded through every signature, because the three only ever travel together and only ever mean anything together: a subdirectory with no URL is not a partial configuration, it is a mistake.

func (Repo) Normalise

func (r Repo) Normalise() Repo

Normalise trims a repository the way it will be stored.

func (Repo) Ref

func (r Repo) Ref() string

Ref is the branch actually used.

func (Repo) Set

func (r Repo) Set() bool

Set reports whether this app builds from source.

func (Repo) Validate

func (r Repo) Validate() error

Validate checks a repository somebody typed.

type Service

type Service struct {
	// contains filtered or unexported fields
}

Service manages app lifecycle.

func NewService

func NewService(
	pool *pgxpool.Pool, orch orchestrator.Orchestrator, log *slog.Logger, opts Options,
) *Service

NewService wires the store and the orchestrator together.

func (*Service) AddDomain

func (s *Service) AddDomain(ctx context.Context, ownerID, name, host string) error

AddDomain claims a hostname for an app.

Nothing routes to it until it is verified, so this deliberately does not reapply the workload: an unproven claim changes no Ingress, and reapplying would suggest otherwise.

func (*Service) AllDomains

func (s *Service) AllDomains(ctx context.Context, ownerID string) ([]InstallDomain, error)

AllDomains lists every custom domain on the install, unsettled first.

Nothing answered "which domains are stuck" before this. A domain lives on one app's page, so an operator watching several had to visit each in turn and remember what they saw — which is how a domain that quietly drifted stayed unnoticed until somebody complained.

func (*Service) ApplyRouting

func (s *Service) ApplyRouting(ctx context.Context, ownerID string, appID uuid.UUID) error

ApplyRouting records that one app's routing projection changed.

This is what the background domain checker calls when a claim becomes provable, or when a live one stops resolving. It takes an owner and an app id rather than a name because the checker works from domain rows, which carry both — and every table carrying owner_id is precisely so that scoping stays a cheap predicate rather than something a background job gets to skip.

func (*Service) AttachVolume

func (s *Service) AttachVolume(
	ctx context.Context, ownerID, appName string, in VolumeInput,
) (Volume, error)

AttachVolume creates storage for an app and applies it.

func (*Service) BackfillReleases

func (s *Service) BackfillReleases(ctx context.Context) error

BackfillReleases resolves and records one bounded batch of legacy apps. It performs no cluster calls and is safe to run repeatedly or from several replicas: the app row is locked and the release/pointer/state commit is one transaction.

func (*Service) Branches

func (s *Service) Branches(ctx context.Context, repoURL, query string) ([]string, error)

Branches lists a repository's branches, most useful first.

query filters by substring, which is what a picker does as somebody types. Anonymous over HTTPS: that covers public repositories, and a private one fails as ErrRepoUnreachable rather than prompting for a credential this install has nowhere to keep.

func (*Service) BuildForDeployment

func (s *Service) BuildForDeployment(
	ctx context.Context, ownerID string, deployID uuid.UUID,
) (Build, error)

BuildForDeployment returns the build behind one deployment, if there was one.

func (*Service) CanBuild

func (s *Service) CanBuild(ctx context.Context) bool

CanBuild reports whether this install can build from a repository.

func (*Service) CancelLiveDeployment

func (s *Service) CancelLiveDeployment(ctx context.Context, ownerID, name string) error

CancelLiveDeployment cancels whatever operation is currently holding an app.

Named rather than identified, because the caller wanting to stop a deploy has the app in hand and not an operation id — and handing the web layer a way to read operation ids only so it can pass one straight back is a wider surface than the thing it wants to do.

ErrNotFound means nothing was in flight, which is also what the second click on a page that has gone stale produces.

func (*Service) CancelOperation

func (s *Service) CancelOperation(ctx context.Context, ownerID string, id uuid.UUID) error

CancelOperation makes cancellation terminal before any best-effort cluster cleanup. Clearing the claim token in the same transaction is what prevents a worker already past its last checkpoint from publishing success afterward.

func (*Service) Capabilities

func (s *Service) Capabilities(ctx context.Context) Capabilities

Capabilities is what this install can do, for a picker to render honestly.

func (*Service) ClaimOperation

func (s *Service) ClaimOperation(ctx context.Context) (Operation, error)

ClaimOperation selects one globally admissible operation. The advisory lock and count live in the same short transaction, which is what makes the build ceiling hold across independent Yacht processes rather than only goroutines.

func (*Service) ClearPositions

func (s *Service) ClearPositions(ctx context.Context, ownerID string, projectID uuid.UUID) error

ClearPositions forgets a project's arrangement so it lays out again.

func (*Service) Count

func (s *Service) Count(ctx context.Context, ownerID string) (int64, error)

Count returns how many apps an owner has.

func (*Service) Create

func (s *Service) Create(ctx context.Context, ownerID string, in CreateInput) (App, error)

Create stores the app, its first immutable release, and a deployment operation in one transaction. The cluster is touched only after that commit, through the same durable stage machine as every later deployment.

func (*Service) CreateProject

func (s *Service) CreateProject(ctx context.Context, ownerID, name string) (Project, error)

CreateProject makes a project for a team.

func (*Service) DefaultBranch

func (s *Service) DefaultBranch(ctx context.Context, repoURL string) (string, error)

DefaultBranch is what the remote itself points HEAD at.

Better than assuming "main": a repository that predates the rename still says master, and picking wrong sends somebody to a build that clones an empty ref.

func (*Service) DefaultProject

func (s *Service) DefaultProject(ctx context.Context, ownerID string) (Project, error)

DefaultProject returns the team's default project, creating it if needed and adopting any app that has no project.

Adoption is what makes the column nullable safe: an app created before projects existed, or one whose project was deleted, is not lost — it is simply waiting for the next read to place it.

func (*Service) Delete

func (s *Service) Delete(ctx context.Context, ownerID, name string) error

Delete removes the workload from the cluster and then the record.

Cluster first: if the record went first and the cluster call failed, the workload would keep running with nothing left to describe it.

func (*Service) DeleteVariable

func (s *Service) DeleteVariable(ctx context.Context, ownerID, appName, key string) error

DeleteVariable removes a variable and applies the app.

func (*Service) DeleteVolume

func (s *Service) DeleteVolume(
	ctx context.Context, ownerID, appName, volumeName string, detach bool,
) error

DeleteVolume removes storage from an app.

Refused while the workload still mounts it unless detach is set, because deleting storage is not something to do as a side effect of tidying up. With detach, the desired attachment is removed and the reconciler converges the workload without it. The persistent claim itself is never deleted here.

func (*Service) DeliverPush added in v0.2.0

func (s *Service) DeliverPush(ctx context.Context, appID uuid.UUID, p Push) (string, error)

DeliverPush acts on one delivery and returns what it did, in a sentence the Git host shows beside the delivery and the settings page shows as the last one. The error is for a delivery that was refused; one that was accepted and deliberately ignored — another branch, a tag — is not an error.

func (*Service) DeployActivity

func (s *Service) DeployActivity(ctx context.Context, ownerID string, days int) (DeployActivity, error)

DeployActivity counts an owner's deploys per day over the last n days.

Every day in the window is returned, including the ones with no deploys. Postgres only reports days that have rows, and drawing those directly would put six columns across a month and read as steady daily deploys — the axis would be describing the data's shape rather than the calendar's.

func (*Service) DeployTemplate

func (s *Service) DeployTemplate(
	ctx context.Context, ownerID, slug, stack string,
) (Project, error)

DeployTemplate creates a project and the stack's apps inside it.

Not a single transaction, and deliberately so. Each app is created by the same Create that a person uses, which applies to the cluster as it goes — a transaction spanning all of them would either hold one open across several cluster calls or roll back database rows for workloads that already exist. A stack that fails halfway leaves what it made, on a canvas that shows exactly which parts arrived.

func (*Service) Deployment

func (s *Service) Deployment(
	ctx context.Context, ownerID, appName string, deployID uuid.UUID,
) (Deployment, error)

Deployment returns one of an app's deployments.

Separate from DeploymentLogs because the sheet has views that show no container output at all and still need the deployment's own detail for their heading. Reading the cluster to fill a pane that discards the result would cost an API call per tab and, worse, make a log fetch happen where the page says none did.

func (*Service) DeploymentHTTPLogs

func (s *Service) DeploymentHTTPLogs(
	ctx context.Context, ownerID, appName string,
) (HTTPLogs, error)

DeploymentHTTPLogs reads the requests made to an app.

The app is resolved by owner first and the hostnames come from that app's own rows, which is the entire tenancy boundary: one ingress controller logs every tenant's traffic into one stream, so what makes this one app's log is that the filter was built from an owner-scoped query and nothing else.

An app with no hostname gets nothing, not everything. That is the failure worth naming, because an empty filter over a shared log is the difference between this feature and a way to read the whole cluster's traffic.

func (*Service) DeploymentLogs

func (s *Service) DeploymentLogs(
	ctx context.Context, ownerID, appName string, deployID uuid.UUID, req LogRequest,
) (DeployLogs, error)

DeploymentLogs reads the output of one deployment.

Only the running deployment has any. Kubernetes keeps a container's log with the container, so the pods that served a superseded deployment took their output with them when they were replaced — retaining it would need a log store shipped off the cluster, which this install does not have.

Saying that is the point. Reading the current pods and captioning them with an old deployment's revision would look like history and be the live log, which is worse than an empty pane: somebody would draw conclusions about a deploy from output that postdates it.

func (*Service) Deployments

func (s *Service) Deployments(
	ctx context.Context, ownerID string, appID uuid.UUID, limit int32,
) ([]Deployment, error)

Deployments returns an app's deployment history, newest first.

func (*Service) Directories

func (s *Service) Directories(ctx context.Context, repoURL, path string) ([]string, error)

Directories lists the directories at a path inside a repository.

path is relative to the repository root and empty means the root itself, matching what the build does with SUBDIR. Returns only directories: a file cannot be a root directory, and offering one would be offering a build that fails on the first step.

func (*Service) DisableGitHook added in v0.2.0

func (s *Service) DisableGitHook(ctx context.Context, ownerID, name string) error

DisableGitHook removes an app's webhook. A Git host still calling it is refused from then on, the same as one that never had the secret.

func (*Service) EnableGitHook added in v0.2.0

func (s *Service) EnableGitHook(ctx context.Context, ownerID, name string) (string, error)

EnableGitHook gives an app a webhook, or a new secret for the one it has, and returns the secret. It is the only time the secret is available in the clear: the page shows it once, to be pasted into the Git host.

func (*Service) EnsureHTTPLogs

func (s *Service) EnsureHTTPLogs(ctx context.Context)

EnsureHTTPLogs turns the ingress controller's access log on at startup.

Request logging is not a feature somebody switches on. An app whose requests are not being recorded is an app nobody can debug, and a platform that made that the default and then put the fix behind a button was asking every user to discover, understand, and accept a cluster-wide restart before they could see their own traffic. So Yacht does it once, here, on its own.

It is still cluster-wide — there is one controller and one log, and Traefik has no per-router or per-host way to switch access logging off — which is exactly why this is not reached from an app's page. Nothing about it is one app's decision, so nothing about it is presented as one.

Idempotent, and quiet when there is nothing to do: a controller already writing an access log is left alone, so this costs a restart once per install rather than once per boot.

func (*Service) EnsureOwner

func (s *Service) EnsureOwner(ctx context.Context, id, displayName, email string) error

EnsureOwner makes sure the owner row exists, so app inserts have a parent.

func (*Service) Get

func (s *Service) Get(ctx context.Context, ownerID, name string) (App, error)

Get returns one app by name, with live status.

func (*Service) GitHook added in v0.2.0

func (s *Service) GitHook(ctx context.Context, ownerID, name string) (Hook, error)

GitHook reports an app's webhook.

func (s *Service) Links(ctx context.Context, ownerID string) ([]Link, error)

Links returns every recorded dependency between an owner's apps.

func (*Service) List

func (s *Service) List(ctx context.Context, ownerID string) ([]App, error)

List returns an owner's apps with live status attached.

Carries how the last deployment ended as well as how the workload is doing. They are different questions and only one of them was being asked: a failed deploy leaves the previous workload running, so the app stays green while the change somebody made never took.

func (*Service) ListInProject

func (s *Service) ListInProject(ctx context.Context, ownerID string, projectID uuid.UUID) ([]App, error)

ListInProject returns a project's apps, with cluster status, as List does.

func (*Service) LogStream

func (s *Service) LogStream(
	ctx context.Context, ownerID, appName string, req LogRequest,
) (iter.Seq2[orchestrator.LogLine, error], error)

LogStream follows an app's container output until ctx is done.

Scoped exactly as Logs is, through the same resolver. Previous runs are not offered: a container that has already died writes nothing more, so following it would hold a connection open on a stream that can never produce a line.

func (*Service) Logs

func (s *Service) Logs(ctx context.Context, ownerID, appName string, req LogRequest) (Logs, error)

Logs reads an app's container output.

The app is resolved by owner first and the namespace comes from that row, so the only pods reachable are the ones belonging to an app the caller owns. A pod name is enough to read any tenant's output, so it is never trusted from the request: the name is checked against the pods this app actually has.

func (*Service) Networking

func (s *Service) Networking(ctx context.Context, ownerID, name string) (Networking, error)

Networking returns an app's routing.

func (*Service) Project

func (s *Service) Project(ctx context.Context, ownerID, slug string) (Project, error)

Project returns one project by slug.

func (*Service) ProjectByID

func (s *Service) ProjectByID(ctx context.Context, ownerID string, id uuid.UUID) (Project, error)

ProjectByID returns one project by id, for a caller holding an app.

func (*Service) Projects

func (s *Service) Projects(ctx context.Context, ownerID string) ([]Project, error)

Projects lists a team's projects.

The default project is created here when the team has none, so the page that lists projects is never empty for a team that has apps. Doing it on read is what lets a team that predates projects open the page and see their work, rather than an empty list and no way to explain it.

func (*Service) RecentActivity

func (s *Service) RecentActivity(
	ctx context.Context, ownerID string, limit int32,
) ([]Activity, error)

RecentActivity returns an owner's most recent deployments across every app.

func (*Service) ReconcileApps

func (s *Service) ReconcileApps(ctx context.Context) error

ReconcileApps performs one deterministic convergence pass. It is exported so tests and repair tooling can run one pass without starting a background clock.

func (*Service) ReconcileBuilds

func (s *Service) ReconcileBuilds(ctx context.Context) error

ReconcileBuilds settles builds that claim to be running.

Level-triggered, against the cluster, which is the only arrangement that works. A build is driven by a goroutine, and a goroutine is not a source of truth: it does not survive a restart, and it never existed on the other replicas. The Job does, and every replica can read it — so "is this still running" is a lookup rather than a guess about a process.

Safe to run concurrently with the goroutine that started the build, and on several replicas at once. Everything here is idempotent and decides from the cluster's answer, so two reconcilers reaching the same conclusion write the same thing.

func (*Service) Redeploy

func (s *Service) Redeploy(ctx context.Context, ownerID, name string) error

Redeploy reapplies the current spec, which restarts the workload's pods.

func (*Service) ReleaseByID

func (s *Service) ReleaseByID(
	ctx context.Context, ownerID string, appID, releaseID uuid.UUID,
) (Release, error)

ReleaseByID returns one owner-scoped snapshot.

func (*Service) Releases

func (s *Service) Releases(
	ctx context.Context, ownerID string, appID uuid.UUID, limit int32,
) ([]Release, error)

Releases returns immutable history newest first.

func (*Service) RemoveDomain

func (s *Service) RemoveDomain(ctx context.Context, ownerID, name string, id uuid.UUID) error

RemoveDomain releases a claim and stops routing it.

func (*Service) RequestDomainCheck

func (s *Service) RequestDomainCheck(ctx context.Context, ownerID string, id uuid.UUID) error

RequestDomainCheck brings a domain's next background check forward.

func (*Service) ResizeVolume

func (s *Service) ResizeVolume(
	ctx context.Context, ownerID, appName, volumeName string, sizeBytes int64,
) error

ResizeVolume grows a volume. It cannot shrink one.

func (*Service) ResolverName

func (s *Service) ResolverName() string

ResolverName says which resolver answers questions about custom domains.

Surfaced because "this name does not resolve" is one resolver's opinion, and somebody comparing it against their own dig output needs to know whether the two asked the same server.

func (*Service) Rollback

func (s *Service) Rollback(ctx context.Context, ownerID, name string, releaseID uuid.UUID) error

Rollback puts an app back on one of its earlier releases.

Two things happen, in one transaction: the app's release-owned settings — image, replicas, port, limits, health check and plain variables — become the release's again, and a deployment of exactly that release is admitted. The first is what makes a rollback stay rolled back. Were only the workload changed, the next scale or variable edit would build a release from the newer settings still on the app and quietly roll forward.

Current state is kept, because a release never held it: hostnames, HTTPS, storage, the repository and every secret stay as they are now. A secret is deliberately not reverted even where the release predates it — the value was never in the release, and deleting it would not restore whatever it replaced.

The image is checked before anything is written. A release whose image the registry has since dropped cannot be restored, and saying so here is better than rolling the app's settings back behind a deployment that fails to pull.

func (*Service) RunAppReconciler

func (s *Service) RunAppReconciler(ctx context.Context)

RunAppReconciler restores missing/drifted active workloads and resumes the post-build stages whose worker disappeared.

func (*Service) RunOperationAdmission

func (s *Service) RunOperationAdmission(ctx context.Context)

RunOperationAdmission reclaims expired ownership and promotes durable queued work while this process is alive. The claim token fences any worker that returns after another process has reclaimed its lease.

func (*Service) RunReconciler

func (s *Service) RunReconciler(ctx context.Context)

RunReconciler settles builds until the context is cancelled.

Runs once immediately, because the case this exists for is a restart, and waiting a full interval to notice would leave every interrupted build claiming to run for that long after the process came back.

func (*Service) RunReleaseBackfill

func (s *Service) RunReleaseBackfill(ctx context.Context)

RunReleaseBackfill retries registry failures without holding up startup. Transient failures retry quickly; missing images and configuration failures remain visible and retry on the slower schedule stored with the cohort row.

func (*Service) Scale

func (s *Service) Scale(ctx context.Context, ownerID, name string, replicas int32) (App, error)

Scale changes the replica count and reapplies.

func (*Service) SetHealth

func (s *Service) SetHealth(
	ctx context.Context, ownerID, name, healthPath string, liveness bool,
) error

SetHealth points a probe at a path, or removes it.

Validated through AppSpec before anything is written: the same rules the cluster would apply, applied where somebody can read the reason.

func (*Service) SetNetworking

func (s *Service) SetNetworking(ctx context.Context, ownerID, name string, httpsOnly, cnameOnly bool) error

SetNetworking records the routing choices. The app reconciler owns the cluster write so request handlers cannot race a candidate deployment.

func (*Service) SetPosition

func (s *Service) SetPosition(ctx context.Context, ownerID, name string, x, y int32) error

SetPosition records where somebody dragged an app's card to.

Positions are the team's, not the person's: a canvas two people arrange differently is two pictures of one system, and neither can be pointed at in a conversation.

func (*Service) SetVariable

func (s *Service) SetVariable(
	ctx context.Context, ownerID, appName string, in VariableInput,
) error

SetVariable stores a variable and applies the app.

func (*Service) Sources

func (s *Service) Sources(ctx context.Context) []Blueprint

Sources lists what an app can be created from on this install.

func (*Service) TargetStatus

func (s *Service) TargetStatus(ctx context.Context, target string) string

TargetStatus reports whether the install's CNAME target itself resolves.

ValidateCNAMETarget checks the shape of the string and nothing else, so a hostname that is well-formed and does not exist is accepted — and then every custom domain on the install fails to verify, reporting the failure against the customer's name rather than against the setting that caused it.

A sentence rather than a bool because the answer is shown, and "could not be checked" is a third state that must not read as "broken".

func (*Service) Update

func (s *Service) Update(ctx context.Context, ownerID, name string, in UpdateInput) (App, error)

Update changes an app and rolls it out.

The image of a Git app is refused rather than accepted and overwritten: it is whatever its last build produced, and the next build will replace anything typed here. Silently discarding somebody's input is worse than saying no.

func (*Service) VerifyDomain

func (s *Service) VerifyDomain(ctx context.Context, ownerID, name string, id uuid.UUID) error

VerifyDomain proves a claim now, rather than waiting for the checker.

The background checker will reach this domain on its own, so this exists for the person standing in front of the page who has just created the record and does not want to wait out the backoff. It runs the same Check the checker runs. The app reconciler observes that version and is the only component allowed to write the Ingress.

type Source

type Source string

Source is what an app was created from.

Stored on the app rather than inferred, because it decides things that cannot be worked out from the image later: whether a hostname is issued, which uid the container runs as, and where its data lives.

const (
	// SourceImage is a container image the person chose. Yacht knows nothing
	// about what is inside it, so it configures nothing on its behalf.
	SourceImage Source = "image"

	// SourceTemplate is a stack rather than a single app. It never reaches an
	// app row: the template page creates each app with that app's own source.
	SourceTemplate Source = "template"

	// SourceGit is a repository Yacht builds into an image and then deploys.
	// The image on the app row is what the last build produced, so it is set
	// by the build rather than chosen.
	SourceGit Source = "git"

	// SourcePostgres is a Postgres database. Yacht knows what the image is, so
	// it can mount the data directory, run as the right user, and generate the
	// credentials — none of which a person should have to look up.
	SourcePostgres Source = "postgres"
)

type Template

type Template struct {
	Slug        string
	Label       string
	Description string

	// Apps are deployed in order, because a later one may take a variable from
	// an earlier one. Order is the dependency; there is no graph to resolve
	// because a stack that needed one would be past what this is for.
	Apps []TemplateApp
}

Template is a stack: several apps deployed together and already wired up.

Data rather than code, for the reason Blueprint is: adding one is filling this in, and nothing that consumes a template needs to know the new one exists. A template composes blueprints rather than restating them, so a change to how Postgres is deployed reaches every template that includes it.

func TemplateFor

func TemplateFor(slug string) (Template, error)

TemplateFor returns one template by slug.

func Templates

func Templates() []Template

Templates returns every stack, in the order a picker should show them.

type TemplateApp

type TemplateApp struct {
	// Name is the suffix. The app is called <stack>-<name>, so deploying the
	// same template twice does not collide and every name says what it belongs
	// to.
	Name   string
	Source Source

	// Image and Port apply only to a plain image; a source that names its own
	// supplies both.
	Image string
	Port  int32

	Env map[string]string

	// NeedsFrom names an earlier app in the stack whose connection variable
	// this one should receive. The variable is copied under the providing
	// blueprint's own ConnectionKey, so the consumer sees exactly the name that
	// blueprint documents.
	NeedsFrom string
}

TemplateApp is one app in a stack.

type UpdateInput

type UpdateInput struct {
	Image string
	Port  int32

	CPURequest    string
	CPULimit      string
	MemoryRequest string
	MemoryLimit   string

	// Internal withdraws the platform hostname rather than merely hiding it.
	Internal bool

	// Repo is where a Git app builds from. Changing it does not rebuild —
	// the next deploy uses it — so it is the one field here that does not
	// roll the workload.
	Repo Repo
}

UpdateInput is what a person may change about an app after creating it.

These had no way to be changed at all. An app's image, its port, what it is allowed to consume, whether it is reachable, and where it builds from were all decided once at create and then frozen — so changing an image tag meant deleting the app and starting over, taking its domains and its history with it.

Not replicas: scaling has its own path because it has its own rule about storage. Not the health probe or the routing toggles: those already have forms shaped to what they mean.

func (UpdateInput) Validate

func (in UpdateInput) Validate(source Source) error

Validate checks an update against the same bounds a create is held to.

Written against the same limits as CreateInput.Validate rather than sharing one function, because the two disagree on purpose: an update has no name to check, and an empty image on a Git app is not missing — it is the build's to fill in.

type Variable

type Variable struct {
	Key    string
	Value  string
	Secret bool
}

Variable is one environment entry.

Value is empty for a secret. Nothing that reads a list of variables — a page, a log line, an error — can print one by accident, because the type it is handed does not carry it.

type VariableInput

type VariableInput struct {
	Key    string
	Value  string
	Secret bool
}

VariableInput sets or replaces a variable.

func (VariableInput) Validate

func (in VariableInput) Validate() error

Validate checks the input before anything is written.

type Volume

type Volume struct {
	ID        uuid.UUID
	AppID     uuid.UUID
	Name      string
	MountPath string
	SizeBytes int64
	Class     string
}

Volume is storage attached to an app.

type VolumeInput

type VolumeInput struct {
	Name      string
	MountPath string
	SizeBytes int64
	Class     string
}

VolumeInput describes storage to attach.

func (VolumeInput) Validate

func (in VolumeInput) Validate() error

Validate checks the input before anything is written or applied.

The orchestrator validates too, and deliberately: this is the message a person reads, that one is the invariant the cluster is protected by. Neither is redundant — the first can be improved without weakening the second.

Jump to

Keyboard shortcuts

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