Documentation
¶
Overview ¶
Package v1alpha1 is the ONE declaration of forge's deploy kinds — Workload, StaticSite and ManagedDatabase — in API group forge.dev.
ONE DECLARATION, TWO DESTINATIONS ¶
These types are the single source of truth for what an app author can say about a tier. forge's KCL schemas for the tiers are GENERATED from them (kcl/tiers/tiers_gen.k), and the control plane's CRDs are these types verbatim. That is the whole point of this package: the same declaration deploys self-hosted (forge renders it with deploy.Render and applies it) and hosted (the control plane receives it as a CR, calls the same Render, and composes its hosted concerns around the result). Before this package there were four definitions of a hosted backend — forge's KCL schema, control-plane's CRD, control-plane's deployrender config and a JSONB row struct — using three unit systems and three different sets of env channels. ADR 0002 (docs/adr/0002-one-workload-model.md) then folded every other way to declare a runnable thing into the one Workload kind.
WHAT IS SPEC, WHAT IS NOT ¶
A spec holds ONLY what the app author declares. Two categories of information that the previous definitions put on the spec are absent from it, on purpose:
- HOSTED IDENTITY (org, customer, environment id, deployment id, and the namespace the platform assigns). A spec field is something an author can WRITE, and ownership must never be author-writable. The control plane stamps identity as labels (see the Label* constants) on the CR it publishes. It also makes "same declaration, two destinations" literally true: a spec carrying an org id cannot be deployed to a cluster that has no org.
- OBSERVED FACTS (allocated hostname, URL, phase, the digest actually live). Those are status, written by whoever observed them — the operator on hosted, `forge env status` computing it on read when self-hosted.
Cluster and namespace are also absent. Both describe the TARGET, not the app, and the target is the environment's deploy block. forge's own KCL doc admitted this ("the platform fills these in; they are not an app-facing knob"). The renderer takes the namespace as a RenderContext argument.
THE SCHEMA IS CLOSED, AND RESTRICTION IS A PROFILE ¶
The schema is closed. A field that is not declared cannot be expressed: KCL refuses it at author time and the API server's structural schema prunes it at apply time. So the Kubernetes surface a Workload can reach is an ALLOWLIST by construction. There is no securityContext, no node placement, no volumes and no raw-manifest passthrough, and a new PodSpec field is unreachable until someone adds it here on purpose.
Inside that closed set, what a DESTINATION accepts is a Profile (see profile.go). ProfileFull is a cluster the author operates. ProfileRestricted is the hosted runtime on shared nodes, and it refuses RBAC, CRDs, raw Secret and ConfigMap reads and the other fields that reach past the hosted user's own pod. Every spec field is classified in one table, FieldProfiles, and a reflection test fails the build when a field is added without a classification. That keeps the profile default-deny.
INVARIANTS LIVE IN GO ¶
Every rule a tier enforces lives in Validate() here, and forge's KCL schema carries a generated copy. The previous design kept the rules (the resource ratio band, image pinning) only as KCL checks, and the hosted path — which never ran forge's KCL — bypassed them. The live dev cluster ran a hosted backend at 100m/128Mi, off the band the KCL check claimed to guarantee. The KCL copy is still useful, because it fails at author time with a message naming the field. But it is a convenience, and Validate() is the check.
+kubebuilder:object:generate=true +groupName=forge.dev
Index ¶
- Constants
- Variables
- func FullOnlyKindReason(k WorkloadKind) string
- func FullOnlyReason(path string) string
- func ValidateDatabaseName(name string) error
- func ValidateImage(image string) error
- func ValidateRuntimeConfig(rc map[string]RuntimeConfigValue) error
- type ConfigMapKeyRef
- type ConfigMapVolumeSource
- type Container
- type DatabaseBackupStatus
- type DatabaseCredentialKey
- type DatabaseDashboards
- type DatabaseRef
- type DatabaseRestore
- type DeletionPolicy
- type DeployPhase
- type DeploymentStrategy
- type EmptyDirVolumeSource
- type Engine
- type EnvVar
- type FieldRef
- type Invalidate
- type KeyToPath
- type ManagedDatabase
- type ManagedDatabaseList
- type ManagedDatabaseSpec
- type ManagedDatabaseStatus
- type ManagedSecretRef
- type PVCVolumeSource
- type Phase
- type PodSecurity
- type PolicyRule
- type Port
- type PortProtocol
- type ProbeTimings
- type Probes
- type Profile
- type Resources
- type RuntimeConfigValue
- type SecretKeyRef
- type SecretVolumeSource
- type StaticSite
- type StaticSiteCDN
- type StaticSiteList
- type StaticSiteSpec
- type StaticSiteStatus
- type TaintEffect
- type TierStatus
- type Toleration
- type TolerationOperator
- type Volume
- type VolumeSource
- type Workload
- type WorkloadKind
- type WorkloadList
- type WorkloadSpec
- func (in *WorkloadSpec) DeepCopy() *WorkloadSpec
- func (in *WorkloadSpec) DeepCopyInto(out *WorkloadSpec)
- func (s WorkloadSpec) EffectiveDeployPhase() DeployPhase
- func (s WorkloadSpec) EffectiveKind() WorkloadKind
- func (s WorkloadSpec) EffectiveLeaderElection() bool
- func (s WorkloadSpec) EffectiveProbes() *Probes
- func (s WorkloadSpec) ExposedPort() *Port
- func (s WorkloadSpec) ProbePort() int32
- func (s WorkloadSpec) UsesGeneratedServiceAccount() bool
- func (s WorkloadSpec) Validate(p Profile) error
- func (s WorkloadSpec) WithDefaults() WorkloadSpec
- type WorkloadStatus
- type WorkloadURLRef
Constants ¶
const ( LabelOrgID = "forge.dev/org-id" LabelEnvironmentID = "forge.dev/environment-id" LabelDeploymentID = "forge.dev/deployment-id" )
Hosted identity labels.
Identity is METADATA, never spec (see the package doc): the control plane stamps these on every CR it publishes, and its operators read them. They are declared here, next to the types, only so the two sides of the wire spell them identically. Render never reads or writes them — a renderer that consulted hosted identity would no longer be the same function for both destinations.
const ( DefaultCPUMillicores int64 = 250 DefaultMemoryBytes int64 = 1 << 30 )
Default requests — forge's entry shape. Mirrored by the kubebuilder default markers on the REQUEST fields above, and TestDefaultMarkersMatchGoDefaults keeps the two equal (and keeps the limits marker-free).
const ( DefaultDashboardRowCap int32 = 1000 MaxDashboardRowCap int32 = 10000 DefaultDashboardTimeoutMs int32 = 5000 MinDashboardTimeoutMs int32 = 100 MaxDashboardTimeoutMs int32 = 10000 DefaultDashboardMaxConcurrency int32 = 2 MaxDashboardMaxConcurrency int32 = 4 )
Defaults and ceilings of DatabaseDashboards. The KCL schema (kcl/schema.k DatabaseDashboards) and the release shape (release.DefaultDatabaseDashboards) hold the same numbers separately; TestDatabaseDashboardsDefaultsAgreeWithKCL keeps the three from drifting.
const ( DefaultDatabaseInstances int32 = 1 MaxDatabaseInstances int32 = 3 DefaultDatabaseStorageGiB int32 = 10 MaxDatabaseStorageGiB int32 = 500 )
Defaults and ceilings. THE CEILING IS THE POINT: a default only helps the author who declared nothing, while a declaration of 50 instances describes a capacity event.
const ( // TmpVolumeName / TmpMountPath are the emptyDir that makes /tmp // writable under forge's read-only root filesystem. Every pod gets it. TmpVolumeName = "tmp" TmpMountPath = "/tmp" // DataVolumeName / DataMountPath are the StorageGiB PersistentVolumeClaim // mount. They are reserved only when StorageGiB > 0: a stateless // workload may use "data" and /data for a scratch volume of its own. DataVolumeName = "data" DataMountPath = "/data" )
Reserved volumes the renderer adds to every pod. Declared here, beside the types, so the renderer and Validate agree on them without being told: a declared Volume that reused one of these names or paths would make an invalid pod (a duplicate volume name) or shadow forge's own mount.
const ( // MaxWorkloadNameLength is the RFC-1123 label limit every Kubernetes // object name, Service DNS name and container name shares. MaxWorkloadNameLength = 63 // StandaloneJobHashLength is the length of the spec hash a standalone // job's Job name carries (<name>-<hash>). A Job's pod template is // immutable, so a changed spec must be a NEW Job, and the hash is what // makes it one. StandaloneJobHashLength = 10 // MaxStandaloneJobNameLength leaves room for "-" plus the hash inside // the 63-character label. MaxStandaloneJobNameLength = MaxWorkloadNameLength - 1 - StandaloneJobHashLength )
Rendered-name limits. metadata.name becomes the name of every object the renderer emits, so its bounds live with the type rather than being discovered at render time.
const ( DefaultProbeInitialDelaySeconds int32 = 0 DefaultProbePeriodSeconds int32 = 5 DefaultProbeTimeoutSeconds int32 = 3 DefaultProbeFailureThreshold int32 = 3 // A startup probe polls every DefaultStartupPeriodSeconds for up to // DefaultStartupPeriodSeconds*DefaultStartupFailureThreshold (360s) // before the kubelet restarts the container. // // The window must cover the process's startup dependency wait: a forge // server retries its database connect for serverkit.DefaultDBConnectTimeout // (5m, DB_CONNECT_TIMEOUT) while reporting not-ready, and the kubelet // must not kill it inside that wait. 360s = 300s + 60s for the rest of // boot. TestStartupWindowCoversDBConnectTimeout pins the relation; a // deployment that raises DB_CONNECT_TIMEOUT past it must widen this too. DefaultStartupPeriodSeconds int32 = 1 DefaultStartupFailureThreshold int32 = 360 DefaultReadinessPath = "/readyz" DefaultLivenessPath = "/healthz" // LivenessMarginSeconds is added on top of the whole readiness budget // to get the liveness initial delay, so a start that is merely slow is // never mistaken for a deadlock. LivenessMarginSeconds int32 = 15 )
Probe defaults, mirrored by the markers above (TestDefaultMarkersMatchGoDefaults keeps the two equal).
const ( DefaultWorkloadKind = KindService DefaultReplicas int32 = 1 DefaultPortProtocol = ProtocolTCP // DefaultHTTPPortName is the port name a probe, a Service route and // the host runtime look for first. DefaultHTTPPortName = "http" )
Default workload values, mirrored by the markers above.
const BeforeAll = "*"
BeforeAll is the BROADCAST entry for Before: gate every workload in the environment without naming any of them. Schema migration is the case it exists for. An enumerated list of dependents goes stale the day a workload is added, and the new workload then serves traffic against a schema it does not have. A wildcard has nothing to forget.
const DefaultKeepReleases = 10
DefaultKeepReleases is forge's default retention depth.
const Group = "forge.dev"
Group is the API group of every deploy-tier kind forge declares.
forge.dev rather than control-plane's historical reliant.dev: the spec is forge's, and reliant.dev is a product name the self-hosted user has no relationship with. A self-hosted cluster that never saw the control plane should not carry a CR group named after it.
const ManagedSecretsSecretName = "forge-managed-secrets"
ManagedSecretsSecretName is the Secret every ManagedSecret env var reads from, keyed by the secret's logical name. Whoever resolves the environment's secret store — forge's secrets provider when self-hosted, the control plane's materializer when hosted — writes this Secret into the workload's namespace before the workload starts.
ONE name, fixed here. Render emits the secretKeyRef and the materializer writes the Secret, and both must agree on the name without being told it.
const MaxDomains = 8
MaxDomains bounds a port's custom domains.
const MaxRuntimeConfigEntries = 128
MaxRuntimeConfigEntries bounds the runtime config document. It is served to every browser on every page load, so it is configuration, not storage.
const MinKeepReleases = 2
MinKeepReleases is the retention floor: the live release plus its predecessor.
const Version = "v1alpha1"
Version is the API version of this package's types.
Variables ¶
var AddToScheme = SchemeBuilder.AddToScheme
AddToScheme adds every kind in this package to a scheme.
var FieldProfiles = map[string]Profile{ "kind": ProfileRestricted, "image": ProfileRestricted, "command": ProfileRestricted, "args": ProfileRestricted, "replicas": ProfileRestricted, "resources": ProfileRestricted, "resources.cpuRequestMillicores": ProfileRestricted, "resources.cpuLimitMillicores": ProfileRestricted, "resources.memoryRequestBytes": ProfileRestricted, "resources.memoryLimitBytes": ProfileRestricted, "env": ProfileRestricted, "env.name": ProfileRestricted, "env.value": ProfileRestricted, "env.managedSecret": ProfileRestricted, "env.managedSecret.name": ProfileRestricted, "env.managedSecret.optional": ProfileRestricted, "env.databaseRef": ProfileRestricted, "env.databaseRef.name": ProfileRestricted, "env.databaseRef.key": ProfileRestricted, "env.workloadURL": ProfileRestricted, "env.workloadURL.name": ProfileRestricted, "env.secretRef": ProfileFull, "env.configMapRef": ProfileFull, "env.fieldRef": ProfileFull, "ports": ProfileRestricted, "ports.name": ProfileRestricted, "ports.port": ProfileRestricted, "ports.protocol": ProfileRestricted, "ports.expose": ProfileRestricted, "ports.appProtocol": ProfileRestricted, "ports.domains": ProfileRestricted, "probes": ProfileRestricted, "probes.port": ProfileRestricted, "probes.readinessPath": ProfileRestricted, "probes.livenessPath": ProfileRestricted, "probes.startupPath": ProfileRestricted, "probes.tcp": ProfileRestricted, "probes.initialDelaySeconds": ProfileRestricted, "probes.periodSeconds": ProfileRestricted, "probes.timeoutSeconds": ProfileRestricted, "probes.failureThreshold": ProfileRestricted, "storageGiB": ProfileRestricted, "before": ProfileRestricted, "deployPhase": ProfileRestricted, "schedule": ProfileFull, "namespacedRBAC": ProfileFull, "clusterRBAC": ProfileFull, "crds": ProfileFull, "group": ProfileFull, "version": ProfileFull, "leaderElection": ProfileFull, "serviceAccountAnnotations": ProfileFull, "sidecars": ProfileFull, "volumes": ProfileFull, "serviceAccount": ProfileFull, "nodeSelector": ProfileFull, "tolerations": ProfileFull, "podAnnotations": ProfileFull, "priorityClassName": ProfileFull, "enableServiceLinks": ProfileFull, "activeDeadlineSeconds": ProfileRestricted, "strategy": ProfileRestricted, "terminationGracePeriodSeconds": ProfileFull, "securityContext": ProfileFull, }
FieldProfiles classifies EVERY WorkloadSpec field, by JSON path, at the most restrictive profile that allows it. It is the whole profile mask, in one table.
THE TABLE IS DEFAULT-DENY. Validate walks a spec by reflection and refuses, under EVERY profile, any set field whose path is missing from this table, and TestFieldProfilesClassifyEverySpecField fails the build when a field is added to the spec without an entry. So a new field can never silently become writable by a hosted user: someone has to write down which profile allows it.
Paths are JSON names joined with '.', and list elements share their list's path ("env.value", "ports.expose"). A field classified ProfileFull is refused whole under ProfileRestricted, so its children need no entry. Every child of a Restricted-allowed struct needs one.
var GroupVersion = schema.GroupVersion{Group: Group, Version: Version}
GroupVersion is forge.dev/v1alpha1.
var KindProfiles = map[WorkloadKind]Profile{ KindService: ProfileRestricted, KindWorker: ProfileRestricted, KindJob: ProfileRestricted, KindCron: ProfileFull, KindOperator: ProfileFull, KindTool: ProfileFull, }
KindProfiles is the kind mask: the most restrictive profile each kind is allowed under. Restricted runs service, worker and job.
var SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)
SchemeBuilder registers this package's kinds. The same shape controller-runtime's scheme.Builder produces, built on apimachinery's runtime.SchemeBuilder so this package pulls in no controller-runtime code.
Functions ¶
func FullOnlyKindReason ¶
func FullOnlyKindReason(k WorkloadKind) string
FullOnlyKindReason is why a Full-only kind is refused under ProfileRestricted, or "" when the kind is allowed there.
func FullOnlyReason ¶
FullOnlyReason is why a ProfileFull field path (a FieldProfiles key) is refused under ProfileRestricted, or "" when the path is not Full-only. Exported so author-time checks (the generated KCL runtime mask) state the same reason admission does, from the one table.
func ValidateDatabaseName ¶
ValidateDatabaseName checks a ManagedDatabase's metadata.name. That name becomes the Cluster name unchanged and, with hyphens folded to underscores, the Postgres database and role names. Postgres silently TRUNCATES an identifier longer than 63 bytes, so two long names could address the same database. 40 characters leaves room for the "_app" role suffix, and an over-long name is refused, never truncated.
func ValidateImage ¶
ValidateImage enforces forge's image invariant: an explicit registry host and a pin (tag or digest).
Explicit registry because forge does not build this image and must not resolve a bare name against a registry the author does not control. Pinned because an unpinned image makes a redeploy silently change what runs. `:latest` counts as a tag, as it did in forge's KCL check. Refusing it is a separate policy decision, and making it here would widen the merge beyond what either side enforced.
func ValidateRuntimeConfig ¶
func ValidateRuntimeConfig(rc map[string]RuntimeConfigValue) error
ValidateRuntimeConfig checks a StaticSite's runtime config: legal keys and exactly one of value / workloadURL per entry.
Types ¶
type ConfigMapKeyRef ¶
type ConfigMapKeyRef struct {
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=253
Name string `json:"name"`
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=253
Key string `json:"key"`
}
ConfigMapKeyRef names one key of a ConfigMap in the workload's own namespace.
func (*ConfigMapKeyRef) DeepCopy ¶
func (in *ConfigMapKeyRef) DeepCopy() *ConfigMapKeyRef
DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConfigMapKeyRef.
func (*ConfigMapKeyRef) DeepCopyInto ¶
func (in *ConfigMapKeyRef) DeepCopyInto(out *ConfigMapKeyRef)
DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
type ConfigMapVolumeSource ¶
type ConfigMapVolumeSource struct {
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=253
Name string `json:"name"`
// Items selects keys and their file paths. Unset projects every key as
// a file named after it.
// +optional
// +kubebuilder:validation:MaxItems=64
Items []KeyToPath `json:"items,omitempty"`
}
ConfigMapVolumeSource names a ConfigMap, and optionally which keys to project.
func (*ConfigMapVolumeSource) DeepCopy ¶
func (in *ConfigMapVolumeSource) DeepCopy() *ConfigMapVolumeSource
DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConfigMapVolumeSource.
func (*ConfigMapVolumeSource) DeepCopyInto ¶
func (in *ConfigMapVolumeSource) DeepCopyInto(out *ConfigMapVolumeSource)
DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
type Container ¶
type Container struct {
// Name is the container name, unique within the pod. It must not equal
// the workload's own name, which the main container uses.
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=63
// +kubebuilder:validation:Pattern=`^[a-z0-9]([-a-z0-9]*[a-z0-9])?$`
Name string `json:"name"`
// Image is the sidecar's image, typically third-party
// (gcr.io/cloud-sql-connectors/cloud-sql-proxy:2.14.1).
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=500
Image string `json:"image"`
// Command overrides the image's entrypoint.
// +optional
// +kubebuilder:validation:MaxItems=64
// +kubebuilder:validation:items:MaxLength=4096
Command []string `json:"command,omitempty"`
// Args are the entrypoint's arguments.
// +optional
// +kubebuilder:validation:MaxItems=64
// +kubebuilder:validation:items:MaxLength=4096
Args []string `json:"args,omitempty"`
// Env is the sidecar's environment. Like WorkloadSpec.Env, it has no
// portable entry-count limit; exec byte limits depend on resolved values.
// +optional
// +listType=map
// +listMapKey=name
Env []EnvVar `json:"env,omitempty"`
// Ports are the sidecar's container ports.
// +optional
// +listType=map
// +listMapKey=name
// +kubebuilder:validation:MaxItems=16
Ports []Port `json:"ports,omitempty"`
// Resources is the sidecar's compute request and limit, with the same
// defaults as the main container.
// +optional
Resources Resources `json:"resources,omitempty"`
// Probes are the sidecar's own probes. Unset means none: unlike a
// service, a sidecar gets no implicit TCP probe.
// +optional
Probes *Probes `json:"probes,omitempty"`
// Native renders the sidecar as a Kubernetes native sidecar: an
// initContainer with restartPolicy Always (GA in Kubernetes 1.29). The
// kubelet starts it BEFORE the main container and waits for its startup
// probe to pass, and stops it AFTER the main container exits. A plain
// sidecar starts concurrently with the main container, so a main
// process that dials the sidecar at boot (a database proxy on
// 127.0.0.1) races it and fails until the sidecar is listening. Set
// Probes.StartupPath so "started" means "ready to serve"; without a
// startup probe the kubelet only waits for the process to launch.
// +optional
Native bool `json:"native,omitempty"`
}
Container is a sidecar: a second container in the workload's pod.
It is a CLOSED subset of a Kubernetes container. There is no securityContext field, on purpose: the renderer applies the SAME hardened securityContext to a sidecar as to the main container (non-root, no privilege escalation, all capabilities dropped, read-only root filesystem). A sidecar that could loosen it would make the main container's hardening a suggestion. There are no volume mounts either. A sidecar shares /tmp and nothing else, and one that needs a file gets it through env.
Its ports are CONTAINER ports only. They are never on the workload's Service and are never exposed, because a Service routes to the workload, not to its helpers. They share the pod's network namespace with the main container, so a port name or number is unique across the whole pod.
func (*Container) DeepCopy ¶
DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Container.
func (*Container) DeepCopyInto ¶
DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (Container) EffectiveProbes ¶
EffectiveProbes is a sidecar's probes: the declared ones with defaults filled and Port resolved, or nil.
type DatabaseBackupStatus ¶
type DatabaseBackupStatus struct {
// LastSuccessfulAt is when the last base backup completed. Nil means
// none ever has.
// +optional
LastSuccessfulAt *metav1.Time `json:"lastSuccessfulAt,omitempty"`
// WALArchivingHealthy is whether WAL is currently reaching the object
// store. THIS IS THE SILENT FAILURE: a database whose archiving has
// stopped keeps serving traffic normally, and every minute it does is a
// minute that cannot be recovered to.
// +optional
WALArchivingHealthy bool `json:"walArchivingHealthy,omitempty"`
// EarliestRestorableAt is the oldest point recovery can reach — the
// floor set by the retention policy. Nil until a first backup exists.
// +optional
EarliestRestorableAt *metav1.Time `json:"earliestRestorableAt,omitempty"`
// LastError is the last archiving or backup error, cleared on success.
// It is surfaced verbatim: someone who can see "no bucket permission"
// fixes it, while someone who sees only a false boolean files a ticket.
// +optional
LastError string `json:"lastError,omitempty"`
}
DatabaseBackupStatus answers one question for whoever owns the data: if this database broke right now, what would come back?
THREE INDEPENDENT FACTS, NOT A BOOLEAN. A single "backups: ok" hides all three of the ways this goes wrong:
- A recent base backup with dead WAL archiving restores, but only to whenever that backup was taken.
- Healthy archiving with no base backup restores to NOTHING. WAL is replayed on top of a base backup; an archive alone is not a backup.
- Both healthy, while the retention floor is newer than the incident being investigated, means the moment you want is already gone.
func (*DatabaseBackupStatus) DeepCopy ¶
func (in *DatabaseBackupStatus) DeepCopy() *DatabaseBackupStatus
DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DatabaseBackupStatus.
func (*DatabaseBackupStatus) DeepCopyInto ¶
func (in *DatabaseBackupStatus) DeepCopyInto(out *DatabaseBackupStatus)
DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
type DatabaseCredentialKey ¶
type DatabaseCredentialKey string
DatabaseCredentialKey selects one part of a ManagedDatabase credential.
"uri" IS THE ONE A CONSUMER SHOULD USE, and it is the default. Reassembling a URI from its parts inside a container's env (postgres://$(USERNAME):$(PASSWORD)@...) does not work: Kubernetes $(VAR) substitution resolves only names declared EARLIER in the same env list, so the pod receives a literal "$(PASSWORD)" and the database reports an authentication failure that names the wrong cause.
The values are exactly the keys CloudNativePG publishes in a Cluster's "<cluster>-app" Secret.
+kubebuilder:validation:Enum=uri;host;port;dbname;username;password
const ( DatabaseKeyURI DatabaseCredentialKey = "uri" DatabaseKeyHost DatabaseCredentialKey = "host" DatabaseKeyPort DatabaseCredentialKey = "port" DatabaseKeyDBName DatabaseCredentialKey = "dbname" DatabaseKeyUsername DatabaseCredentialKey = "username" DatabaseKeyPassword DatabaseCredentialKey = "password" )
type DatabaseDashboards ¶ added in v1.0.9
type DatabaseDashboards struct {
// Enabled lets the dashboard role log in. False renders it NOLOGIN.
// +optional
Enabled bool `json:"enabled,omitempty"`
// RowCap is the most rows one panel query returns.
// +optional
// +kubebuilder:default=1000
// +kubebuilder:validation:Minimum=1
// +kubebuilder:validation:Maximum=10000
RowCap int32 `json:"rowCap,omitempty"`
// TimeoutMs is the statement timeout of one panel query, in
// milliseconds.
// +optional
// +kubebuilder:default=5000
// +kubebuilder:validation:Minimum=100
// +kubebuilder:validation:Maximum=10000
TimeoutMs int32 `json:"timeoutMs,omitempty"`
// MaxConcurrency is how many panel queries run at once. The role's
// connection limit is one more than this, so a query that is being
// cancelled never starves the next.
// +optional
// +kubebuilder:default=2
// +kubebuilder:validation:Minimum=1
// +kubebuilder:validation:Maximum=4
MaxConcurrency int32 `json:"maxConcurrency,omitempty"`
}
DatabaseDashboards is the opt-in dashboard role and the limits its queries run under. The limits are enforced by whoever queries as the role (the control plane, pkg/dbquery); the role itself is bounded by Postgres: the column SELECTs the project's migrations grant it (and nothing but read privileges, which the migrator enforces), a connection limit, and a read-only transaction. See pkg/dbgrants.
func (*DatabaseDashboards) DeepCopy ¶ added in v1.0.9
func (in *DatabaseDashboards) DeepCopy() *DatabaseDashboards
DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DatabaseDashboards.
func (*DatabaseDashboards) DeepCopyInto ¶ added in v1.0.9
func (in *DatabaseDashboards) DeepCopyInto(out *DatabaseDashboards)
DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (DatabaseDashboards) Validate ¶ added in v1.0.9
func (d DatabaseDashboards) Validate() error
Validate refuses a limit outside its range rather than clamping it.
func (DatabaseDashboards) WithDefaults ¶ added in v1.0.9
func (d DatabaseDashboards) WithDefaults() DatabaseDashboards
WithDefaults fills every unset limit.
type DatabaseRef ¶
type DatabaseRef struct {
// Name is the ManagedDatabase's name.
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=40
// +kubebuilder:validation:Pattern=`^[a-z0-9]([-a-z0-9]*[a-z0-9])?$`
Name string `json:"name"`
// Key is the credential part. Empty means uri.
// +optional
Key DatabaseCredentialKey `json:"key,omitempty"`
}
DatabaseRef names a ManagedDatabase, in the workload's own namespace, whose credential to project.
func (*DatabaseRef) DeepCopy ¶
func (in *DatabaseRef) DeepCopy() *DatabaseRef
DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DatabaseRef.
func (*DatabaseRef) DeepCopyInto ¶
func (in *DatabaseRef) DeepCopyInto(out *DatabaseRef)
DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (DatabaseRef) EffectiveKey ¶
func (r DatabaseRef) EffectiveKey() DatabaseCredentialKey
EffectiveKey resolves an unset Key to uri.
type DatabaseRestore ¶
type DatabaseRestore struct {
// SourceDatabase is the ManagedDatabase whose backups are read. It may
// be this database's own name (recreating it in place) or another in
// the same environment — never one in another environment, which the
// platform enforces by scoping the object-store prefix, not by
// checking a string here.
// +kubebuilder:validation:MinLength=1
SourceDatabase string `json:"sourceDatabase"`
// PointInTime stops recovery at an RFC3339 timestamp instead of
// replaying every archived WAL. Unset recovers as far as the archive
// goes.
//
// This is the field that makes the difference between "we have
// backups" and "we can undo what happened at 14:05", and it is the
// only reason continuous WAL archiving is worth its cost.
// +optional
PointInTime string `json:"pointInTime,omitempty"`
}
DatabaseRestore is a declarative recovery: which database's backups to read, and optionally how far to replay them.
WHY SourceDatabase IS NOT OPTIONAL. Defaulting it to "this database" would make the common typo — declaring a restore on the database you meant to restore INTO, with the source left blank — silently mean "recover myself from my own backups", which is indistinguishable from a working restore until you look for the data that is missing.
func (*DatabaseRestore) DeepCopy ¶
func (in *DatabaseRestore) DeepCopy() *DatabaseRestore
DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DatabaseRestore.
func (*DatabaseRestore) DeepCopyInto ¶
func (in *DatabaseRestore) DeepCopyInto(out *DatabaseRestore)
DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
type DeletionPolicy ¶
type DeletionPolicy string
DeletionPolicy decides what deleting the resource does to the DATA.
THE DEFAULT IS RETAIN, and it is the most consequential default on this type. A resource is configuration and can be deleted by a kubectl delete, a pruning GitOps sync, a namespace teardown or a typo. None of those are statements about whether the data should continue to exist.
+kubebuilder:validation:Enum=retain;delete
const ( // DeletionPolicyRetain leaves the Cluster and its volumes in place, // unowned. The data survives. DeletionPolicyRetain DeletionPolicy = "retain" // DeletionPolicyDelete deletes the Cluster and therefore its volumes. // This is irreversible, and it is only ever reached because someone // typed it. DeletionPolicyDelete DeletionPolicy = "delete" )
type DeployPhase ¶
type DeployPhase string
DeployPhase places a standalone job (kind job, no Before) in a deploy.
pre-rollout must COMPLETE before any other workload is applied, and a failure aborts the deploy with nothing changed. post-rollout rides with the workloads and is awaited after they roll out. A gating job (Before set) runs as an initContainer, so the pod ordering already IS its phase, and the field is refused there.
+kubebuilder:validation:Enum=pre-rollout;post-rollout
const ( DeployPhasePreRollout DeployPhase = "pre-rollout" DeployPhasePostRollout DeployPhase = "post-rollout" )
type DeploymentStrategy ¶
type DeploymentStrategy string
DeploymentStrategy is how a Deployment replaces its pods on a rollout.
RollingUpdate starts the new pods before the old ones stop, so the workload stays available and two versions of it run at once. Recreate stops every old pod first, so exactly one version ever runs, at the cost of a gap with none.
+kubebuilder:validation:Enum=RollingUpdate;Recreate
const ( StrategyRollingUpdate DeploymentStrategy = "RollingUpdate" StrategyRecreate DeploymentStrategy = "Recreate" )
type EmptyDirVolumeSource ¶
type EmptyDirVolumeSource struct {
// SizeLimitBytes caps the volume. Unset means no cap beyond the node's
// ephemeral storage.
// +optional
// +kubebuilder:validation:Minimum=1
SizeLimitBytes int64 `json:"sizeLimitBytes,omitempty"`
}
EmptyDirVolumeSource is pod-lifetime scratch space.
The size limit is in BYTES, following Resources: a Kubernetes quantity string ("1Gi") is a rendering concern and never appears in a spec.
func (*EmptyDirVolumeSource) DeepCopy ¶
func (in *EmptyDirVolumeSource) DeepCopy() *EmptyDirVolumeSource
DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EmptyDirVolumeSource.
func (*EmptyDirVolumeSource) DeepCopyInto ¶
func (in *EmptyDirVolumeSource) DeepCopyInto(out *EmptyDirVolumeSource)
DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
type Engine ¶
type Engine string
Engine is a database engine. A closed set with one member: the tier provisions CloudNativePG Clusters, and CNPG runs Postgres. The field exists so that adding an engine later widens an enum instead of migrating a resource that already has rows.
+kubebuilder:validation:Enum=postgres
const EnginePostgres Engine = "postgres"
type EnvVar ¶
type EnvVar struct {
// Name is the variable name.
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=253
// +kubebuilder:validation:Pattern=`^[A-Za-z_][A-Za-z0-9_]*$`
Name string `json:"name"`
// Value is an inline literal.
// +optional
// +kubebuilder:validation:MaxLength=32768
Value string `json:"value,omitempty"`
// SecretRef projects one key of a Secret in the workload's namespace.
// +optional
SecretRef *SecretKeyRef `json:"secretRef,omitempty"`
// ManagedSecret names a value in the environment's secret store, and
// whether its absence is tolerated.
// +optional
ManagedSecret *ManagedSecretRef `json:"managedSecret,omitempty"`
// DatabaseRef projects a ManagedDatabase's credential.
// +optional
DatabaseRef *DatabaseRef `json:"databaseRef,omitempty"`
// WorkloadURL is the public URL of another workload in the SAME
// environment — the canonical case is a backend's CORS_ORIGINS naming
// the StaticSite that calls it. It names a workload, never a hostname,
// so it follows the workload when its URL changes.
//
// It is a REFERENCE that must be resolved before a pod can carry it:
// the control plane resolves it on a hosted env (and re-resolves when
// the target's URL moves), and pkg/deploy.Render refuses a spec that
// still holds one rather than render an empty variable.
// +optional
WorkloadURL *WorkloadURLRef `json:"workloadURL,omitempty"`
// ConfigMapRef projects one key of a ConfigMap in the workload's
// namespace. Full profile only.
// +optional
ConfigMapRef *ConfigMapKeyRef `json:"configMapRef,omitempty"`
// FieldRef projects a pod field through the Downward API
// (valueFrom.fieldRef.fieldPath). Full profile only.
// +optional
FieldRef *FieldRef `json:"fieldRef,omitempty"`
}
EnvVar is one container environment variable.
SEVEN CHANNELS, AT MOST ONE SET ¶
MERGE DECISION. forge's tier used its generic EnvVar (value / secret_ref / config_map_ref / field_ref). control-plane's CRD used value / secretRef / a platform-secret name / databaseRef. The merged type has five channels every profile may use, and two that only the Full profile may:
Value — an inline literal. Both sides had it.
SecretRef — one key of a Secret in the workload's own namespace. Both sides had it. Self-hosted, forge's secrets provider renders that Secret. Hosted, the operator additionally refuses names outside the customer's prefix, because the hosted namespace also holds platform-owned secrets. That check is a HOSTED decoration and stays in control-plane.
ManagedSecret — a value stored in the environment's secret STORE, named by a bare logical name. This is control-plane's platform-secret channel, kept on merit and renamed. It is the highest-level way to say "my API key". The author names the secret, never a Kubernetes object, so there is no spelling of the field that reaches another customer's value: scope comes from where the workload runs, never from the string. The concept is not hosted-only. Self-hosted resolves the name through forge's secrets provider for the env, and hosted resolves it through the managed store (OpenBao). Either way the value is materialized into ManagedSecretsSecretName before the pod starts. It is renamed because the old name was the hosted platform's vocabulary, which means nothing to a self-hosted user, while every environment has a secret store.
ManagedSecret.Optional means the materializer SKIPS the name when the store has no value for it, and the pod starts without the variable. That is the projection of an OPTIONAL sensitive config field (a tier's off-switch: "unset means the feature is not registered"), which would otherwise be a hard deploy requirement in every environment that left the feature off. A required name the store lacks fails the materialization, and nothing starts; that is the default because an app reading an unset credential as "" often falls back to an unauthenticated path. The renderer marks the secretKeyRef optional as well, so the pod also starts when the key is absent from the Secret.
DatabaseRef — the credential of a ManagedDatabase in the same namespace. control-plane's design, kept on merit. It removes the most common hand-wiring task in a three-tier app, and it ran live (a backend reached a real CNPG Postgres through it). It NAMES A DATABASE, NEVER A SECRET, so the set of things it can resolve to is closed by construction.
WorkloadURL — the public URL of a sibling workload (a backend's CORS_ORIGINS naming its StaticSite). Added with StaticSite runtime config, which uses the same reference kind. Like DatabaseRef it NAMES A WORKLOAD, never a hostname, so it cannot point outside the env.
ConfigMapRef — one key of a ConfigMap in the workload's namespace. FULL PROFILE ONLY.
FieldRef — a Downward-API field path (metadata.name, status.podIP). FULL PROFILE ONLY.
Why the last two are Full-only: a Workload is one type for every runtime (ADR 0002), so a cluster the author operates must be able to say everything forge.K8sCluster could. A shared hosted namespace is different. ConfigMapRef reads an arbitrary namespace object the hosted user did not write, and FieldRef exposes pod and node placement the platform owns. So Validate(ProfileRestricted) refuses both, by name, through FieldProfiles. That is an explicit allowlist with a default-deny test behind it, not a denylist that has to chase new fields: an unclassified field fails the build.
SecretRef is Full-only by the same table. A raw Secret name in a shared namespace can address a platform-owned Secret, and ManagedSecret is the hosted spelling of "my credential".
Two channels set is REFUSED by Validate rather than resolved by precedence. A spec that says two contradictory things has no correct reading, and guessing one hides the mistake. Kubernetes also rejects an env entry carrying both value and valueFrom, but only server-side, so a client dry-run passes and the deploy dies against the real cluster. Nothing set is legal and means an empty-valued variable.
func (*EnvVar) DeepCopy ¶
DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EnvVar.
func (*EnvVar) DeepCopyInto ¶
DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
type FieldRef ¶
type FieldRef struct {
// FieldPath is the pod field, e.g. metadata.name or status.podIP.
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=253
FieldPath string `json:"fieldPath"`
}
FieldRef selects a pod field for the Downward API. Only the path is modelled. apiVersion is always v1 for the fields a container env can read, so a knob for it would be a field that is either redundant or wrong.
func (*FieldRef) DeepCopy ¶
DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FieldRef.
func (*FieldRef) DeepCopyInto ¶
DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
type Invalidate ¶
type Invalidate string
Invalidate is the per-deploy CDN cache policy.
- entrypoints (DEFAULT) — invalidate only the documents that cannot be content-addressed: the HTML entry points and the runtime config document. Content-hashed assets never need invalidating, so this is O(a few paths) per deploy regardless of site size.
- none — invalidate nothing. Correct for a short TTL or a promotion that did not change the bytes.
- all — purge /*. Correct only for a build that does not hash asset filenames. Invalidation is rate-limited per project, so this is a deliberate choice with a real cost and never a default.
+kubebuilder:validation:Enum=entrypoints;none;all
const ( InvalidateEntrypoints Invalidate = "entrypoints" InvalidateNone Invalidate = "none" InvalidateAll Invalidate = "all" )
type KeyToPath ¶
type KeyToPath struct {
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=253
Key string `json:"key"`
// Path is RELATIVE to the mount and may not contain "..", so a key
// can never be written outside the volume.
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=4096
Path string `json:"path"`
}
KeyToPath projects one key to a file path relative to the mount.
func (*KeyToPath) DeepCopy ¶
DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KeyToPath.
func (*KeyToPath) DeepCopyInto ¶
DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
type ManagedDatabase ¶
type ManagedDatabase struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec ManagedDatabaseSpec `json:"spec,omitempty"`
Status ManagedDatabaseStatus `json:"status,omitempty"`
}
ManagedDatabase is a dedicated Postgres.
+kubebuilder:object:root=true +kubebuilder:subresource:status +kubebuilder:resource:shortName=mdb,categories=forge +kubebuilder:printcolumn:name="Engine",type=string,JSONPath=`.spec.engine` +kubebuilder:printcolumn:name="Instances",type=integer,JSONPath=`.spec.instances` +kubebuilder:printcolumn:name="Phase",type=string,JSONPath=`.status.phase` +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`
func (*ManagedDatabase) DeepCopy ¶
func (in *ManagedDatabase) DeepCopy() *ManagedDatabase
DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManagedDatabase.
func (*ManagedDatabase) DeepCopyInto ¶
func (in *ManagedDatabase) DeepCopyInto(out *ManagedDatabase)
DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (*ManagedDatabase) DeepCopyObject ¶
func (in *ManagedDatabase) DeepCopyObject() runtime.Object
DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
type ManagedDatabaseList ¶
type ManagedDatabaseList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []ManagedDatabase `json:"items"`
}
ManagedDatabaseList contains a list of ManagedDatabase.
+kubebuilder:object:root=true
func (*ManagedDatabaseList) DeepCopy ¶
func (in *ManagedDatabaseList) DeepCopy() *ManagedDatabaseList
DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManagedDatabaseList.
func (*ManagedDatabaseList) DeepCopyInto ¶
func (in *ManagedDatabaseList) DeepCopyInto(out *ManagedDatabaseList)
DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (*ManagedDatabaseList) DeepCopyObject ¶
func (in *ManagedDatabaseList) DeepCopyObject() runtime.Object
DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
type ManagedDatabaseSpec ¶
type ManagedDatabaseSpec struct {
// Engine is the database engine. Default postgres.
// +optional
// +kubebuilder:default=postgres
Engine Engine `json:"engine,omitempty"`
// Instances is how many Postgres instances run: one primary plus
// (Instances-1) hot standbys. Default 1, ceiling MaxDatabaseInstances.
// A standby buys failover, not durability.
// +optional
// +kubebuilder:default=1
// +kubebuilder:validation:Minimum=1
// +kubebuilder:validation:Maximum=3
Instances int32 `json:"instances,omitempty"`
// StorageGiB sizes EACH instance's volume, so the cluster provisions
// Instances × StorageGiB. Default DefaultDatabaseStorageGiB.
// +optional
// +kubebuilder:default=10
// +kubebuilder:validation:Minimum=1
// +kubebuilder:validation:Maximum=500
StorageGiB int32 `json:"storageGiB,omitempty"`
// DeletionPolicy is what deleting this resource does to the data.
// Default retain.
// +optional
// +kubebuilder:default=retain
DeletionPolicy DeletionPolicy `json:"deletionPolicy,omitempty"`
// Restore, when set, bootstraps this database from a backup instead of
// from an empty initdb. DECLARING it is the whole restore procedure:
// there is no imperative RPC, because a restore that only exists as a
// command is one nobody can review, repeat, or run at 3am from a git
// revert.
//
// It is honoured ONCE, at bootstrap. A Cluster that already holds data
// is never re-bootstrapped, so adding this to a live database does
// nothing — restoring over a running database means declaring a NEW
// one that names the old as its source, which is also what keeps a
// mistaken restore from destroying the original.
// +optional
Restore *DatabaseRestore `json:"restore,omitempty"`
// Dashboards opts the database into live dashboard panels: it lets the
// dashboard role (forge_dash) log in. The role reads only what the
// project's migrations GRANT it. Nil means disabled, and a disabled
// database renders the role NOLOGIN with no password, so declaring
// nothing never leaves a login behind.
// +optional
Dashboards *DatabaseDashboards `json:"dashboards,omitempty"`
}
ManagedDatabaseSpec is a dedicated Postgres, rendered as a CloudNativePG Cluster in the workload's own namespace.
MERGE DECISIONS. control-plane's type wins, because forge had no schema. It ran live (the `threetier` database, Ready, backing a backend through DatabaseRef). Two changes on merit:
- StorageGiB, not control-plane's StorageGB. The value was always rendered as "<n>Gi", so the field name claimed decimal gigabytes while the volume was sized in binary GiB. The units now agree with Workload.StorageGiB and with what is actually provisioned.
- OrgID, EnvironmentID, DeploymentID and Namespace are gone from the spec: identity is labels and the namespace is the target. The database's name is metadata.name. control-plane's DatabaseName field duplicated it, and the two could disagree.
SCALARS AND CLOSED ENUMS ONLY. There is no postgresql.conf passthrough, no extension list, no version selector, no backup stanza and no raw CNPG object. A field the tier would have to REJECT is a field it does not declare.
func (*ManagedDatabaseSpec) DeepCopy ¶
func (in *ManagedDatabaseSpec) DeepCopy() *ManagedDatabaseSpec
DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManagedDatabaseSpec.
func (*ManagedDatabaseSpec) DeepCopyInto ¶
func (in *ManagedDatabaseSpec) DeepCopyInto(out *ManagedDatabaseSpec)
DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (ManagedDatabaseSpec) Validate ¶
func (s ManagedDatabaseSpec) Validate() error
Validate checks a ManagedDatabase spec. Out-of-range values are REFUSED, not clamped (see WithDefaults).
func (ManagedDatabaseSpec) WithDefaults ¶
func (s ManagedDatabaseSpec) WithDefaults() ManagedDatabaseSpec
WithDefaults fills every unset field. Out-of-range values are left for Validate to refuse rather than silently clamped. control-plane clamped them, which meant a declaration of 50 instances deployed as 3 with no error. A declaration the platform cannot honour should fail, not quietly shrink.
type ManagedDatabaseStatus ¶
type ManagedDatabaseStatus struct {
// +optional
Phase Phase `json:"phase,omitempty"`
// +optional
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
// ClusterName, DatabaseName and RoleName are the identifiers actually
// created. They are RECORDED rather than re-derived, so that a later
// change to the naming rule cannot re-point a name that already holds
// data.
// +optional
ClusterName string `json:"clusterName,omitempty"`
// +optional
DatabaseName string `json:"databaseName,omitempty"`
// +optional
RoleName string `json:"roleName,omitempty"`
// SecretName is the credential Secret CNPG published, in the Cluster's
// namespace.
// +optional
SecretName string `json:"secretName,omitempty"`
// EngineVersion is the Postgres version CNPG reports running. It is
// read back rather than declared, because the version is a platform
// decision.
// +optional
EngineVersion string `json:"engineVersion,omitempty"`
// ReadyAt is nil until the Cluster has been healthy, with a published
// credential, at least once.
// +optional
ReadyAt *metav1.Time `json:"readyAt,omitempty"`
// +optional
Message string `json:"message,omitempty"`
// Backup is what the executor observed about this database's backups,
// nil when the environment configures no object store.
//
// NIL AND "UNHEALTHY" ARE DIFFERENT ANSWERS, which is why this is a
// pointer rather than a value. A database nobody is backing up by
// design must not report the same state as one whose archiving has
// broken.
// +optional
Backup *DatabaseBackupStatus `json:"backup,omitempty"`
// +listType=map
// +listMapKey=type
// +optional
Conditions []metav1.Condition `json:"conditions,omitempty"`
}
ManagedDatabaseStatus is the observed state of a ManagedDatabase.
func (*ManagedDatabaseStatus) DeepCopy ¶
func (in *ManagedDatabaseStatus) DeepCopy() *ManagedDatabaseStatus
DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManagedDatabaseStatus.
func (*ManagedDatabaseStatus) DeepCopyInto ¶
func (in *ManagedDatabaseStatus) DeepCopyInto(out *ManagedDatabaseStatus)
DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
type ManagedSecretRef ¶
type ManagedSecretRef struct {
// Name is the secret's logical name. A BARE LOGICAL NAME: env-var shaped,
// with no separator to traverse, carrying no customer or environment
// component, so no spelling of it reaches another customer's value.
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=253
// +kubebuilder:validation:Pattern=`^[A-Za-z_][A-Za-z0-9_]*$`
Name string `json:"name"`
// Optional tolerates the store having no value: the materializer skips
// the name, and the pod starts without the variable. Default false: a
// missing value fails the materialization and nothing starts.
// +optional
Optional bool `json:"optional,omitempty"`
}
ManagedSecretRef names one value in the environment's secret store.
func (*ManagedSecretRef) DeepCopy ¶
func (in *ManagedSecretRef) DeepCopy() *ManagedSecretRef
DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManagedSecretRef.
func (*ManagedSecretRef) DeepCopyInto ¶
func (in *ManagedSecretRef) DeepCopyInto(out *ManagedSecretRef)
DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
type PVCVolumeSource ¶
type PVCVolumeSource struct {
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=253
ClaimName string `json:"claimName"`
}
PVCVolumeSource names an existing PersistentVolumeClaim in the workload's namespace.
func (*PVCVolumeSource) DeepCopy ¶
func (in *PVCVolumeSource) DeepCopy() *PVCVolumeSource
DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PVCVolumeSource.
func (*PVCVolumeSource) DeepCopyInto ¶
func (in *PVCVolumeSource) DeepCopyInto(out *PVCVolumeSource)
DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
type Phase ¶
type Phase string
Phase is a tier's observed lifecycle phase.
ONE VOCABULARY FOR ALL THREE TIERS. control-plane had three: SimpleBackend used Pending/Ready/Degraded/Failed, StaticSite used Pending/Syncing/Ready/Failed, and ManagedDatabase used Pending/Provisioning/Ready/Locked/Failed. Syncing and Provisioning are the same fact, "work is in flight and nothing is wrong yet", so they fold into Progressing. That is also the word the deployments ledger's observed_state column already uses. One vocabulary is what lets deploy.ObservedStateOf map a phase without first knowing which kind reported it.
- Pending — declared, not yet acted on. Nothing serves.
- Progressing — applied, converging: a rollout, a sync, a Cluster provisioning.
- Ready — serving what the spec declares.
- Degraded — exists but is not serving: a crash loop, a failing probe. The platform has done its part and the fault is inside the workload.
- Failed — the platform REFUSED or could not converge: a rejected image, a cross-customer reference. Nothing was applied.
- Locked — ManagedDatabase only: a retained database whose CR was deleted. Every byte still exists, but nothing names it any more.
+kubebuilder:validation:Enum=Pending;Progressing;Ready;Degraded;Failed;Locked
type PodSecurity ¶
type PodSecurity struct {
// RunAsUser is the uid every container runs as. Must not be 0:
// runAsNonRoot is always true.
// +optional
// +kubebuilder:validation:Minimum=1
RunAsUser *int64 `json:"runAsUser,omitempty"`
// RunAsGroup is the primary gid. Must not be 0.
// +optional
// +kubebuilder:validation:Minimum=1
RunAsGroup *int64 `json:"runAsGroup,omitempty"`
// FSGroup owns mounted volumes. Must not be 0.
// +optional
// +kubebuilder:validation:Minimum=1
FSGroup *int64 `json:"fsGroup,omitempty"`
// ReadOnlyRootFilesystem false gives the containers a writable root
// filesystem. Default true.
// +optional
ReadOnlyRootFilesystem *bool `json:"readOnlyRootFilesystem,omitempty"`
}
PodSecurity is the closed set of pod-identity knobs forge lets a workload change. Each unset field keeps forge's default (uid/gid 65532, fsGroup only when a volume needs it, a read-only root filesystem). The overrides apply to the pod AND to every container forge renders in it (main, sidecar, gating initContainer), so the pod has one identity.
func (*PodSecurity) DeepCopy ¶
func (in *PodSecurity) DeepCopy() *PodSecurity
DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PodSecurity.
func (*PodSecurity) DeepCopyInto ¶
func (in *PodSecurity) DeepCopyInto(out *PodSecurity)
DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
type PolicyRule ¶
type PolicyRule struct {
// APIGroups are the API groups; "" is the core group.
// +kubebuilder:validation:MinItems=1
APIGroups []string `json:"apiGroups"`
// Resources are the resource names (plural, lowercase), optionally with
// a subresource ("pods/log").
// +kubebuilder:validation:MinItems=1
Resources []string `json:"resources"`
// Verbs are the allowed verbs, or "*".
// +kubebuilder:validation:MinItems=1
Verbs []string `json:"verbs"`
// ResourceNames narrows the rule to named objects.
// +optional
ResourceNames []string `json:"resourceNames,omitempty"`
}
PolicyRule is one RBAC rule: verbs on resources in API groups.
A LOCAL MIRROR of rbac/v1 PolicyRule, not the upstream type, for two reasons. The spec schema is closed and self-contained, so the KCL generator refuses a reference that leaves this package. And a mirror keeps the surface an allowlist: nonResourceURLs (meaningful only in a ClusterRole, and granting access to raw API server paths) and any future upstream field are unreachable until someone adds them here on purpose.
func (*PolicyRule) DeepCopy ¶
func (in *PolicyRule) DeepCopy() *PolicyRule
DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PolicyRule.
func (*PolicyRule) DeepCopyInto ¶
func (in *PolicyRule) DeepCopyInto(out *PolicyRule)
DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
type Port ¶
type Port struct {
// Name is the port's name, referenced by Services and probes. An
// IANA_SVC_NAME, as Kubernetes requires of a container port name: at
// most 15 characters, lowercase alphanumerics and '-'.
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=15
// +kubebuilder:validation:Pattern=`^[a-z0-9]([-a-z0-9]*[a-z0-9])?$`
Name string `json:"name"`
// Port is the container port number.
// +kubebuilder:validation:Minimum=1
// +kubebuilder:validation:Maximum=65535
Port int32 `json:"port"`
// Protocol is the transport. Default tcp.
// +optional
// +kubebuilder:default=tcp
Protocol PortProtocol `json:"protocol,omitempty"`
// AppProtocol is the application protocol the port speaks (h2c, grpc,
// http, https, ws), set as the Service port's appProtocol so a gateway
// or mesh can route it correctly. It changes no pod behaviour.
// +optional
// +kubebuilder:validation:MaxLength=63
// +kubebuilder:validation:Pattern=`^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?[a-z0-9]([-a-z0-9.]*[a-z0-9])?$`
AppProtocol string `json:"appProtocol,omitempty"`
// Expose asks the environment to route a PUBLIC hostname to this port:
// the gateway when self-hosted, the platform's allocated hostname on
// hosted. Only a service can expose a port, because only a service
// has a Service object to route to. At most one port per workload is
// exposed, because status reports one hostname.
// +optional
Expose bool `json:"expose,omitempty"`
// Domains are CUSTOM hostnames served IN ADDITION to the default
// hostname. They are only meaningful on the exposed port. The default
// hostname needs no declaration: hosted allocates a collision-safe one
// and self-hosted derives one from the env's gateway, so a public
// workload never has to invent a domain. Hosted verifies ownership of a
// custom domain before serving it, because on shared infrastructure an
// unverified hostname is a takeover vector. Self-hosted trusts it.
// +optional
// +kubebuilder:validation:MaxItems=8
Domains []string `json:"domains,omitempty"`
}
Port is one named port a workload listens on.
func (*Port) DeepCopy ¶
DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Port.
func (*Port) DeepCopyInto ¶
DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
type PortProtocol ¶
type PortProtocol string
PortProtocol is a port's transport. Lowercase because it is an authoring value (fw.Port always spelled it so), and the renderer upper-cases it for Kubernetes.
+kubebuilder:validation:Enum=tcp;udp
const ( ProtocolTCP PortProtocol = "tcp" ProtocolUDP PortProtocol = "udp" )
type ProbeTimings ¶
type ProbeTimings struct {
InitialDelaySeconds int32
PeriodSeconds int32
TimeoutSeconds int32
FailureThreshold int32
}
ProbeTimings are one probe's timings, as rendered.
func (*ProbeTimings) DeepCopy ¶
func (in *ProbeTimings) DeepCopy() *ProbeTimings
DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProbeTimings.
func (*ProbeTimings) DeepCopyInto ¶
func (in *ProbeTimings) DeepCopyInto(out *ProbeTimings)
DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
type Probes ¶
type Probes struct {
// Port is the container port probed. Unset means the port named http,
// or the first declared port (see WorkloadSpec.ProbePort).
// +optional
// +kubebuilder:validation:Minimum=1
// +kubebuilder:validation:Maximum=65535
Port int32 `json:"port,omitempty"`
// ReadinessPath is the HTTP readiness path. Unset means /readyz.
// +optional
// +kubebuilder:validation:MaxLength=2048
// +kubebuilder:validation:Pattern=`^/.*$`
ReadinessPath string `json:"readinessPath,omitempty"`
// LivenessPath is the HTTP liveness path. Unset means /healthz.
// +optional
// +kubebuilder:validation:MaxLength=2048
// +kubebuilder:validation:Pattern=`^/.*$`
LivenessPath string `json:"livenessPath,omitempty"`
// StartupPath is the HTTP startup-probe path. Unset means no startup
// probe. The kubelet holds off readiness and liveness until it passes,
// and a NATIVE sidecar (Container.Native) is only considered started,
// so the main container only launches, once it does. Unlike the other
// two paths it has no default: only the container knows what "started"
// means.
// +optional
// +kubebuilder:validation:MaxLength=2048
// +kubebuilder:validation:Pattern=`^/.*$`
StartupPath string `json:"startupPath,omitempty"`
// TCP probes with a TCP connect instead of an HTTP GET. A TCP probe
// sets no paths.
// +optional
TCP bool `json:"tcp,omitempty"`
// InitialDelaySeconds is the READINESS initial delay. Default 0:
// readiness is cheap and never restarts, so there is nothing to wait
// for.
// +optional
// +kubebuilder:default=0
// +kubebuilder:validation:Minimum=0
InitialDelaySeconds int32 `json:"initialDelaySeconds,omitempty"`
// PeriodSeconds is the READINESS period. Default 5.
// +optional
// +kubebuilder:default=5
// +kubebuilder:validation:Minimum=1
PeriodSeconds int32 `json:"periodSeconds,omitempty"`
// TimeoutSeconds is the per-attempt timeout, for both probes. Default 3,
// NOT 2. serverkit's readiness handler has a 2s deadline of its own, so a
// 2s probe timeout would race it and report the handler's slow-but-correct
// "not ready" as a probe timeout.
// +optional
// +kubebuilder:default=3
// +kubebuilder:validation:Minimum=1
TimeoutSeconds int32 `json:"timeoutSeconds,omitempty"`
// FailureThreshold is how many consecutive READINESS failures take a
// replica out of the endpoints. Default 3.
// +optional
// +kubebuilder:default=3
// +kubebuilder:validation:Minimum=1
FailureThreshold int32 `json:"failureThreshold,omitempty"`
}
Probes are a workload's readiness and liveness probes, declared ONCE.
WHY ONE DECLARATION DRIVES TWO DIFFERENT PROBES ¶
The two probes answer different questions, and they must not share timings. Readiness gates TRAFFIC. It is fast, so a wedged replica leaves the Service endpoints within one budget, and it never restarts anything, so a dependency outage degrades the service instead of crash-looping the fleet. Liveness RESTARTS. Its timings are DERIVED from the readiness budget by Liveness(), so that it can never race a cold start however readiness is tuned. The derivation is the only one there is: liveness timings are not separately settable, because every independent knob forge has shipped for them was eventually set shorter than readiness and restart-looped a slow start.
This replaced HealthCheck, whose ONE probe fed both liveness and readiness. That shape is how a hosted API shipped with no probes (the field was optional and had no default) and how, when one was set, a slow dependency restarted pods that only needed to stop taking traffic.
An HTTP probe GETs ReadinessPath and LivenessPath (default /readyz and /healthz, serverkit's split). TCP probes with a connect instead, which is correct for a workload that serves a non-HTTP protocol. Probing "/" by default would restart-loop such a workload and make the loop look like an application fault.
func (*Probes) DeepCopy ¶
DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Probes.
func (*Probes) DeepCopyInto ¶
DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (Probes) Liveness ¶
func (p Probes) Liveness() ProbeTimings
Liveness returns the liveness timings, DERIVED from the readiness budget: the initial delay is the readiness initial delay plus the whole readiness failure window (period × failures) plus LivenessMarginSeconds, and the period is twice the readiness period. Timeout and failure threshold are shared. This is the one place the arithmetic lives, and renderers call it rather than restating it.
func (Probes) Readiness ¶
func (p Probes) Readiness() ProbeTimings
Readiness returns the (defaulted) readiness timings.
func (Probes) WithDefaults ¶
WithDefaults fills every unset timing, and fills the paths of an HTTP probe. A TCP probe's paths stay empty.
type Profile ¶
type Profile int
Profile is what a DESTINATION allows a Workload to say (ADR 0002 §3).
It is a property of where the workload runs, never of the workload. The hosted control plane picks it server-side from the destination's isolation. The CLI runs the same check at render time only so the author sees the error before publishing. A client-chosen profile would be a hosted user choosing their own sandbox.
Profiles are ORDERED from most to least permissive. A field classified at profile X is allowed under X and every more permissive profile, which is what Permits encodes.
const ( // ProfileFull allows every field: a cluster the author operates, and // (later) a hosted user's own vcluster. ProfileFull Profile = iota // ProfileRestricted is the hosted runtime on shared nodes. It allows // what reaches only the hosted user's own pod, and refuses anything that // reaches the Kubernetes API, another object in the namespace, or // platform-owned identity. ProfileRestricted )
type Resources ¶
type Resources struct {
// +optional
// +kubebuilder:default=250
// +kubebuilder:validation:Minimum=1
CPURequestMillicores int64 `json:"cpuRequestMillicores,omitempty"`
// Unset means equal to the CPU request.
// +optional
// +kubebuilder:validation:Minimum=1
CPULimitMillicores int64 `json:"cpuLimitMillicores,omitempty"`
// +optional
// +kubebuilder:default=1073741824
// +kubebuilder:validation:Minimum=1
MemoryRequestBytes int64 `json:"memoryRequestBytes,omitempty"`
// Unset means equal to the memory request.
// +optional
// +kubebuilder:validation:Minimum=1
MemoryLimitBytes int64 `json:"memoryLimitBytes,omitempty"`
}
Resources is a workload's compute request and limit, in NEUTRAL units: CPU in millicores, memory in BYTES.
MERGE DECISION: the units and the shape were already shared (forge's KCL and control-plane's CRD agreed on four independent ints, millicores and bytes), so both sides win. Bytes, not MiB. MiB appeared only in control-plane's deployrender, as a third unit system inside one product. Bytes are Kubernetes' own base unit and the only memory unit with no dialect (Mi is binary, M is decimal). Kubernetes quantity strings ("2Gi") are a rendering concern and never appear in a spec. They are a metering input, and re-parsing a dialect is where a billing number quietly picks up a rounding rule nobody chose.
The 4 GiB-per-vCPU shape band is NOT a rule of this type. It is a BILLING rule and applies to hosted environments only (owner decision). It lives in deploy.CheckShapeBand, which the hosted path calls, and it is not imposed on every self-hosted user through the schema. Validate enforces only what is true everywhere: requests are positive and limits are at least the request.
The defaults are forge's 250m / 1 GiB entry shape for an unset REQUEST, and the (defaulted) request for an unset LIMIT. The shape is conformant with the band, so an app that omits resources deploys to either destination unchanged, and "set a request, leave the limit" deploys with limits equal to the requests.
The LIMITS deliberately carry no +kubebuilder:default marker. "Equal to the request" is not a static value. A static 250m default would be stamped by the API server under a 500m request, and Validate would then refuse the CR the operator was handed. The generated KCL has no default for the limits either, for the same reason. So the absence travels through the CR and the KCL record, and WithDefaults resolves it wherever the spec is consumed.
func (*Resources) DeepCopy ¶
DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Resources.
func (*Resources) DeepCopyInto ¶
DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (Resources) Validate ¶
Validate checks the request/limit pairs. Zero means "use the default" and is valid. Negative values and a limit below its request are refused: Kubernetes itself rejects the latter at admission, and control-plane silently RAISED the limit, which hid the authoring mistake.
func (Resources) WithDefaults ¶
WithDefaults fills every unset value. Requests first, to the entry shape. Then each unset limit is set to its RESOLVED request, so a request-only spec gets limit == request rather than a static limit below it. A limit that IS set is left alone, and Validate refuses it if it is below its request.
type RuntimeConfigValue ¶
type RuntimeConfigValue struct {
// Value is an inline literal.
// +optional
// +kubebuilder:validation:MaxLength=32768
Value *string `json:"value,omitempty"`
// WorkloadURL resolves to another workload's public URL.
// +optional
WorkloadURL *WorkloadURLRef `json:"workloadURL,omitempty"`
}
RuntimeConfigValue is one entry of a StaticSite's runtime config: EXACTLY ONE of a literal value or a workload URL reference.
Value is a pointer because an explicitly empty string is a legal value (the conventional "feature off" sentinel) and must not read as "unset". Every value is a STRING on the wire. A bundle that wants a number or a boolean coerces it (forge's generated config module does), so the document has one type rule no matter which side resolved it.
+kubebuilder:validation:ExactlyOneOf=value;workloadURL
func (*RuntimeConfigValue) DeepCopy ¶
func (in *RuntimeConfigValue) DeepCopy() *RuntimeConfigValue
DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RuntimeConfigValue.
func (*RuntimeConfigValue) DeepCopyInto ¶
func (in *RuntimeConfigValue) DeepCopyInto(out *RuntimeConfigValue)
DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
type SecretKeyRef ¶
type SecretKeyRef struct {
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=253
Name string `json:"name"`
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=253
Key string `json:"key"`
// Optional lets the pod start when the Secret or the key does not exist;
// the variable is then simply unset. It is what the config projection
// emits for an OPTIONAL sensitive field, whose absence the app already
// handles. A required reference to a missing key holds the pod in
// CreateContainerConfigError, with no application log, which is the
// right outcome only when the value really is required.
// +optional
Optional bool `json:"optional,omitempty"`
}
SecretKeyRef names one key of a Secret in the workload's own namespace.
func (*SecretKeyRef) DeepCopy ¶
func (in *SecretKeyRef) DeepCopy() *SecretKeyRef
DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretKeyRef.
func (*SecretKeyRef) DeepCopyInto ¶
func (in *SecretKeyRef) DeepCopyInto(out *SecretKeyRef)
DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
type SecretVolumeSource ¶
type SecretVolumeSource struct {
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=253
Name string `json:"name"`
// Items selects keys and their file paths. Unset projects every key as
// a file named after it.
// +optional
// +kubebuilder:validation:MaxItems=64
Items []KeyToPath `json:"items,omitempty"`
// Optional lets the pod start while the Secret does not exist yet; the
// kubelet fills the volume in once it appears. A Secret the control plane
// delivers after the first deploy (forge.ControlPlaneSecret) is mounted
// this way, so a first deploy never waits on it.
// +optional
Optional bool `json:"optional,omitempty"`
}
SecretVolumeSource names a Secret, and optionally which keys to project.
func (*SecretVolumeSource) DeepCopy ¶
func (in *SecretVolumeSource) DeepCopy() *SecretVolumeSource
DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretVolumeSource.
func (*SecretVolumeSource) DeepCopyInto ¶
func (in *SecretVolumeSource) DeepCopyInto(out *SecretVolumeSource)
DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
type StaticSite ¶
type StaticSite struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec StaticSiteSpec `json:"spec"`
Status StaticSiteStatus `json:"status,omitempty"`
}
StaticSite is a static site served from object storage.
+kubebuilder:object:root=true +kubebuilder:subresource:status +kubebuilder:resource:shortName=ss,categories=forge +kubebuilder:printcolumn:name="Phase",type=string,JSONPath=`.status.phase` +kubebuilder:printcolumn:name="Hostname",type=string,JSONPath=`.status.hostname` +kubebuilder:printcolumn:name="Live",type=string,JSONPath=`.status.liveDigest` +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`
func (*StaticSite) DeepCopy ¶
func (in *StaticSite) DeepCopy() *StaticSite
DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StaticSite.
func (*StaticSite) DeepCopyInto ¶
func (in *StaticSite) DeepCopyInto(out *StaticSite)
DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (*StaticSite) DeepCopyObject ¶
func (in *StaticSite) DeepCopyObject() runtime.Object
DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
type StaticSiteCDN ¶
type StaticSiteCDN struct {
// +kubebuilder:validation:MinLength=1
URLMap string `json:"urlMap"`
// Invalidate is the per-deploy cache policy. Empty means entrypoints.
// The zero value MUST NOT mean all: an unset field is exactly the case
// where the quota-expensive choice is least likely to have been
// considered.
// +optional
// +kubebuilder:default=entrypoints
Invalidate Invalidate `json:"invalidate,omitempty"`
// ExtraInvalidatePaths adds absolute site paths to the entrypoints set,
// for documents the planner cannot infer (a bare /manifest.json, a
// service worker). Ignored under none and all.
// +optional
ExtraInvalidatePaths []string `json:"extraInvalidatePaths,omitempty"`
}
StaticSiteCDN is the CDN in front of the site's bucket, and what a deploy does to the edge cache afterwards. Neither side creates the CDN: URLMap names an EXISTING load-balancer URL map whose backend is the bucket.
func (*StaticSiteCDN) DeepCopy ¶
func (in *StaticSiteCDN) DeepCopy() *StaticSiteCDN
DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StaticSiteCDN.
func (*StaticSiteCDN) DeepCopyInto ¶
func (in *StaticSiteCDN) DeepCopyInto(out *StaticSiteCDN)
DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (StaticSiteCDN) EffectiveInvalidate ¶
func (c StaticSiteCDN) EffectiveInvalidate() Invalidate
EffectiveInvalidate resolves an unset policy to entrypoints.
type StaticSiteList ¶
type StaticSiteList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []StaticSite `json:"items"`
}
StaticSiteList contains a list of StaticSite.
+kubebuilder:object:root=true
func (*StaticSiteList) DeepCopy ¶
func (in *StaticSiteList) DeepCopy() *StaticSiteList
DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StaticSiteList.
func (*StaticSiteList) DeepCopyInto ¶
func (in *StaticSiteList) DeepCopyInto(out *StaticSiteList)
DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (*StaticSiteList) DeepCopyObject ¶
func (in *StaticSiteList) DeepCopyObject() runtime.Object
DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
type StaticSiteSpec ¶
type StaticSiteSpec struct {
// Bucket is the bucket the site's prefixes live in: gs://name or a bare
// name. Self-hosted, the user's own bucket. Hosted, the platform assigns
// a shared bucket and a per-customer prefix and ignores this value.
// +optional
// +kubebuilder:validation:MaxLength=222
Bucket string `json:"bucket,omitempty"`
// BasePath mounts the site under a sub-path ("/admin"). Empty is the
// root.
// +optional
// +kubebuilder:validation:Pattern=`^/.*$`
BasePath string `json:"basePath,omitempty"`
// AssetDir is the build's CONTENT-HASHED asset directory, relative to
// the site's mount (BasePath): "assets" for a Vite build, "_next/static"
// for a Next.js export. Every file under it is named for its bytes.
//
// It is a SERVING fact, not a build input, because two serving
// guarantees hang off it and neither can be inferred from a bucket:
//
// - CACHE POLICY. Files under it may be cached forever
// (`public, max-age=31536000, immutable`); everything else — the
// entry document, config.js, version.json — must revalidate
// (`no-cache`), so a deploy is visible on the next load.
// - STALE TABS KEEP WORKING. A page loaded before a deploy lazy-loads
// chunks the new release no longer contains. The server keeps the
// previous releases' files under this directory serveable, so that
// import succeeds instead of failing on a 404 (or, worse, on an HTML
// fallback parsed as JavaScript).
//
// Empty means the build declares none: no immutable caching and no
// carried-forward assets.
// +optional
// +kubebuilder:validation:MaxLength=255
AssetDir string `json:"assetDir,omitempty"`
// ReleaseRepository is the OCI repository LiveDigest was pushed to —
// the whole reference, registry host included, with no tag or digest
// on it. Together with LiveDigest it is the complete address of the
// release: `<releaseRepository>@<liveDigest>`.
//
// IT IS RECORDED, NOT RECONSTRUCTED, and that is the whole point of
// the field. A site's release goes to the frontend's OWN declared
// image reference plus the platform's layout segment
// (deploytarget.HostedStaticRepository), which is a reference only the
// builder knows: it may be an org's registry subtree, or a registry
// the platform does not host at all. Anything that recomposed a path
// from a registry base, an org id and a site name would be writing
// down a second, independent derivation of where the bytes are — and
// when the two disagree, the puller 404s on an artifact that exists.
// That is exactly the defect this field closes, and it is the same
// rule a workload's image already follows: the reference carries its
// registry, and the release ledger keys artifacts by full reference.
//
// WHERE a caller may push is therefore a platform policy enforced as
// ADMISSION on this recorded value — a reference inside the platform's
// own registry must be inside that org's subtree — never by deriving
// the value and calling the derivation a boundary.
//
// Empty before the first deploy, like LiveDigest. A spec carrying a
// digest but no repository is not pullable, and whoever executes it
// must refuse rather than guess a path.
// +optional
// +kubebuilder:validation:MaxLength=512
ReleaseRepository string `json:"releaseRepository,omitempty"`
// LiveDigest is the release the live prefix must serve. A digest, never
// a tag: a tag is a mutable pointer, and re-pointing one would void
// "the bytes that passed staging are the bytes in prod". Empty before
// the first deploy. Addressed within ReleaseRepository.
// +optional
// +kubebuilder:validation:Pattern=`^sha256:[a-f0-9]{64}$`
LiveDigest string `json:"liveDigest,omitempty"`
// PreviousDigest is the release that was live before LiveDigest.
// Retention never deletes it: pages loaded from it moments ago are still
// fetching its hashed assets.
// +optional
// +kubebuilder:validation:Pattern=`^sha256:[a-f0-9]{64}$`
PreviousDigest string `json:"previousDigest,omitempty"`
// RetainedDigests are releases that MUST survive retention (every digest
// an environment is promoted to, plus any pin), projected from the
// release ledger. A retention planner that cannot resolve its protected
// set deletes NOTHING. It never reads "I know of nothing to protect" as
// "nothing needs protecting".
// +optional
RetainedDigests []string `json:"retainedDigests,omitempty"`
// KeepReleases is how many archived releases to retain, newest first.
// Unset means DefaultKeepReleases. An explicit 0 retains everything.
// Otherwise it must be at least MinKeepReleases, because keeping fewer
// than the live release plus its predecessor would delete assets pages
// loaded moments ago still fetch.
//
// A POINTER because there are three states. control-plane's plain
// omitempty int32 made "unset" and "retain everything" the same value,
// so a CR that omitted the field silently disabled retention. forge's
// KCL defaulted to 10. Both are honoured now: nil is the default, and
// 0 is a deliberate choice.
// +optional
// +kubebuilder:validation:Minimum=0
KeepReleases *int32 `json:"keepReleases,omitempty"`
// Entrypoints are the site-absolute paths that cannot be
// content-addressed and are invalidated on every deploy. Empty uses the
// planner's default set.
// +optional
Entrypoints []string `json:"entrypoints,omitempty"`
// CDN is the CDN in front of the bucket. Omitted means a bucket served
// directly, with no invalidation.
// +optional
CDN *StaticSiteCDN `json:"cdn,omitempty"`
// Domains are custom hostnames, in addition to the default one.
// +optional
// +kubebuilder:validation:MaxItems=8
Domains []string `json:"domains,omitempty"`
// RuntimeConfig is the site's runtime configuration document: the
// object the browser receives as window.__FORGE_CONFIG__, keyed by the
// name the bundle reads (API_URL, OIDC_ISSUER, ...).
//
// It is SPEC, not a file inside the release, and that is the point.
// A release artifact that carried its own config.js would be frozen to
// the environment it was built for, and promoting it would ship
// staging's backend URL to production. Keeping the document here makes
// the artifact environment-agnostic: whoever executes the spec writes
// `live/<basePath>/config.js` AFTER syncing live/ from the release, on
// every sync (a sync prunes files the release does not contain), as
//
// window.__FORGE_CONFIG__ = {"KEY": "resolved value", ...};
//
// (pkg/deploy.RuntimeConfigJS renders exactly that document.) An entry
// is either a literal or a reference to another workload's URL, which
// is resolved where the URL is known: by the control plane for a
// hosted env, at render time by forge otherwise. See RuntimeConfigValue.
// +optional
// +kubebuilder:validation:MaxProperties=128
RuntimeConfig map[string]RuntimeConfigValue `json:"runtimeConfig,omitempty"`
}
StaticSiteSpec is a site served from an object-storage bucket, optionally behind a CDN.
WHAT A DEPLOY IS ¶
Both sides already implemented the same model, and it is kept. An assembled tree is archived IMMUTABLY under releases/<digest>/, and the live/ prefix is synced from one release. Deploy and promotion are therefore the same mechanical move: re-point live/ at an archive that already exists. Neither rebuilds, so a promotion cannot produce different bytes than the release that was tested.
MERGE DECISIONS:
- The DEPLOYED spec carries control-plane's fields (the digests to serve and to protect, retention, CDN). forge's BUILD inputs (public_dir, bundle, cache_control) are not here, because they describe how to PRODUCE a release, not what to serve. They stay on forge's KCL StaticSite build block, which runs before an artifact exists and is deliberately not a CR field. A CR carrying public_dir would name a directory on a machine the operator never sees.
- The digests are SPEC, not status (control-plane's design, on merit). LiveDigest is the pointer whose movement IS the deploy. PreviousDigest and RetainedDigests are what retention must never delete. The retention guarantee has to arrive from the release ledger and not be inferred from the bucket, and a value the executor computed for itself could be lost to a status wipe, after which retention would no longer know what to protect.
- Nothing here encodes HOW bytes move. forge's `gcloud storage cp` executor is to be replaced by a single storage-SDK planner. These types describe only the desired pointer and the retention set, so they fit either executor.
func (*StaticSiteSpec) DeepCopy ¶
func (in *StaticSiteSpec) DeepCopy() *StaticSiteSpec
DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StaticSiteSpec.
func (*StaticSiteSpec) DeepCopyInto ¶
func (in *StaticSiteSpec) DeepCopyInto(out *StaticSiteSpec)
DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (StaticSiteSpec) EffectiveKeepReleases ¶
func (s StaticSiteSpec) EffectiveKeepReleases() int32
EffectiveKeepReleases applies the default and the predecessor floor. The floor is applied here, on the type, so that no retention planner is one forgotten max() away from deleting the predecessor. 0 means "retain everything" and is not floored. Validate refuses 1 outright; the floor here is the defense-in-depth for a spec that skipped it.
func (StaticSiteSpec) Validate ¶
func (s StaticSiteSpec) Validate() error
Validate checks a StaticSite spec.
type StaticSiteStatus ¶
type StaticSiteStatus struct {
TierStatus `json:",inline"`
// BucketPrefix is the site's root prefix within its bucket, RECORDED
// rather than re-derived, so that a later change to the derivation rule
// cannot silently re-point a prefix that already holds live objects.
// +optional
BucketPrefix string `json:"bucketPrefix,omitempty"`
// LiveDigest is what the live prefix was CONFIRMED to serve. It lags
// spec.liveDigest until a sync succeeds, and that gap is the drift
// signal. PreviousDigest is its confirmed predecessor.
// +optional
LiveDigest string `json:"liveDigest,omitempty"`
// +optional
PreviousDigest string `json:"previousDigest,omitempty"`
// RecentDigests are the releases the live prefix CONFIRMED serving
// before LiveDigest, newest first (PreviousDigest leads). The executor
// keeps their files under spec.assetDir serveable — a page loaded from
// one of them still lazy-loads its chunks after a deploy — so retention
// must never delete them. Ordered history that a bucket listing cannot
// give: archive prefixes carry no time.
// +optional
// +kubebuilder:validation:MaxItems=32
RecentDigests []string `json:"recentDigests,omitempty"`
// ReleaseCount is how many archived releases the last retention pass
// observed.
// +optional
// +kubebuilder:validation:Minimum=0
ReleaseCount int32 `json:"releaseCount,omitempty"`
// LastSyncedAt is nil until the live prefix has synced once.
// +optional
LastSyncedAt *metav1.Time `json:"lastSyncedAt,omitempty"`
}
StaticSiteStatus is the observed state of a StaticSite.
func (*StaticSiteStatus) DeepCopy ¶
func (in *StaticSiteStatus) DeepCopy() *StaticSiteStatus
DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StaticSiteStatus.
func (*StaticSiteStatus) DeepCopyInto ¶
func (in *StaticSiteStatus) DeepCopyInto(out *StaticSiteStatus)
DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
type TaintEffect ¶
type TaintEffect string
TaintEffect is the taint effect a toleration matches. Empty matches every effect.
+kubebuilder:validation:Enum=NoSchedule;PreferNoSchedule;NoExecute
const ( TaintNoSchedule TaintEffect = "NoSchedule" TaintPreferNoSchedule TaintEffect = "PreferNoSchedule" TaintNoExecute TaintEffect = "NoExecute" )
type TierStatus ¶
type TierStatus struct {
// +optional
Phase Phase `json:"phase,omitempty"`
// ObservedGeneration is the metadata.generation this status describes. A
// status whose ObservedGeneration lags the object's generation describes
// a PREVIOUS spec, and a reader must not treat it as current.
// +optional
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
// Hostname is the hostname serving this workload, and URL is its https://
// form. On hosted this is the ALLOCATED platform hostname, recorded only
// after the allocator committed it. Custom domains from spec.domains are
// served in addition to it.
// +optional
Hostname string `json:"hostname,omitempty"`
// +optional
URL string `json:"url,omitempty"`
// Message is the human-readable reason for the current phase.
// +optional
Message string `json:"message,omitempty"`
// +listType=map
// +listMapKey=type
// +optional
Conditions []metav1.Condition `json:"conditions,omitempty"`
}
TierStatus is the observed state shared by the kinds that serve traffic (Workload, StaticSite).
It was called WorkloadStatus until Workload became a kind of its own. The shared part is now TierStatus, so that WorkloadStatus can be the Workload kind's full status, following the <Kind>Status convention every other kind in this package uses.
MERGE DECISION: control-plane's shape wins outright, since forge had no status at all. A self-hosted user could not ask "what is running in prod and is it up" without kubectl. Live, control-plane's status answered exactly the questions a user asks ("ImageRejected: … not from an allowed registry", "no release promoted to this site yet"). ONE shape: the operator writes it to the CR on hosted, and `forge env status` computes it on read when self-hosted.
Every field is OBSERVED, never desired. A value appears only after the thing it names is already true.
func (*TierStatus) DeepCopy ¶
func (in *TierStatus) DeepCopy() *TierStatus
DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TierStatus.
func (*TierStatus) DeepCopyInto ¶
func (in *TierStatus) DeepCopyInto(out *TierStatus)
DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
type Toleration ¶
type Toleration struct {
// Key is the taint key. Empty, with operator Exists, tolerates every
// taint.
// +optional
// +kubebuilder:validation:MaxLength=316
Key string `json:"key,omitempty"`
// Operator is Exists or Equal. Unset means Equal (Kubernetes' default).
// +optional
Operator TolerationOperator `json:"operator,omitempty"`
// Value is the taint value to match, for operator Equal.
// +optional
// +kubebuilder:validation:MaxLength=63
Value string `json:"value,omitempty"`
// Effect is the taint effect to match. Unset matches every effect.
// +optional
Effect TaintEffect `json:"effect,omitempty"`
// TolerationSeconds bounds how long a NoExecute taint is tolerated
// before eviction. Unset means forever.
// +optional
// +kubebuilder:validation:Minimum=0
TolerationSeconds *int64 `json:"tolerationSeconds,omitempty"`
}
Toleration lets the workload's pods schedule onto nodes with a matching taint. It is a local mirror of core/v1 Toleration, for the same reasons as PolicyRule: the schema is closed and self-contained.
The spellings are Kubernetes' own (Exists, NoSchedule), not lowercased. A toleration is copied from a node pool's taint, and a second spelling would be a translation step where the two could disagree.
func (*Toleration) DeepCopy ¶
func (in *Toleration) DeepCopy() *Toleration
DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Toleration.
func (*Toleration) DeepCopyInto ¶
func (in *Toleration) DeepCopyInto(out *Toleration)
DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
type TolerationOperator ¶
type TolerationOperator string
TolerationOperator is how a toleration matches a taint's value.
+kubebuilder:validation:Enum=Exists;Equal
const ( TolerationOpExists TolerationOperator = "Exists" TolerationOpEqual TolerationOperator = "Equal" )
type Volume ¶
type Volume struct {
// Name is the volume name, unique within the pod.
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=63
// +kubebuilder:validation:Pattern=`^[a-z0-9]([-a-z0-9]*[a-z0-9])?$`
Name string `json:"name"`
// MountPath is the absolute path the volume mounts at in the main
// container.
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=4096
// +kubebuilder:validation:Pattern=`^/.*$`
MountPath string `json:"mountPath"`
// ReadOnly mounts the volume read-only. Secret and ConfigMap volumes
// are conventionally read-only; an emptyDir is usually writable scratch.
// +optional
ReadOnly bool `json:"readOnly,omitempty"`
// Source is where the volume's content comes from: exactly one kind.
Source VolumeSource `json:"source"`
}
Volume is one pod volume and its mount into the MAIN container.
One declaration models both the pod's spec.volumes entry and the container's volumeMounts entry, because they share a name and a workload almost always mounts a volume exactly once.
THE SOURCES ARE CLOSED: NO hostPath, projected OR csi ¶
There are exactly four sources: a Secret, a ConfigMap, an emptyDir and an existing PersistentVolumeClaim. The others are unreachable on purpose, even under ProfileFull:
- hostPath mounts the NODE's filesystem into the pod. It defeats every other isolation forge renders (non-root, read-only rootfs, dropped capabilities), since a writable hostPath is a node compromise, and the restricted Pod Security Standard forbids it outright. A workload that truly needs the node is a DaemonSet-shaped piece of infrastructure, not a forge workload.
- projected aggregates ServiceAccount tokens, the Downward API and cluster trust bundles into one mount. Token projection with a custom audience is how a pod mints credentials for OTHER systems, and that belongs to the ServiceAccount a workload runs as, not to a volume declaration.
- csi hands the pod to an arbitrary storage driver, with driver-specific attributes forge cannot validate (secrets-store CSI, inline ephemeral volumes). A driver's configuration surface is exactly the open-ended passthrough this schema exists to avoid.
Needing one of them is the signal that the object belongs in the environment's infra block as a raw manifest, which forge applies but does not model.
func (*Volume) DeepCopy ¶
DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Volume.
func (*Volume) DeepCopyInto ¶
DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
type VolumeSource ¶
type VolumeSource struct {
// Secret projects a Secret in the workload's namespace as files.
// +optional
Secret *SecretVolumeSource `json:"secret,omitempty"`
// ConfigMap projects a ConfigMap in the workload's namespace as files.
// +optional
ConfigMap *ConfigMapVolumeSource `json:"configMap,omitempty"`
// EmptyDir is pod-lifetime scratch space, which is the way to give a
// workload a writable path under forge's read-only root filesystem.
// +optional
EmptyDir *EmptyDirVolumeSource `json:"emptyDir,omitempty"`
// PVC mounts an EXISTING PersistentVolumeClaim. For storage forge
// provisions, use WorkloadSpec.StorageGiB.
// +optional
PVC *PVCVolumeSource `json:"pvc,omitempty"`
}
VolumeSource is exactly one of the four closed volume sources.
+kubebuilder:validation:ExactlyOneOf=secret;configMap;emptyDir;pvc
func (*VolumeSource) DeepCopy ¶
func (in *VolumeSource) DeepCopy() *VolumeSource
DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VolumeSource.
func (*VolumeSource) DeepCopyInto ¶
func (in *VolumeSource) DeepCopyInto(out *VolumeSource)
DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
type Workload ¶
type Workload struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec WorkloadSpec `json:"spec"`
Status WorkloadStatus `json:"status,omitempty"`
}
Workload is one runnable thing: a service, worker, job, cron, operator or tool.
+kubebuilder:object:root=true +kubebuilder:subresource:status +kubebuilder:resource:shortName=wl,categories=forge +kubebuilder:printcolumn:name="Kind",type=string,JSONPath=`.spec.kind` +kubebuilder:printcolumn:name="Replicas",type=integer,JSONPath=`.spec.replicas` +kubebuilder:printcolumn:name="Ready",type=integer,JSONPath=`.status.readyReplicas` +kubebuilder:printcolumn:name="Phase",type=string,JSONPath=`.status.phase` +kubebuilder:printcolumn:name="Hostname",type=string,JSONPath=`.status.hostname` +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`
func (*Workload) DeepCopy ¶
DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Workload.
func (*Workload) DeepCopyInto ¶
DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (*Workload) DeepCopyObject ¶
DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (Workload) Validate ¶
Validate checks a Workload OBJECT under profile p: the rules that need metadata.name, then every WorkloadSpec rule (Spec.Validate). All errors are collected together.
THIS IS THE ADMISSION ENTRYPOINT. The control plane admits Workload CRs, so it calls this rather than Spec.Validate. A spec cannot see its own name, and three rules are about that name:
- the name is an RFC-1123 label of at most 63 characters, because it is every rendered object's name (Deployment, Service, ServiceAccount, main container);
- a STANDALONE job's name is at most MaxStandaloneJobNameLength (52), because its Job is named <name>-<10-char spec hash>. A gating job renders no Job of its own, so the 63-character limit is its limit;
- no sidecar shares the workload's name, which the main container uses. Kubernetes refuses two containers of one name, but only server-side, so the deploy would die against the real cluster.
Cross-WORKLOAD rules (duplicate names in one env, what `before` names, job cycles) need the whole set and stay with pkg/deploy.RenderWorkloads. Metadata other than the name (labels, namespace) is the platform's to stamp and is not checked here.
type WorkloadKind ¶
type WorkloadKind string
WorkloadKind is HOW a workload runs. It never says what language the workload is in, what code is inside it, or how it was built.
The set and its meaning are the ones fw.Workload already used (kcl/workloads/capabilities.k), so one vocabulary covers every runtime:
- service — a long-running Deployment with a ClusterIP Service.
- worker — a long-running Deployment that nothing dials by name.
- job — a one-shot that runs to completion, either gating other workloads (Before) or as a standalone Job placed by DeployPhase.
- cron — a CronJob on Schedule.
- operator — a controller-runtime manager: cluster RBAC, CRDs, leader election.
- tool — built into the image and NEVER scheduled. A tool renders no manifest, so every runtime capability is refused for it.
+kubebuilder:validation:Enum=service;worker;job;cron;operator;tool
const ( KindService WorkloadKind = "service" KindWorker WorkloadKind = "worker" KindJob WorkloadKind = "job" KindCron WorkloadKind = "cron" KindOperator WorkloadKind = "operator" KindTool WorkloadKind = "tool" )
type WorkloadList ¶
type WorkloadList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []Workload `json:"items"`
}
WorkloadList contains a list of Workload.
+kubebuilder:object:root=true
func (*WorkloadList) DeepCopy ¶
func (in *WorkloadList) DeepCopy() *WorkloadList
DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WorkloadList.
func (*WorkloadList) DeepCopyInto ¶
func (in *WorkloadList) DeepCopyInto(out *WorkloadList)
DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (*WorkloadList) DeepCopyObject ¶
func (in *WorkloadList) DeepCopyObject() runtime.Object
DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
type WorkloadSpec ¶
type WorkloadSpec struct {
// Kind is how the workload runs. Default service.
// +optional
// +kubebuilder:default=service
Kind WorkloadKind `json:"kind,omitempty"`
// Image is the container image, resolved by the lowering (the build
// output, or an author-named third-party image). ProfileRestricted
// also requires it to be registry-qualified and pinned, because the
// platform pulls it from a registry the hosted user does not control. A
// cluster the author operates may run a locally imported bare name.
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=500
Image string `json:"image"`
// Command overrides the image's entrypoint. Rare: a forge-built
// project binary is selected by Args, not by replacing the entrypoint.
// +optional
// +kubebuilder:validation:MaxItems=64
// +kubebuilder:validation:items:MaxLength=4096
Command []string `json:"command,omitempty"`
// Args are the entrypoint's arguments, typically the project binary's
// subcommand. The host runtime derives its command from the build plus
// these same args, so they are stated once for every runtime.
// +optional
// +kubebuilder:validation:MaxItems=64
// +kubebuilder:validation:items:MaxLength=4096
Args []string `json:"args,omitempty"`
// Replicas is the pod count for a service, worker or operator. Default
// 1. A batch kind's concurrency belongs to its Job or CronJob, and a
// tool is never scheduled, so neither may set more than one.
// +optional
// +kubebuilder:default=1
// +kubebuilder:validation:Minimum=1
Replicas int32 `json:"replicas,omitempty"`
// Resources is the compute request and limit. Omitted means the entry
// shape (250m / 1 GiB).
// +optional
Resources Resources `json:"resources,omitempty"`
// Env is the container environment. There is no portable entry-count limit:
// exec limits apply to the resolved environment and arguments in bytes,
// including values supplied by secret references at runtime.
// +optional
// +listType=map
// +listMapKey=name
Env []EnvVar `json:"env,omitempty"`
// Ports are the ports the container listens on. A service's ports are
// all on its ClusterIP Service. An operator's and a worker's are
// container ports only, because nothing resolves them by name.
// +optional
// +listType=map
// +listMapKey=name
// +kubebuilder:validation:MaxItems=16
Ports []Port `json:"ports,omitempty"`
// Probes are the readiness and liveness probes. Omitted means the
// policy in EffectiveProbes: TCP on the first port for a service that
// declares ports, and none for anything else. The lowering writes HTTP
// /readyz + /healthz explicitly for a service forge built, and for a
// forge-built operator that declares ports.
// +optional
Probes *Probes `json:"probes,omitempty"`
// StorageGiB provisions a ReadWriteOnce PersistentVolumeClaim of this
// many GiB, mounted at /data. Zero means stateless.
//
// A ReadWriteOnce volume can be mounted by one pod at a time, so a
// workload with storage runs exactly one replica with a Recreate
// rollout. A rolling update would start the new pod while the old one
// still holds the volume, and wedge. A bound PVC keeps existing, and on
// hosted keeps billing, while the workload is scaled to zero. That is
// deliberate: it is what the cloud itself charges.
// +optional
// +kubebuilder:validation:Minimum=0
StorageGiB int32 `json:"storageGiB,omitempty"`
// Schedule is a cron's schedule: a 5-field cron expression or one of
// @hourly, @daily, @weekly, @monthly, @yearly.
// +optional
// +kubebuilder:validation:MaxLength=128
Schedule string `json:"schedule,omitempty"`
// Before names the workloads that must not start until this job exits
// 0, or [BeforeAll] for every workload in the environment.
// +optional
// +kubebuilder:validation:MaxItems=64
Before []string `json:"before,omitempty"`
// DeployPhase places a standalone job in the deploy. Unset means
// pre-rollout (EffectiveDeployPhase). There is no default marker: a
// static default would be stamped on every kind, and Validate would
// then refuse the CR it was handed.
// +optional
DeployPhase DeployPhase `json:"deployPhase,omitempty"`
// NamespacedRBAC grants a Role + RoleBinding in the workload's own
// namespace, in addition to forge's default config-read rules. The Role
// is rendered whenever the workload has ANY RBAC (either tier, or kind
// operator); with none, there is no Role at all and no mounted token.
// +optional
// +kubebuilder:validation:MaxItems=64
NamespacedRBAC []PolicyRule `json:"namespacedRBAC,omitempty"`
// ClusterRBAC grants a ClusterRole + ClusterRoleBinding. Any scheduled
// kind may declare it (Full profile only): an operator watches its CRDs
// cluster-wide, and a service may legitimately resolve resources across
// namespaces (a proxy routing to per-workspace objects) without being a
// controller. It ADDS to the namespaced Role rather than replacing it:
// the ClusterRole carries only cluster-scoped intent (these rules, and an
// operator's CRD-derived rules), while the config-read defaults and an
// operator's leader-election lease stay in the namespaced Role. Both
// bind the one ServiceAccount, so NamespacedRBAC and ClusterRBAC may be
// declared together, each at its own scope.
// +optional
// +kubebuilder:validation:MaxItems=64
ClusterRBAC []PolicyRule `json:"clusterRBAC,omitempty"`
// CRDs are the custom resource kinds an operator's manager owns. May be
// empty: an operator that owns none yet, or reconciles built-in kinds.
// +optional
// +kubebuilder:validation:MaxItems=64
// +kubebuilder:validation:items:Pattern=`^[A-Z][A-Za-z0-9]*$`
CRDs []string `json:"crds,omitempty"`
// Group is the API group of an operator's CRDs.
// +optional
// +kubebuilder:validation:MaxLength=253
// +kubebuilder:validation:Pattern=`^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$`
Group string `json:"group,omitempty"`
// Version is the API version of an operator's CRDs.
// +optional
// +kubebuilder:validation:MaxLength=63
// +kubebuilder:validation:Pattern=`^v[0-9]+((alpha|beta)[0-9]+)?$`
Version string `json:"version,omitempty"`
// LeaderElection turns an operator's leader election on or off. Unset
// means on (EffectiveLeaderElection). A pointer, with no default
// marker, because the default is true only for an operator. A static
// default would stamp it onto every kind.
// +optional
LeaderElection *bool `json:"leaderElection,omitempty"`
// ServiceAccountAnnotations are stamped on the workload's generated
// ServiceAccount and on no other object. Cloud workload identity is the
// case it exists for: GKE reads iam.gke.io/gcp-service-account, and EKS
// reads eks.amazonaws.com/role-arn, off the KSA the pod runs as.
// +optional
// +kubebuilder:validation:MaxProperties=32
ServiceAccountAnnotations map[string]string `json:"serviceAccountAnnotations,omitempty"`
// Sidecars are additional containers in the workload's pod: a Cloud SQL
// Auth Proxy, a registry forwarder. They get the same hardened
// securityContext as the main container.
// +optional
// +listType=map
// +listMapKey=name
// +kubebuilder:validation:MaxItems=8
Sidecars []Container `json:"sidecars,omitempty"`
// Volumes are extra pod volumes, each mounted into the main container.
// For storage forge provisions, use StorageGiB.
// +optional
// +listType=map
// +listMapKey=name
// +kubebuilder:validation:MaxItems=32
Volumes []Volume `json:"volumes,omitempty"`
// ServiceAccount names an EXISTING ServiceAccount to run as, instead of
// the one forge generates. The case is cloud workload identity bound to
// a ServiceAccount that other infrastructure owns. forge then renders no
// ServiceAccount, Role or binding for the workload, so this is mutually
// exclusive with NamespacedRBAC, ClusterRBAC and
// ServiceAccountAnnotations: each of those describes the generated
// ServiceAccount, which no longer exists.
//
// The renderer leaves the pod's automountServiceAccountToken UNSET when
// this is set, so the named ServiceAccount's own
// automountServiceAccountToken decides and its owner controls it. forge
// cannot know whether that identity calls the Kubernetes API, and a
// pod-level false would silently disable one that does. (A generated
// ServiceAccount's token mounts only when the workload has RBAC.)
// +optional
// +kubebuilder:validation:MaxLength=253
// +kubebuilder:validation:Pattern=`^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$`
ServiceAccount string `json:"serviceAccount,omitempty"`
// NodeSelector constrains the pods to nodes with these labels.
// +optional
// +kubebuilder:validation:MaxProperties=32
NodeSelector map[string]string `json:"nodeSelector,omitempty"`
// Tolerations let the pods schedule onto tainted nodes.
// +optional
// +kubebuilder:validation:MaxItems=32
Tolerations []Toleration `json:"tolerations,omitempty"`
// PodAnnotations are stamped on the pod template.
// +optional
// +kubebuilder:validation:MaxProperties=64
PodAnnotations map[string]string `json:"podAnnotations,omitempty"`
// Strategy is how the Deployment replaces its pods on a rollout.
//
// Unset is Kubernetes' default, RollingUpdate, which surges a new pod
// before the old one stops — so a workload whose replica count is a
// CORRECTNESS bound rather than a capacity choice (a sweeper that is not
// idempotent under concurrency) runs twice for the length of a rollout,
// which `replicas: 1` looks like it prevents and does not. Declare
// Recreate there and accept the gap with no pod.
//
// RollingUpdate alongside storageGiB is REFUSED: a ReadWriteOnce volume
// is mounted by one pod at a time, so the surge pod can never start and
// the rollout wedges. Storage alone already renders Recreate.
// +optional
Strategy DeploymentStrategy `json:"strategy,omitempty"`
// PriorityClassName names a PriorityClass that ranks these pods against
// everything else competing for a node: the scheduler places a higher
// priority pod first, and a preempting class evicts lower priority pods
// to make room. The case is a workload sharing nodes with pods that
// already carry a class — without one it is priority 0 and loses every
// contest, so a rollout's surge pod can be preempted repeatedly and take
// minutes to land.
//
// The class is CLUSTER-SCOPED and must already exist: Kubernetes REFUSES
// a pod naming a PriorityClass the cluster does not have, so the class
// has to be applied to every cluster this workload lands on, before the
// pods that name it. Unset leaves the pod at the cluster's default
// (globalDefault, else 0). Full profile only.
// +optional
// +kubebuilder:validation:MaxLength=253
// +kubebuilder:validation:Pattern=`^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$`
PriorityClassName string `json:"priorityClassName,omitempty"`
// EnableServiceLinks opts the pods IN to Kubernetes' service links: an
// env var per Service in the namespace (<NAME>_PORT=tcp://ip:port,
// <NAME>_SERVICE_HOST, <NAME>_SERVICE_PORT, ...). Unset (false) renders
// pod.enableServiceLinks: false, because those names are not namespaced
// to anything: a Service called `temporal` injects TEMPORAL_PORT, which
// a typed config loader then parses as a port number and the process
// CrashLoops at boot (reliant's daemon-gateway, 2026-10-21; Zitadel's
// ZITADEL_PORT before it). Resolve a Service through DNS
// (<svc>.<namespace>.svc) or an explicit env var instead. Set this only
// for an image that reads the injected variables. Full profile only.
// +optional
EnableServiceLinks bool `json:"enableServiceLinks,omitempty"`
// TerminationGracePeriodSeconds overrides the drain-derived grace
// period (PRE_STOP_DELAY + SHUTDOWN_TIMEOUT + 5 from the workload's env).
// For a process whose shutdown budget is not expressed in those env
// vars: a Temporal worker finishing in-flight activities needs 60s.
// Full profile only. 0 is honoured (SIGKILL immediately).
// +optional
// +kubebuilder:validation:Minimum=0
// +kubebuilder:validation:Maximum=3600
TerminationGracePeriodSeconds *int32 `json:"terminationGracePeriodSeconds,omitempty"`
// ActiveDeadlineSeconds bounds a job or cron run: Kubernetes fails the
// Job (Job.spec.activeDeadlineSeconds) once it has run this long, and
// the host runtime kills the process. A one-shot that can hang would
// otherwise block a deploy or `forge env up` forever. Allowed under
// every profile: a timeout only ever shortens what runs.
// +optional
// +kubebuilder:validation:Minimum=1
ActiveDeadlineSeconds *int64 `json:"activeDeadlineSeconds,omitempty"`
// SecurityContext loosens forge's pod identity defaults for an image
// that needs it: a Next.js standalone image that runs as uid 1000 and
// writes into its own tree. Full profile only. It is deliberately
// NOT a Kubernetes securityContext passthrough: no privileged, no
// capabilities, no host namespaces, and runAsNonRoot stays forced, so
// every pod is still Pod Security `restricted`.
// +optional
SecurityContext *PodSecurity `json:"securityContext,omitempty"`
}
WorkloadSpec is everything that runs, as ONE declaration (ADR 0002).
It is the wire contract for every runtime. The CLI lowers an authored fw.Workload to this type and renders it for a cluster the author operates, and the hosted control plane receives it as a forge.dev Workload CR, validates it under ProfileRestricted, and renders it itself. The KCL fw.Workload is GENERATED from this type, so the author-facing field set and the API server's pruning schema cannot disagree.
ONE TYPE, TWO PROFILES ¶
The type describes everything a workload can be on a cluster the author operates. What a destination ACCEPTS is a Profile, checked by Validate: ProfileFull allows every field, and ProfileRestricted (hosted, shared nodes) allows the subset in FieldProfiles and KindProfiles. That keeps hosted and self-hosted as one model with one renderer, rather than a hosted tier that has to be outgrown.
WHAT IS NOT HERE ¶
Name is metadata.name. Namespace and cluster describe the target, not the app. Build and runtime are authoring facts that the lowering has already consumed by the time a spec exists: the build becomes Image, and the runtime decides where the spec goes. Hosted identity is labels. Observed facts are status.
func (*WorkloadSpec) DeepCopy ¶
func (in *WorkloadSpec) DeepCopy() *WorkloadSpec
DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WorkloadSpec.
func (*WorkloadSpec) DeepCopyInto ¶
func (in *WorkloadSpec) DeepCopyInto(out *WorkloadSpec)
DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (WorkloadSpec) EffectiveDeployPhase ¶
func (s WorkloadSpec) EffectiveDeployPhase() DeployPhase
EffectiveDeployPhase is a standalone job's phase: DeployPhase, or pre-rollout. Empty for anything that is not a standalone job.
func (WorkloadSpec) EffectiveKind ¶
func (s WorkloadSpec) EffectiveKind() WorkloadKind
EffectiveKind resolves an unset Kind to service.
func (WorkloadSpec) EffectiveLeaderElection ¶
func (s WorkloadSpec) EffectiveLeaderElection() bool
EffectiveLeaderElection is whether an operator elects a leader: true unless explicitly disabled. Always false for other kinds.
func (WorkloadSpec) EffectiveProbes ¶
func (s WorkloadSpec) EffectiveProbes() *Probes
EffectiveProbes is the probe policy of ADR 0002 §5, defined once:
- declared probes are used as declared, with defaults filled;
- a service that declares ports but no probes gets a TCP probe on its first port, because a Service routing to a pod nobody checks is how a hosted API ran unprobed;
- anything else gets none: a worker's, operator's or job's health is its own business unless it says otherwise.
A service or operator that forge BUILT gets HTTP /readyz + /healthz: both run under serverkit, which serves them on the process's PORT. The lowering knows the build, so it writes that probe into the spec explicitly, and it arrives here as declared.
The returned probe has Port resolved, so a renderer never re-derives it.
func (WorkloadSpec) ExposedPort ¶
func (s WorkloadSpec) ExposedPort() *Port
ExposedPort is the port a public hostname routes to, or nil when nothing is exposed. Its Domains are the custom hostnames served beside the default one. Validate guarantees at most one exposed port, and that Domains appear on no other port.
func (WorkloadSpec) ProbePort ¶
func (s WorkloadSpec) ProbePort() int32
ProbePort is the port the probes connect to: Probes.Port if set, else the port named http, else the first declared port, else 0 (nothing to probe).
func (WorkloadSpec) UsesGeneratedServiceAccount ¶
func (s WorkloadSpec) UsesGeneratedServiceAccount() bool
UsesGeneratedServiceAccount reports whether forge renders the workload's ServiceAccount (named after the workload). False when ServiceAccount names an existing one.
func (WorkloadSpec) Validate ¶
func (s WorkloadSpec) Validate(p Profile) error
Validate checks a Workload spec against the invariants every destination shares and then against profile p. Every violation is collected and returned together.
The structural rules come first, and they hold under EVERY profile: a spec that contradicts itself (a schedule on a job, storage on three replicas) has no correct rendering anywhere. The profile rules come second, and say only what a destination refuses to run.
func (WorkloadSpec) WithDefaults ¶
func (s WorkloadSpec) WithDefaults() WorkloadSpec
WithDefaults fills every unset value that has a static default: the kind, replicas, resources, port protocols and (when present) probe timings, including each sidecar's. DeployPhase and LeaderElection are resolved by their Effective* helpers instead, because their defaults depend on the kind.
type WorkloadStatus ¶
type WorkloadStatus struct {
TierStatus `json:",inline"`
// ServiceName is the Service the hostname routes to. Empty for a kind
// with no Service object.
// +optional
ServiceName string `json:"serviceName,omitempty"`
// WorkloadName is the Deployment, Job or CronJob carrying the pods.
// +optional
WorkloadName string `json:"workloadName,omitempty"`
// ReadyReplicas is the API server's own observed ready count. It is
// this resource's liveness proof, and it is why a workload that never
// started bills nothing.
// +optional
// +kubebuilder:validation:Minimum=0
ReadyReplicas int32 `json:"readyReplicas,omitempty"`
// UpdatedReplicas is how many replicas are running the CURRENT pod
// template — the one carrying ObservedImage — and are ready.
//
// It exists because ReadyReplicas alone cannot tell a finished rollout
// from a failing one. Under RollingUpdate the old replicas stay ready
// while the new ones crash-loop, so ReadyReplicas ≥ desired holds
// throughout, and a reader that gates on it alone passes a release
// that never served a request. UpdatedReplicas is the count that goes
// to zero in exactly that case, which is why it is the field a rollout
// gate reads.
// +optional
// +kubebuilder:validation:Minimum=0
UpdatedReplicas int32 `json:"updatedReplicas,omitempty"`
// DesiredReplicas is how many the controller is working toward. It is
// reported rather than inferred from spec.replicas because the two can
// legitimately differ — mid-scale, or while a status describes a
// previous generation — and comparing UpdatedReplicas against the
// spec in that window would read a normal scale as a stalled rollout.
// +optional
// +kubebuilder:validation:Minimum=0
DesiredReplicas int32 `json:"desiredReplicas,omitempty"`
// ObservedImage is the image the running workload was CONFIRMED to
// carry. It lags spec.image until a rollout completes, and that gap is
// the drift signal.
//
// CONFIRMED means the rollout to that template finished — the same
// completion test `kubectl rollout status` applies. Until it does, the
// field keeps its PREVIOUS value, the image last confirmed running,
// because reporting the pod template's image the moment it is written
// would make "observed" mean "declared" and leave nothing to compare
// a desired digest against.
// +optional
ObservedImage string `json:"observedImage,omitempty"`
// LastReadyAt is nil until the workload has had a ready replica at
// least once. A nil value is the machine-readable form of "declared,
// never ran".
// +optional
LastReadyAt *metav1.Time `json:"lastReadyAt,omitempty"`
}
WorkloadStatus is the observed state of a Workload.
func (*WorkloadStatus) DeepCopy ¶
func (in *WorkloadStatus) DeepCopy() *WorkloadStatus
DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WorkloadStatus.
func (*WorkloadStatus) DeepCopyInto ¶
func (in *WorkloadStatus) DeepCopyInto(out *WorkloadStatus)
DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
type WorkloadURLRef ¶
type WorkloadURLRef struct {
// Name is the target workload's name (its metadata.name).
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=63
// +kubebuilder:validation:Pattern=`^[a-z0-9]([-a-z0-9]*[a-z0-9])?$`
Name string `json:"name"`
}
WorkloadURLRef names a workload, in the same environment, whose public URL is the value. Resolution is scoped by construction: the name carries no org, env or namespace component, so a reference can only ever reach a sibling workload.
What the URL IS depends on the target's kind: a StaticSite's URL is its site origin plus basePath, and a Workload's is the origin of the hostname its exposed port is served at. A reference to a workload with no public URL (nothing exposed, a database) is unresolvable and is refused, never guessed.
func (*WorkloadURLRef) DeepCopy ¶
func (in *WorkloadURLRef) DeepCopy() *WorkloadURLRef
DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WorkloadURLRef.
func (*WorkloadURLRef) DeepCopyInto ¶
func (in *WorkloadURLRef) DeepCopyInto(out *WorkloadURLRef)
DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (WorkloadURLRef) Validate ¶
func (r WorkloadURLRef) Validate() error
Validate checks a workload URL reference names a workload: an RFC-1123 label, which is what every tier's metadata.name is.