orchestrator

package
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Sep 26, 2026 License: MIT Imports: 13 Imported by: 0

Documentation

Overview

Package orchestrator defines the contract between Yacht's control plane and whatever actually runs workloads.

SEAM 1 of 4. This interface is the primary extension point of the engine. The engine ships a single-cluster Kubernetes implementation (subpackage k8s) and a no-op implementation for tests. A wrapping application can supply its own — for example one that selects a cluster from a registry and applies per-owner scheduling — without the engine knowing anything about it.

Two rules keep this seam usable:

  1. No Kubernetes types appear in this package. Everything crossing the boundary is a plain Go type defined here. An implementation backed by something other than Kubernetes stays possible, and callers never import client-go.
  2. Naming policy lives with the caller, not the implementation. The caller decides namespaces and resource names; implementations only apply them. A wrapping layer needs different naming, and this is what lets it.

Index

Constants

View Source
const (
	// LabelManagedBy marks objects the engine owns, so cleanup and queries can
	// safely ignore everything else in a cluster.
	LabelManagedBy = "app.kubernetes.io/managed-by"

	// LabelName is the conventional Kubernetes app name.
	LabelName = "app.kubernetes.io/name"

	// LabelApp is the engine's own stable handle on a workload. This is the
	// selector key.
	LabelApp = "yacht/app"

	// LabelOwner records the owning principal. Deliberately NOT part of the
	// selector: selectors are immutable, and keeping ownership out of them
	// leaves room to reassign a workload without recreating it.
	LabelOwner = "yacht/owner-id"

	// ManagedByValue is the value of LabelManagedBy.
	ManagedByValue = "yacht"
)

Label and annotation keys applied to every object the engine manages.

These are part of the engine's public contract with the cluster: once workloads are running, changing them is not a rename but a migration. Kubernetes Deployment selectors are immutable, so a change to SelectorLabels in particular means deleting and recreating every Deployment in the fleet. Decide these before the first real workload, not after.

View Source
const (
	// AnnotationRevision changes on every apply, which is what triggers a
	// rollout when the pod template is otherwise unchanged.
	AnnotationRevision = "yacht/revision"

	// These two annotations are the database convergence key observed by the
	// app reconciler.
	AnnotationReleaseID     = "yacht/release-id"
	AnnotationConfigVersion = "yacht/config-version"
)

Annotation keys.

View Source
const (
	// HTTPLogNotEnabled means the ingress controller is running and is not
	// writing an access log. A setting, not a fault, and a different thing to
	// tell somebody than "no requests yet".
	HTTPLogNotEnabled = "not-enabled"

	// HTTPLogNoController means no ingress controller was found at all.
	HTTPLogNoController = "no-controller"
)

Why an app's HTTP log is empty.

View Source
const FieldManager = "yacht-engine"

FieldManager identifies the engine to Kubernetes server-side apply. Using a stable field manager is what lets repeated applies converge instead of fighting over fields.

View Source
const PullSecretName = "yacht-registry"

PullSecretName is the Secret an app's namespace holds its pull credential in. Named here rather than in the Kubernetes layer because the app service decides when to supply one.

Variables

View Source
var DefaultLimits = ResourceLimits{
	DefaultCPU:    "100m",
	DefaultMemory: "128Mi",
	MaxCPU:        "2",
	MaxMemory:     "4Gi",
}

DefaultLimits is the fallback applied when a NamespaceSpec leaves Limits empty. Modest on purpose: a self-hoster running a single node should not have one runaway container evict everything else.

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

ErrNotFound is returned when a requested workload does not exist.

View Source
var ErrNotStarted = errors.New("orchestrator: the container has not started")

ErrNotStarted means a container exists but has not run yet.

Its own error because it is not a failure to read: there is genuinely no log , and the reason is on the pod rather than in the log. Reported as a failure it reads as "the log is broken" for a container whose problem is that it never started — which is the more useful thing to say and is already on screen a few lines above.

View Source
var ErrNotSupported = errors.New("orchestrator: not supported on this cluster")

HTTPLogger is an orchestrator that can report requests.

Optional, like Builder and NodeManager: an orchestrator with no ingress controller in front of it is still a working orchestrator. ErrNotSupported means this cluster cannot be asked to do something.

A configuration answer rather than a failure: an ingress controller somebody else installed is not broken, it is simply not Yacht's to reconfigure.

Functions

func CertSecretName

func CertSecretName(host string) string

CertSecretName is the Secret an issued certificate for host is kept in.

The hostname itself where it fits, so `kubectl get certificates` reads as a list of domains. A Secret name may be 253 characters but the Certificate that shares it ends up in label values, which may be 63; past that the name is shortened and a hash of the whole host keeps it unique.

func ObjectLabels

func ObjectLabels(ref Ref) map[string]string

ObjectLabels returns the full label set for a managed object: the selector labels plus descriptive ones that are safe to change later.

func SelectorLabels

func SelectorLabels(name string) map[string]string

SelectorLabels returns the minimal, stable label set used to match a workload's pods.

Keep this small. Every key added here becomes immutable for the life of the Deployment.

func ValidateDNSLabel

func ValidateDNSLabel(field, s string) error

ValidateDNSLabel reports whether s is a legal Kubernetes object name.

Callers are expected to generate names rather than pass user input straight through, but this is the backstop: a name that reaches a cluster and collides with another owner's resource is the failure mode that matters most, so the check is here at the boundary rather than trusted to every caller.

Types

type AppSpec

type AppSpec struct {
	Ref

	ReleaseID     string
	ConfigVersion int64
	Image         string
	Replicas      int32

	// Port the container listens on. Zero means the workload takes no traffic.
	Port int32

	Env map[string]string

	// Resource requests and limits, as Kubernetes quantity strings. Empty
	// fields fall back to the namespace LimitRange.
	CPURequest    string
	CPULimit      string
	MemoryRequest string
	MemoryLimit   string

	// WritableRootFilesystem disables the read-only root filesystem.
	//
	// The default (false) is correct and should stay the default: it is one of
	// the cheapest real defences available. But some images genuinely cannot
	// run without writing outside /tmp, and the honest choice is an explicit,
	// visible escape hatch rather than silently weakening the default for
	// everyone. /tmp is always writable — see the k8s implementation.
	WritableRootFilesystem bool

	// Hosts are the hostnames routed to this workload. Empty means the
	// workload is reachable only inside the cluster.
	//
	// Plain strings rather than a richer type: the orchestrator's job is to
	// route a name, and which names are legitimate — platform-issued or
	// customer-claimed — is a decision that belongs above this seam.
	Hosts []string

	// Internal keeps the workload off the public internet: no hostname, and a
	// Service on the port it actually listens on rather than the fixed one.
	Internal bool

	// RunAsUser is the uid the container runs as. Zero leaves it to the image.
	//
	// Needed because the security posture requires a non-root user and most
	// stock images do not name one — the kubelet then refuses the pod with
	// "image will run as root" rather than picking something. A source that
	// knows its image knows the uid; an arbitrary image does not, and gets
	// nothing here.
	RunAsUser int64

	// FSGroup owns the mounted volumes.
	//
	// A freshly provisioned claim belongs to root, so a container running as
	// anyone else cannot write to it. This is what makes a volume usable by a
	// non-root process, and without it storage and the security posture are
	// mutually exclusive.
	FSGroup int64

	// ScratchPaths are writable empty directories inside the container.
	//
	// The root filesystem is read-only, which most software is fine with until
	// it wants one specific path — a socket directory, a lock file. Naming
	// those is what lets the read-only default stay the default.
	ScratchPaths []string

	// HealthPath is an HTTP path on the workload's own port that reports
	// whether it is ready to serve. Empty means no probe.
	//
	// One path drives both probes, because an app that answers differently on
	// two paths is answering a question nobody asked it.
	HealthPath string

	// Liveness turns the same path into a restart condition as well.
	//
	// Off by default, and deliberately a separate switch. Readiness only
	// withholds traffic; liveness kills the container. A probe that is a little
	// too impatient turns a slow-starting app into a restart loop, which
	// presents as the app being broken rather than as the probe being wrong.
	Liveness bool

	// Secrets are environment values that must not appear in the pod template.
	//
	// They become a Kubernetes Secret the container reads with envFrom, rather
	// than literals in the Deployment — so they are absent from
	// `kubectl get deploy -o yaml`, which is the copy people read, paste into
	// issues, and check into repositories.
	Secrets map[string]string

	// Volumes are storage the workload keeps across redeploys.
	//
	// Attaching any forces the workload to recreate rather than roll on
	// deploy, and limits it to one replica — both because a ReadWriteOnce
	// claim mounts on one node at a time. See VolumeSpec.
	Volumes []VolumeSpec

	// TLSHosts are the hostnames terminated TLS is requested for.
	//
	// A subset of Hosts, not all of them, and that is the whole point. The
	// certificate this relies on is the ingress controller's own default — a
	// wildcard for the platform domain — so listing a hostname it cannot match
	// does not produce a certificate, it produces a TLS handshake the browser
	// refuses. A custom domain is never under the platform domain, so it never
	// belongs here.
	//
	// It carries no certificate reference. An Ingress's TLS Secret must live in
	// the Ingress's own namespace, and every app has its own namespace, so one
	// pre-provisioned wildcard cannot be referenced from all of them.
	//
	// Empty leaves the TLS block off entirely.
	TLSHosts []string

	// IssuedHosts are hostnames that each get a certificate of their own,
	// issued by CertIssuer — the answer for a name the platform wildcard can
	// never cover, which is every custom domain.
	//
	// A subset of Hosts and disjoint from TLSHosts. One certificate per name
	// rather than one listing them all, because a certificate is issued whole
	// or not at all: a single domain whose DNS has drifted would otherwise hold
	// every other domain on the app hostage.
	//
	// Each lands in the Secret CertSecretName names, in the app's own
	// namespace, which is where an Ingress can reference it.
	IssuedHosts []string

	// CertIssuer is the cluster-wide issuer IssuedHosts are requested from.
	// Empty means the install has none, and IssuedHosts must be empty too.
	CertIssuer string

	// CNAMETarget, when set, becomes the ExternalDNS target annotation, so that
	// controller publishes a CNAME rather than the nodes' own addresses as A
	// records. Empty leaves the annotation off: an install running no
	// ExternalDNS should not carry configuration for one.
	CNAMETarget string

	// HTTPSOnly routes through the ingress controller's secure entrypoint only.
	HTTPSOnly bool

	// RegistryAuth is a Docker config.json for pulling this app's image, when
	// it comes from a private registry. Empty leaves the namespace without a
	// pull secret, which is right for a public image.
	RegistryAuth []byte
}

AppSpec is the desired state of a long-running workload.

Note what is absent: there is no field for privileged execution, host networking, host paths, or a service account token. Those are not omissions to be filled in later — the engine does not offer them, and the Kubernetes implementation hard-codes a restricted security context regardless of what a caller asks for.

func (AppSpec) Validate

func (s AppSpec) Validate() error

Validate checks the spec well enough to avoid sending nonsense to a cluster.

type AppStatus

type AppStatus struct {
	Phase              Phase
	ReleaseID          string
	ConfigVersion      int64
	Generation         int64
	ObservedGeneration int64
	Desired            int32
	Updated            int32
	Ready              int32
	Available          int32
	AvailableCondition bool
	Terminal           bool
	Reason             string
	Message            string
}

AppStatus is the observed state of a workload.

type BuildRequest

type BuildRequest struct {
	// Owner scopes the build's own resources, so a Job can be found and
	// cleaned up by the team it belongs to.
	Owner OwnerID

	// App is the app being built, for naming and labelling only.
	App string

	// Image is where the result must be pushed, fully qualified. Decided by
	// the caller rather than here: the path encodes which team owns the image,
	// and a builder choosing it could put one team's build in another's path.
	Image string

	// RepoURL, Ref and Subdir are what to build.
	RepoURL string
	Ref     string
	Subdir  string

	// Insecure means the registry speaks plain HTTP, so the push has to be
	// told not to expect TLS. Both build strategies need it and neither
	// discovers it, so it travels with the request.
	Insecure bool

	// RegistryAuth is a Docker config.json authorising the push. Passed per
	// build rather than held by the orchestrator, so the credential lives in
	// the one package allowed to unseal it and crosses this seam only when a
	// build actually needs it.
	RegistryAuth []byte

	// Log receives output as it arrives. A build that only reported at the end
	// would be silent for the several minutes it is most worth watching.
	Log func(chunk string)
}

BuildRequest is one build of a repository into an image.

type BuildResult

type BuildResult struct {
	// CommitSHA is what was actually built. A branch moves; this is the answer
	// to "what is running" a week later.
	CommitSHA string

	// RunAsUser is the numeric uid the image runs as, resolved during the
	// build. Zero when it could not be worked out, which leaves the decision
	// where it was before.
	RunAsUser int64
}

BuildResult is what a build produced.

type BuildState

type BuildState struct {
	// Found is false when there is no such Job. That means it finished and was
	// cleaned up, or it never started — either way nothing is running, which
	// is the answer the reconciler needs.
	Found bool

	// Done and Failed describe a Job that is still there.
	Done   bool
	Failed bool
	Reason string
}

BuildState is what the cluster knows about a build.

type Builder

type Builder interface {
	Build(ctx context.Context, req BuildRequest) (BuildResult, error)

	// BuildJobName is the name the Job for a request will have, known before
	// the build starts so it can be recorded. Deriving it here rather than
	// passing one in keeps the naming in the layer that has to make it a legal
	// Kubernetes object name.
	BuildJobName(req BuildRequest) string

	// BuildState reports what the cluster says about a build that was started
	// earlier, by name.
	//
	// This is what makes an interrupted build recoverable: the goroutine that
	// started it does not survive a restart and never existed on the other
	// replicas, but the Job is there for all of them to read.
	BuildState(ctx context.Context, jobName string) (BuildState, error)

	// CancelBuild removes a Job by the stable name BuildJobName returned. The
	// operation is idempotent: a Job already gone is a cancelled build too.
	CancelBuild(ctx context.Context, jobName string) error
}

Builder turns a repository into an image.

Optional, like NodeManager, and for the same reason: an orchestrator that cannot run a build is a working orchestrator, and folding this into the required interface would make every implementation carry it.

type Certificate

type Certificate struct {
	// Issued is true once a certificate for the host is in place. False
	// covers both "still being issued" and "failing to issue": the Secret is
	// only written on success, and it cannot tell those apart.
	Issued bool

	// NotAfter is when the issued certificate expires. The issuer renews well
	// before then; a date that has passed means renewal has been failing.
	NotAfter time.Time

	// Trusted is true when the certificate chains to an authority browsers
	// trust. Let's Encrypt's staging environment — the installer's default —
	// issues certificates that do not, and a visitor gets the same warning as
	// for no certificate at all.
	Trusted bool
}

Certificate is what is known about the certificate issued for one hostname.

type CertificateReader

type CertificateReader interface {
	Certificate(ctx context.Context, ref Ref, host string) (Certificate, error)
}

CertificateReader reports on certificates issued for IssuedHosts.

Optional, and asserted for like NodeManager. An implementation that cannot read them leaves the dashboard saying issuance was requested rather than what became of it.

type ClusterInspector

type ClusterInspector interface {
	// ClusterSummary returns headline capacity and utilisation.
	ClusterSummary(ctx context.Context) (ClusterSummary, error)

	// Nodes lists the machines backing the runtime.
	Nodes(ctx context.Context) ([]NodeInfo, error)

	// Pods lists running pods.
	Pods(ctx context.Context, opts PodListOptions) ([]PodInfo, error)

	// Events lists recent cluster events, newest first, capped at limit.
	Events(ctx context.Context, limit int) ([]EventInfo, error)

	// Logs reads a container's output.
	//
	// On the read-only seam because it is a read. The scoping that matters is
	// not here: the namespace and pod arrive already resolved from an app the
	// caller looked up by owner, because a pod name on its own is enough to
	// read another tenant's logs.
	Logs(ctx context.Context, opts LogOptions) ([]LogLine, error)

	// LogStream follows a container's output until ctx is done.
	//
	// Separate from Logs rather than a flag on LogOptions: the batch call
	// returns a complete slice, and a flag that makes it never return would
	// leave that signature honest for one value of one field and false for the
	// other.
	//
	// The iterator holds a connection open for as long as it is read, so a
	// caller must either drain it or cancel ctx. Scoping is the caller's job
	// here exactly as it is for Logs — the namespace and pod arrive already
	// resolved from an app looked up by owner.
	LogStream(ctx context.Context, opts LogOptions) (iter.Seq2[LogLine, error], error)

	// Volumes lists an owner's persistent volume claims.
	//
	// Scoped rather than cluster-wide: a claim names the workload it belongs to,
	// and one team reading another's is a disclosure. A claim the engine did not
	// create carries no owner and belongs to whoever runs the cluster, so it is
	// shown to nobody here.
	Volumes(ctx context.Context, owner OwnerID) ([]VolumeInfo, error)
}

ClusterInspector reports on the runtime itself rather than on workloads.

Read-only by design. Everything that mutates a cluster goes through the workload methods above, so an implementation can serve inspection from a cache, a read replica, or a restricted credential without that choice leaking into the deploy path.

type ClusterSummary

type ClusterSummary struct {
	Nodes      int
	NodesReady int

	Pods        int
	PodCapacity int

	CPUUsedMillis     int64
	CPUCapacityMillis int64
	MemUsedBytes      int64
	MemCapacityBytes  int64

	Volumes    int
	UsageKnown bool
}

ClusterSummary is the headline state of the runtime.

func (ClusterSummary) CPUPercent

func (c ClusterSummary) CPUPercent() int

CPUPercent returns cluster CPU utilisation.

func (ClusterSummary) MemoryPercent

func (c ClusterSummary) MemoryPercent() int

MemoryPercent returns cluster memory utilisation.

type EventInfo

type EventInfo struct {
	Namespace string
	Type      string // Normal | Warning
	Reason    string
	Message   string
	Object    string // e.g. "Pod/web-7d9f4b6c85-2xk9p"
	Count     int32
	LastSeen  time.Time
}

EventInfo is one cluster event.

Events are where Kubernetes explains itself. "Pod is Pending" tells an operator nothing; "Back-off pulling image … ErrImagePull" or "Insufficient cpu" tells them exactly what to fix. Surfacing them is the difference between a dashboard that reports a problem and one that helps.

func (EventInfo) Warning

func (e EventInfo) Warning() bool

Warning reports whether the event is an anomaly rather than routine chatter.

type HTTPLogLine

type HTTPLogLine struct {
	At       time.Time
	Host     string
	Method   string
	Path     string
	Protocol string
	Status   int
	Bytes    int64
	Client   string
	Duration time.Duration
}

HTTPLogLine is one request as the ingress controller saw it.

type HTTPLogOptions

type HTTPLogOptions struct {
	// Hosts is the tenancy boundary. One controller logs every tenant's
	// traffic, so the caller resolves which hosts belong to the app from an
	// owner-scoped query and this layer never decides it.
	Hosts []string
}

HTTPLogOptions asks for the requests made to some hostnames.

type HTTPLogger

type HTTPLogger interface {
	HTTPLogs(ctx context.Context, opts HTTPLogOptions) (HTTPLogs, error)

	// HTTPLogHint is the configuration that switches the access log on, for
	// the page to show when Yacht cannot apply it.
	HTTPLogHint() string

	// EnableHTTPLogs applies it, where this cluster's controller is one Yacht
	// can reconfigure. Called once at startup rather than from a page: it
	// restarts a controller every workload routes through, which is a
	// cluster-wide cost and so never one app's decision to make.
	EnableHTTPLogs(ctx context.Context) error

	// HTTPLogsEnabled reports whether the running controller is writing one.
	HTTPLogsEnabled(ctx context.Context) bool
}

type HTTPLogs

type HTTPLogs struct {
	Lines []HTTPLogLine

	// Reason explains an empty result that is not simply "no traffic".
	Reason string
}

HTTPLogs is what the ingress controller recorded.

type LogLine

type LogLine struct {
	At   time.Time
	Text string
}

LogLine is one line of container output.

The timestamp is separate from the text so the page can align and format it, and so a line whose own content happens to start with something timestamp-shaped is not mistaken for one.

type LogOptions

type LogOptions struct {
	// Namespace and Pod name the container. Both come from an app the caller
	// has already resolved by owner — never from a request — because a pod
	// name is enough to read any tenant's output.
	Namespace string
	Pod       string

	// Tail caps how many lines come back. A pod that has been up for a month
	// has more output than anyone wants to render, and no cap means the whole
	// lot crosses the wire before anything is shown.
	Tail int64

	// Previous reads the container that died rather than the one running now.
	// It is the only way to see why something crash-looped: the running
	// container started after the interesting part.
	Previous bool
}

LogOptions selects which container output to read.

type NamespaceSpec

type NamespaceSpec struct {
	Owner OwnerID
	Name  string

	// Limits become a default LimitRange in the namespace, so a workload that
	// specifies no resources of its own still cannot run unbounded. Zero value
	// means DefaultLimits is used; it is deliberately not possible to create a
	// namespace with no limits at all.
	Limits ResourceLimits
}

NamespaceSpec describes an isolation boundary for one owner's workloads.

func (NamespaceSpec) Validate

func (s NamespaceSpec) Validate() error

Validate checks the spec.

type NodeInfo

type NodeInfo struct {
	Name    string
	Ready   bool
	Roles   []string
	Pool    string // scheduling pool, from the yacht/pool label
	Address string

	Version      string // kubelet version
	OS           string
	Architecture string

	CPUCapacityMillis int64
	CPUUsedMillis     int64
	MemCapacityBytes  int64
	MemUsedBytes      int64

	Pods          int
	PodCapacity   int
	CreatedAt     time.Time
	UsageKnown    bool // false when no metrics source is available
	Unschedulable bool

	// Reason and Message are why the node is not Ready, taken from its own
	// conditions.
	//
	// The same argument PodInfo.Reason makes, for the same reason. Ready is one
	// bit, and a machine that has registered and is starting its container
	// runtime draws exactly like one that registered an hour ago and never came
	// up — which are the two situations somebody watching a join has to tell
	// apart. Kubernetes explains itself in the conditions; this carries that
	// across without a Kubernetes type crossing the seam.
	//
	// Empty while the node is Ready.
	Reason  string
	Message string
}

NodeInfo describes one machine in the runtime.

func (NodeInfo) CPUPercent

func (n NodeInfo) CPUPercent() int

CPUPercent returns CPU utilisation, or 0 when usage is unknown.

func (NodeInfo) Joining

func (n NodeInfo) Joining() bool

Joining reports whether this node has registered and is not serving yet.

The interesting half of "not ready": a machine partway through starting is progress, and one that has been not-ready for an hour is a problem, but the boolean says the same thing about both. Callers pair this with how long the node has existed.

func (NodeInfo) MemoryPercent

func (n NodeInfo) MemoryPercent() int

MemoryPercent returns memory utilisation, or 0 when usage is unknown.

type NodeManager

type NodeManager interface {
	// Cordon stops or resumes scheduling onto a node. It does not move
	// anything already running there.
	Cordon(ctx context.Context, node string, unschedulable bool) error

	// Drain evicts the pods a node is running and reports how many it asked
	// to leave.
	//
	// Eviction is a request, not a deletion: it respects disruption budgets,
	// so a pod whose budget would be violated stays and is reported rather
	// than being taken down anyway. It returns once the evictions are
	// requested, not once they have finished — the caller watches the node
	// empty rather than holding a request open for however long that takes.
	Drain(ctx context.Context, node string) (requested int, err error)

	// DeleteNode removes the node object. The machine itself is not touched:
	// it stops being part of the cluster, and shutting it down is a separate
	// act somebody performs where the machine is.
	DeleteNode(ctx context.Context, node string) error
}

NodeManager takes a machine out of service.

Optional, and asserted for rather than required. Everything else here can be served by a restricted credential — inspection from a cache or a read replica, workloads through a namespaced role — and evicting pods across every namespace and deleting a node object is the one thing that cannot. An implementation that cannot do it simply does not satisfy this, and the surface offering it stays off rather than failing when somebody presses it.

Deliberately not part of ClusterInspector, which is read-only by design.

type Noop

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

Noop is an in-memory Orchestrator.

It exists so the control plane can boot, and its HTTP handlers can be tested, without a cluster. It is not a simulator: it records what it was asked to do and reports workloads as running.

func NewNoop

func NewNoop() *Noop

NewNoop returns an empty in-memory orchestrator.

func (*Noop) AppStatus

func (n *Noop) AppStatus(_ context.Context, ref Ref) (AppStatus, error)

func (*Noop) ApplyApp

func (n *Noop) ApplyApp(_ context.Context, spec AppSpec) error

func (*Noop) Apps

func (n *Noop) Apps() []string

Apps returns the workload refs recorded so far, sorted. Test helper.

func (*Noop) ClusterSummary

func (n *Noop) ClusterSummary(context.Context) (ClusterSummary, error)

ClusterSummary reports what was applied, not a simulated cluster.

Node and capacity figures stay zero because there is no cluster to report on. Inventing plausible numbers here would make a disconnected install look healthy, which is precisely the state an operator needs to notice.

func (*Noop) DeleteApp

func (n *Noop) DeleteApp(_ context.Context, ref Ref) error

func (*Noop) DeleteNamespace

func (n *Noop) DeleteNamespace(_ context.Context, name string) error

func (*Noop) EnsureNamespace

func (n *Noop) EnsureNamespace(_ context.Context, spec NamespaceSpec) error

func (*Noop) Events

func (n *Noop) Events(context.Context, int) ([]EventInfo, error)

Events returns nothing: there is no cluster to emit any.

func (*Noop) LogStream

func (n *Noop) LogStream(
	context.Context, LogOptions,
) (iter.Seq2[LogLine, error], error)

LogStream ends immediately. Without a cluster there is no container writing, and a stream that stayed open would hang every caller waiting for a line that is never coming.

func (*Noop) Logs

func (n *Noop) Logs(context.Context, LogOptions) ([]LogLine, error)

Logs returns nothing. There is no container to have written any, and inventing lines would make an empty install look like a running one.

func (*Noop) Namespaces

func (n *Noop) Namespaces() []string

Namespaces returns the namespaces recorded so far, sorted. Test helper.

func (*Noop) Nodes

func (n *Noop) Nodes(context.Context) ([]NodeInfo, error)

Nodes returns nothing: an in-memory orchestrator has no machines.

func (*Noop) Ping

func (n *Noop) Ping(context.Context) error

func (*Noop) Pods

func (n *Noop) Pods(_ context.Context, opts PodListOptions) ([]PodInfo, error)

Pods synthesises one entry per replica of each applied workload, so the dashboard has something coherent to render without a cluster.

func (*Noop) Volumes

func (n *Noop) Volumes(context.Context, OwnerID) ([]VolumeInfo, error)

Volumes returns nothing: the engine's in-memory mode provisions no storage.

type Orchestrator

type Orchestrator interface {
	// EnsureNamespace creates or updates an owner's namespace, including the
	// security posture and default limits that go with it.
	EnsureNamespace(ctx context.Context, spec NamespaceSpec) error

	// DeleteNamespace removes a namespace and everything in it. It returns nil
	// if the namespace is already gone.
	DeleteNamespace(ctx context.Context, name string) error

	// ApplyApp converges a workload to the given spec.
	ApplyApp(ctx context.Context, spec AppSpec) error

	// DeleteApp removes a workload. It returns nil if already gone.
	DeleteApp(ctx context.Context, ref Ref) error

	// AppStatus reports observed state. It returns ErrNotFound if the workload
	// does not exist.
	AppStatus(ctx context.Context, ref Ref) (AppStatus, error)

	// Ping verifies the runtime is reachable. Used by health checks and at
	// startup, so a misconfigured cluster fails loudly instead of at first
	// deploy.
	Ping(ctx context.Context) error

	ClusterInspector
}

Orchestrator applies desired state to a runtime and reports back what it observes.

Implementations must be safe to call repeatedly with the same arguments: every method is expected to converge rather than fail on "already exists".

type OwnerID

type OwnerID string

OwnerID identifies who owns a resource.

The engine runs with exactly one owner (see package identity). A wrapping application maps this to whatever its own principal is. The orchestrator never interprets the value: it labels resources with it and otherwise treats it as opaque.

type Phase

type Phase string

Phase is a coarse lifecycle state, deliberately smaller than the set of conditions Kubernetes reports.

const (
	PhasePending  Phase = "pending"
	PhaseRunning  Phase = "running"
	PhaseDegraded Phase = "degraded"
	PhaseStopped  Phase = "stopped"
)

type PodInfo

type PodInfo struct {
	Name      string
	Namespace string
	Phase     string
	Node      string
	Ready     int32
	Total     int32
	Restarts  int32
	CreatedAt time.Time

	// App is the workload this pod belongs to, empty for pods the engine
	// does not manage.
	App   string
	Owner OwnerID

	// Reason and Message are why a container is not running, taken from the
	// container's own waiting state.
	//
	// Kubernetes explains itself here the way it does in events, and the phase
	// alone does not: "Pending" and "CreateContainerConfigError" are the same
	// picture to somebody watching a deploy, and only one of them says the
	// image runs as root and the cluster refuses it. Empty while the container
	// is running or has never been asked to start.
	Reason  string
	Message string

	// DrainMoves reports whether draining this pod's node would move it.
	//
	// Decided where the drain is implemented rather than by a caller reading
	// fields and re-deriving the rule. A page that counted differently from
	// the drain would call a node empty while the drain still had work, and
	// the two would drift the first time either changed.
	DrainMoves bool
}

PodInfo describes one running pod.

func (PodInfo) Healthy

func (p PodInfo) Healthy() bool

Healthy reports whether every container in the pod is ready.

type PodListOptions

type PodListOptions struct {
	// Namespace limits results. Empty means every namespace.
	Namespace string

	// ManagedOnly restricts results to workloads the engine created.
	ManagedOnly bool

	// Node limits results to pods scheduled on one machine. Empty means every
	// node.
	Node string

	// Owner limits results to one principal's workloads.
	//
	// Empty means every owner, which is right for an operator view and wrong
	// for anything a member can reach. These pages were written when the engine
	// had one owner and everything in the cluster belonged to whoever was
	// looking; teams made that false, and a cluster page is the easiest place
	// to forget it.
	Owner OwnerID
}

PodListOptions narrows a pod query.

type Ref

type Ref struct {
	Owner     OwnerID
	Namespace string
	Name      string
}

Ref uniquely identifies a workload.

func (Ref) String

func (r Ref) String() string

func (Ref) Validate

func (r Ref) Validate() error

Validate checks that a Ref is well formed and safe to send to a cluster.

type ResourceLimits

type ResourceLimits struct {
	DefaultCPU    string // applied to containers that request nothing
	DefaultMemory string
	MaxCPU        string // ceiling any single container may request
	MaxMemory     string
}

ResourceLimits expresses CPU and memory bounds using Kubernetes quantity strings ("500m", "512Mi").

func (ResourceLimits) OrDefaults

func (l ResourceLimits) OrDefaults() ResourceLimits

OrEmpty returns l, substituting DefaultLimits for any unset field.

type VolumeInfo

type VolumeInfo struct {
	Name          string
	Namespace     string
	Phase         string // Pending | Bound | Lost
	StorageClass  string
	CapacityBytes int64
	RequestBytes  int64
	AccessModes   []string
	App           string
	Owner         OwnerID
	CreatedAt     time.Time
}

VolumeInfo is one persistent volume claim.

func (VolumeInfo) Bound

func (v VolumeInfo) Bound() bool

Bound reports whether the claim has been satisfied.

type VolumeSpec

type VolumeSpec struct {
	Name      string
	MountPath string
	SizeBytes int64

	// Class is the StorageClass. Empty means the cluster default.
	Class string
}

VolumeSpec is one piece of storage attached to a workload.

Size is bytes rather than a Kubernetes quantity string so that comparing a new size against the old is arithmetic. That comparison is the whole of the expansion rule: Kubernetes cannot shrink a claim, and neither can anything else — the filesystem on it may be full.

Directories

Path Synopsis
Package k8s implements orchestrator.Orchestrator against a Kubernetes cluster (K3s in the common case).
Package k8s implements orchestrator.Orchestrator against a Kubernetes cluster (K3s in the common case).

Jump to

Keyboard shortcuts

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