Documentation
¶
Overview ¶
Package config defines billet's on-disk configuration and its validation rules.
A single billet.yaml describes both roles. `billet server` reads the server and github sections; `billet node` reads the node section. BOTH read the tier catalog — the node needs each tier's image, command, disk and shm, which the lease riding on a launch command does not carry — so the catalog is duplicated on every machine with nothing checking that the copies agree. On a single machine the two processes read the same file.
Index ¶
- Constants
- func AWSDNSSuffix(region string) string
- func CheckBackupEndpoint(endpoint string) error
- func CheckBackupS3(b BackupS3Config) []error
- func CheckCeph(p CephConfig) []error
- func CheckCodeBuild(b CodeBuildConfig) []error
- func CheckCodeBuildEndpoint(endpoint string) error
- func CheckCodeBuildRegion(region string) error
- func CheckDSNEnv(name string) error
- func CheckEBSS3(e EBSS3Config) []error
- func CheckEC2Endpoint(endpoint string) error
- func CheckEC2Region(region string) error
- func CheckEC2SecurityGroups(field string, groups []string, required bool) []error
- func CheckFirecracker(f FirecrackerConfig) []error
- func CheckHostPort(field, addr string) error
- func CheckOrg(org string) error
- func CheckRegistryMirrors(r RegistryMirrors) []error
- func CheckRemoteShapes(p ProviderKind, types []RemoteShape) []error
- func CheckRepository(repository string) error
- func CheckRunnerGroup(group string) error
- func CheckSQSQueueNode(raw, node string) error
- func CheckSQSQueueURL(raw, region string) error
- func CheckSSMParameterPath(path string) error
- func CheckTargetName(name string) error
- func CheckTart(t TartConfig) []error
- func CheckWorkflowRef(value string) error
- func CodeBuildTrustConflict(tiers []Tier, node string) error
- func DefaultServerStateDir() string
- func DefaultUntrustedDNS() []string
- func LoopbackAddr(addr string) bool
- func SplitRepository(repository string) (string, string, bool)
- func TierTargetPolicyErrors(where string, t Tier, target GitHubTarget) []error
- func ValidateNodeName(where, name string) error
- func ValidateRelease(r *ReleaseConfig) []error
- type BackupConfig
- type BackupS3Config
- type ByteSize
- type CacheScope
- type CephConfig
- type CodeBuildConfig
- type CodeBuildEnvironment
- type Config
- func (c *Config) GitHubTarget(name string) (GitHubTarget, bool)
- func (c *Config) GitHubTargets() []GitHubTarget
- func (c *Config) MacOSFleetProvider(node string) ProviderKind
- func (c *Config) MacOSLimitForNode(name string) int
- func (c *Config) NodePolicies() map[string]NodePolicy
- func (c *Config) NodePolicyFor(name string) (NodePolicy, bool)
- func (c *Config) TierByLabel(label string) (*Tier, bool)
- func (c *Config) TierTarget(t *Tier) (GitHubTarget, bool)
- func (c *Config) Validate() error
- type Contribution
- type ControllerMode
- type EBSS3Config
- type EC2Config
- type EC2InstanceType
- type FirecrackerConfig
- type GitHubConfig
- type GitHubTarget
- type GuestOS
- type IdentityBackend
- type IdentityConfig
- type IdentitySSMConfig
- type ImagesConfig
- type MaintenanceWindow
- type NodeCacheConfig
- type NodeConfig
- type NodePolicy
- type NodeTLS
- type PlacementPolicy
- type PostgresStateConfig
- type ProviderKind
- type RegistryMirrors
- type ReleaseConfig
- type RemoteCostNode
- type RemoteShape
- type ServerConfig
- func (s *ServerConfig) DrainTimeoutDuration() (time.Duration, error)
- func (s *ServerConfig) IdentityBackendKind() IdentityBackend
- func (s *ServerConfig) IdentitySSM() *IdentitySSMConfig
- func (s *ServerConfig) LedgerBackend() StateBackend
- func (s *ServerConfig) LedgerDSNEnv() string
- func (s *ServerConfig) LedgerPath() string
- type SiteConfig
- type SiteStoreKind
- type StateBackend
- type StateConfig
- type TargetScope
- type TartConfig
- type TartIsolation
- type Tier
- func (t Tier) AcceptableProviders() []ProviderKind
- func (t Tier) AcceptsProvider(p ProviderKind) bool
- func (t Tier) GuestOSProviderErrors(where string) []error
- func (t Tier) ImageFor(provider ProviderKind) string
- func (t Tier) InterceptionErrors(where string) []error
- func (t Tier) LaunchErrors(where string) []error
- func (t Tier) PoolPolicyErrors(where string) []error
- func (t Tier) ProviderErrors(where string) []error
- func (t Tier) ReservationErrors(where string) []error
- func (t Tier) ReservedMemory() ByteSize
- func (t Tier) ReservedVCPU() int
- func (t Tier) RunnerCommand() []string
- func (t Tier) RunnerCommandFor(provider ProviderKind) []string
- type TierLaunch
- type USDPerHour
- type WorkloadTrust
Constants ¶
const ( DefaultFirecrackerBinary = "/usr/local/bin/firecracker" DefaultJailerBinary = "/usr/local/bin/jailer" // DefaultKernelDir is where managed kernels are installed and where a // generation's recorded kernel name is resolved at launch. DefaultKernelDir = "/var/lib/billet/kernels" // DefaultChrootBase is the jailer's own default, so billet and a hand-run // jailer agree about where a microVM lives. DefaultChrootBase = "/srv/jailer" // DefaultJailUIDMin is where the per-microVM uid range starts. // // ABOVE EVERYTHING A DISTRIBUTION USES. Regular accounts stop at 60000 on // Debian and Ubuntu, systemd's DynamicUser range is 61184-65519, and the // subordinate-uid ranges `useradd` hands out for user namespaces start at // 100000 and are allocated 65536 at a time. Starting at 900000 sits clear of // all of it, and clear of the 65534 `nobody` that a truncated or overflowed // value would otherwise land on. DefaultJailUIDMin = 900000 // DefaultJailUIDCount is how many microVMs one host may run at once, as far as // uids go. Far above what any machine can hold, so it is a guard rather than a // capacity limit — the allocator's budget is the real one. DefaultJailUIDCount = 1024 // DefaultImageVerifyPort is adjacent to the control plane and guest cache // ports, and is reserved for the short-lived `billet images verify` listener. DefaultImageVerifyPort = 7719 // MinJailUID is the lowest start billet will accept. Below this a range starts // overlapping accounts that belong to somebody, and a microVM would run as a // user with a home directory and a login shell. MinJailUID = 100000 )
DefaultFirecrackerBinary and friends are where the reference host installs these, which is also where each project's own instructions put them.
const ( ChannelStable = "stable" ChannelCandidate = "candidate" )
The release channels billet publishes.
DECLARED HERE RATHER THAN IMPORTED FROM internal/releasesource, because config is a LEAF package: depguard forbids it importing any other billet package, and that rule is what keeps a validation rule from depending on the thing it validates. The two lists are kept in step by a test in internal/integration, which is where a cross-layer proof belongs — a package-local test here could only assert this file agrees with itself.
const CodeBuildAccountQueuedBuilds = 30
CodeBuildAccountQueuedBuilds is how many builds CodeBuild will hold in the queue for a WHOLE ACCOUNT before refusing StartBuild outright. A THIRD external ceiling, measured on 2026-09-02 rather than read: the thirty-first queued build is refused with `AccountLimitExceededException: Cannot have more than 30 builds in queue for the account`, and Service Quotas lists no quota to raise it. It is shared by every project in the account, so it bounds what a deployment may escrow against CodeBuild: a burst beyond the concurrency quota plus this many is refused at launch, which billet reports as a conclusive failure and GitHub requeues at most three times.
const CodeBuildBuildCeilingMinutes = 2160
CodeBuildBuildCeilingMinutes is CodeBuild's own maximum build timeout, and it is not billet's to raise. Every CodeBuild-backed job inherits it.
const CodeBuildBuildFloorMinutes = 5
CodeBuildBuildFloorMinutes is CodeBuild's minimum build timeout.
const CodeBuildQueuedCeilingMinutes = 480
CodeBuildQueuedCeilingMinutes is how long CodeBuild will hold a QUEUED build before failing it. A SECOND external ceiling, and the one an operator does not expect: on a fleet at capacity it is what kills a job that never got a machine.
const CodeBuildQueuedFloorMinutes = 5
CodeBuildQueuedFloorMinutes is CodeBuild's minimum queued timeout.
const DefaultCephConf = "/etc/ceph/ceph.conf"
DefaultCephConf is where Ceph's own search path finds a cluster's monitors, and therefore where billet is talking to when node.ceph.conf_path says nothing.
const DefaultCephUser = "billet"
DefaultCephUser is the RADOS identity billet authenticates as when the config names none. Deliberately not `admin` — see CephConfig.User.
const DefaultMacOSVMLimit = 2
DefaultMacOSVMLimit is Apple's licensing cap on macOS guests per Apple-branded host. Linux guests on the same machine are not subject to it.
A DEFAULT, not a hard ceiling: a deployment sets its own per-host number via NodePolicy.MacOSVMLimit. What the default guarantees is that a config which says nothing gets Apple's standard terms rather than "unlimited".
The static check is a guard, not the enforcement point — the allocator also holds a host-wide count of running plus warm macOS guests at runtime, because two separately-valid tiers on one node share one physical Mac.
const DefaultMemoryPerVCPU = 4 * GiB
DefaultMemoryPerVCPU is the proportion a `sizes` ladder is shaped to when the tier says nothing.
THE SAME NUMBER EVERY GENERATED CATALOGUE ALREADY USES, so a config that writes `sizes: [2, 4, 8]` and nothing else gets exactly the ladder `billet init` would have written for it. Changing it here would silently resize every deployment that took the default, which is why it is a named constant rather than a literal in the expansion.
const DefaultTargetName = "default"
DefaultTargetName is the name of the target the `github:` block declares.
A deployment that serves one organization writes `github:` and nothing else; that block IS a target, named so every path that resolves a tier to its credential has one vocabulary. A `targets:` entry may not take the name, because two blocks naming one target is two spellings of one value.
const PollInterval = 15 * time.Second
PollInterval is how often a node re-reports capacity to the server when nothing has changed. Assignment itself is push-driven; this is only a liveness and drift backstop.
const TapPrefix = "bt-"
TapPrefix is how the firecracker backend names the network device it creates for each guest.
DECLARED HERE AND USED THERE, rather than the other way round, because config may not import a provider — and a second copy in this file would be a constant that can drift from the thing it is validating against, which is the whole failure this check exists to prevent.
Variables ¶
This section is empty.
Functions ¶
func AWSDNSSuffix ¶ added in v0.7.0
AWSDNSSuffix is the DNS suffix of the partition a region belongs to.
DECLARED HERE AND USED THERE, exactly like TapPrefix and for the same reason: config may not import awsjson, and a second copy of this rule in this file would be a constant that can drift from the thing it is validating against. awsjson.DNSSuffixFor is this function.
MEASURED 2026-09-04 rather than read, because the documentation lists endpoints per service page and gets the legacy forms wrong. `sqs.cn-north-1.amazonaws.com` and `cn-north-1.queue.amazonaws.com` do not resolve; `sqs.cn-north-1.amazonaws.com.cn` does, and `cn-north-1.queue.amazonaws.com.cn` is a CNAME onto it. In the other direction `sqs.us-west-2.amazonaws.com.cn` does not resolve. GOVCLOUD IS NOT A SEPARATE CASE despite being a separate partition: `sqs.us-gov-west-1.amazonaws.com` resolves and the .cn form does not. The VPC-endpoint zone is delegated per partition too — `vpce.amazonaws.com` is served by ns-1714.awsdns-22.co.uk and `vpce.amazonaws.com.cn` by ns-960.awsdns-cn-60.com — which is corroboration rather than a probe, since the names below it are created with an endpoint.
A PARTITION BILLET HAS NOT BEEN TAUGHT ABOUT answers "amazonaws.com" here, and the SQS host check therefore REFUSES its queue URL rather than admitting a host that is not one: the ISO partitions are not under amazonaws.com at all. That is the safe direction and it is unchanged.
func CheckBackupEndpoint ¶
CheckBackupEndpoint applies the same rule to the archive store's endpoint.
THE SAME RULE, NOT A SECOND READING OF IT. Both endpoints receive requests billet signs and sends a session token with, so both refuse plaintext outside loopback for one reason — and a second implementation is a second security boundary, which is the argument internal/awssig already makes about having one signer rather than two.
func CheckBackupS3 ¶
func CheckBackupS3(b BackupS3Config) []error
CheckEBSS3 applies the safety rules needed by both config loading and the exported cloud-store constructor. CheckBackupS3 reports everything wrong with the archive store.
EXPORTED AND CALLED FROM BOTH SIDES, like CheckEBSS3 and CheckCeph: the store constructor is exported too, so a caller whose configuration did not come through config.Load must not be able to build one that points somewhere else.
THE REGION IS TWO DIFFERENT FACTS depending on the endpoint, and conflating them refuses correct deployments. With no endpoint it selects the AWS host billet dials, so it has to look like an AWS region or every request goes somewhere that does not exist. With one, it is only the SIGNING region — the server on the far side decides what it accepts, and Ceph RGW and MinIO deployments legitimately use names AWS never issued.
func CheckCeph ¶
func CheckCeph(p CephConfig) []error
CheckCeph refuses a storage block billet cannot safely act on.
EXPORTED AND CALLED FROM BOTH SIDES, like CheckEC2Endpoint and for the same reason: the client's constructor is exported and cannot assume its configuration came through Load. A rule enforced in only one of the two has a second entry point that does not enforce it.
It takes a VALUE, so a caller cannot hand it a nil pointer and read the empty result as approval.
func CheckCodeBuild ¶
func CheckCodeBuild(b CodeBuildConfig) []error
CheckCodeBuild reports everything wrong with the CodeBuild block.
EXPORTED AND CALLED FROM BOTH SIDES, the alloc.New rule: the provider's constructor is exported too, so a caller whose configuration did not come through config.Load must not be able to build one that signs requests for somewhere else, runs untrusted work, or writes a runner registration outside the path its IAM policy was scoped to.
IT DOES NOT CHECK THE CEILING ACKNOWLEDGEMENT, which is deliberately the node validator's job: this function is also what `billet check` and the provider constructor call, and refusing there would make a diagnostic unusable on a config somebody is in the middle of writing. The acknowledgement gates a node that SERVES work, which is where validateCodeBuildNode applies it.
func CheckCodeBuildEndpoint ¶
CheckCodeBuildEndpoint applies billet's one endpoint rule to this backend's API host. See CheckEC2Endpoint for why nothing here renders the value.
func CheckCodeBuildRegion ¶
CheckCodeBuildRegion refuses a region that cannot be signed with.
The same rule as CheckEC2Region and for the same reason: the region is signed into every request AND interpolated into the default endpoint, so it decides which host a signed request reaches. Measured on the ec2 side — `x@attacker.example/?` yields a url whose host is `attacker.example`.
func CheckDSNEnv ¶
CheckDSNEnv is the one rule for what may name the PostgreSQL connection string's environment variable.
EXPORTED BECAUSE THERE ARE TWO ENTRY POINTS. Config validation reaches it for a file on disk, and `billet init` reaches it for a flag — and a rule enforced at only one of two entry points is an entry point that does not enforce it. Without this, `--state-dsn-env 9-lives` was accepted by the generator, written into the file, and then refused by Parse with a message blaming a generated block the operator never typed.
func CheckEBSS3 ¶
func CheckEBSS3(e EBSS3Config) []error
func CheckEC2Endpoint ¶
CheckEC2Endpoint refuses an endpoint that would carry a credential in the clear or send a signed request somewhere billet did not mean.
NOTHING HERE RENDERS THE ENDPOINT, and that is the rule rather than an oversight. Every attempt to render it safely was wrong in a new way: interpolating it printed a password; wrapping url.Parse's error printed one too, because *url.Error embeds the whole URL; and url.Redacted masks only a HIERARCHICAL url's password, so it leaves an opaque one (`http:alice:secret@host`) and any `?token=` query completely intact. Both measured. Naming the field and the failed component tells an operator everything they can act on and cannot leak anything.
LOOPBACK IS THE EXCEPTION to https, and it is billet's existing rule rather than a new one: a loopback wire has no certificates at all, because there the trust boundary is the machine itself.
func CheckEC2Region ¶
CheckEC2Region refuses a region that is not one.
EXPORTED, because a region is not only an address: it is interpolated into the DEFAULT ENDPOINT HOST, and it is part of the scope every request is signed with. The first of those is why the provider's constructor re-applies it — measured, a region of `x@attacker.example/?` produces a default endpoint whose host is `attacker.example`, and the signed request and its session token go there.
A SHAPE RATHER THAN A LIST. An allowlist goes stale the next time AWS opens a region, and being stale means refusing a config that is correct. The shape catches the mistake people make, which is dropping the hyphens, and still admits partitions billet has never run in.
func CheckEC2SecurityGroups ¶
CheckEC2SecurityGroups refuses a list billet cannot safely launch against.
EXPORTED AND CALLED FROM BOTH SIDES, like CheckEC2Endpoint and for the same reason: the provider's constructor is exported and cannot assume its configuration came through Load.
BOTH HALVES OF THE RULE LIVE HERE. An earlier version exported only the blank-entry half, so the constructor accepted a config with NO trusted group at all — and RunInstances without a group lets EC2 pick the VPC's default, which in a VPC somebody already had usually permits a good deal more than they are picturing. A rule split across two places has an entry point that does not enforce it, which is the thing exporting it was meant to fix.
`required` is false for the untrusted list, where EMPTY IS MEANINGFUL: its absence is what refuses fork pull-request work.
func CheckFirecracker ¶
func CheckFirecracker(f FirecrackerConfig) []error
CheckFirecracker refuses a microVM block billet cannot safely act on.
EXPORTED AND CALLED FROM BOTH SIDES, like CheckCeph and CheckEC2Endpoint: the provider's constructor is exported, so a rule enforced only in Load has a second entry point that does not enforce it.
It takes a VALUE, so a caller cannot hand it a nil pointer and read the empty result as approval.
func CheckHostPort ¶
CheckHostPort validates a host:port the way config validation will, naming the field. Exported for callers that must refuse a bad value by the flag that carried it BEFORE it is rendered into a config — the same validate-in-both-consumers rule as CheckRunnerGroup.
func CheckOrg ¶
CheckOrg reports why github.org cannot be carried to GitHub as written, or nil.
Exported for the same reason CheckRunnerGroup and CheckWorkflowRef are: `billet init` validates its --org flag against the one rule config validation applies, so a bad flag is refused by its own name rather than surfacing later as a config-load error blaming a generated file.
func CheckRegistryMirrors ¶
func CheckRegistryMirrors(r RegistryMirrors) []error
CheckRegistryMirrors refuses a set that cannot be three separate HTTPS origins.
func CheckRemoteShapes ¶
func CheckRemoteShapes(p ProviderKind, types []RemoteShape) []error
CheckRemoteShapes validates one remote backend's ordered shape catalogue.
Exported because the allocator receives the same catalogue over node registration and cannot assume it came through Config.Load.
THE PROVIDER RATHER THAN A FIELD NAME, so the messages name the key the operator actually wrote. It used to be `CheckEC2InstanceTypes`, whose prose was hard-coded to `node.ec2.instance_types` — which was fine while there was one remote backend and became a diagnostic pointing at a field that is not in a codebuild operator's file the moment there were two. `ProviderKind.ShapeField` is the one place that mapping lives, and the allocator has the registered provider to hand.
func CheckRepository ¶ added in v0.10.0
CheckRepository reports why a repository target written as owner/name cannot be carried to GitHub as written, or nil.
Exported for the reason CheckOrg is: `billet init --repository` and `billet github-app create --repository` validate the flag against the one rule config validation applies.
func CheckRunnerGroup ¶
CheckRunnerGroup reports why a runner group name cannot be looked up by the scale-set client, or nil for an empty name (GitHub's default group). Exported alongside CheckWorkflowRef so `billet init` can validate a `--runner-group` flag against the transport-safety rule before writing a config.
func CheckSQSQueueNode ¶
CheckSQSQueueNode makes the one-queue-per-node topology enforceable from independently deployed node configs: distinct node names imply distinct queue URLs rather than relying on an operator remembering not to share one.
func CheckSQSQueueURL ¶
CheckSQSQueueURL refuses a warning queue that cannot be signed safely.
THE SUFFIX IS SELECTED BY THE REGION, NEVER OFFERED AS A CHOICE. This admitted either partition's suffix for every region, so `cn-north-1` accepted `sqs.cn-north-1.amazonaws.com` — which is not a host — while `billet init iam` derived the queue ARN's partition from the same region and rendered a correct `arn:aws-cn:sqs:...`. Everything an operator can see is then right: the policy applies, the queue exists, the node starts. Behind it the node signs a ReceiveMessage for cn-north-1 and sends it to a name that does not resolve, so the two-minute spot warning never arrives, every reclaim becomes an unexplained failed job, and the lease stays charged until it expires. The queue host, the region billet signs for and the partition billet is authorised in have to be one partition, and only the region can decide which.
func CheckSSMParameterPath ¶
CheckSSMParameterPath refuses a Parameter Store prefix that would widen an IAM grant or land in a namespace AWS keeps for itself.
EXPORTED BECAUSE THE PATH HAS TWO READERS AND ONE RULE. A codebuild node writes registrations under it and its config is checked here at load; the node then REPORTS the same path at registration, and the control plane sweeps under it, so alloc.RegisterNode re-applies this to a value that arrived over the wire rather than through Load (the alloc.New rule). Two spellings of the rule would be two spellings that drift.
The message starts with the quoted value so a caller can prefix the config key it is checking.
func CheckTargetName ¶ added in v0.10.0
CheckTargetName is the rule a target's name is held to: the tier-label grammar, because a tier names its target by it and both end up in the same diagnostics, file names and store parameters. Exported so a command can refuse a name before an App is created for it.
func CheckTart ¶
func CheckTart(t TartConfig) []error
CheckTart validates the Apple Silicon block.
EXPORTED for the reason alloc.New re-applies config's rules: a caller that built a TartConfig in code never passed through Load, and a rule enforced in only one of the two entry points is not enforced.
func CheckWorkflowRef ¶
CheckWorkflowRef reports why a workflow ref is not a usable GitHub allowlist entry, or nil. Exported for the same reason CheckEC2Region and CheckCeph are: `billet init` validates a `--workflow` flag against the one rule tier validation applies, so a bad flag is refused by its own name rather than surfacing later as a config-load error blaming the generated tier.
func CodeBuildTrustConflict ¶ added in v0.6.0
CodeBuildTrustConflict reports whether the tiers that can place on the named codebuild node span both trust classes, naming the tiers on each side.
A CODEBUILD NODE IS ONE PROJECT AND ONE SERVICE ROLE, and that role reads the path every registration for the node is staged under; a VPC-connected build can read its own role from inside. So a fork's job on a node a trusted tier also names could read a trusted job's staged registration, which is a credential. The docs asked the operator to keep the two apart; this is the rule. A tier reaches a node when it pins it by name or names no node at all, so an unpinned tier reaches every codebuild node, and two unpinned tiers of different classes conflict on all of them (`node` is then "").
EXPORTED BECAUSE TWO CALLERS NEED IT: config load, where the file is being written, and the control plane at registration, because the node's file may be the node's alone and the catalogue is the control plane's.
func DefaultServerStateDir ¶
func DefaultServerStateDir() string
DefaultServerStateDir is where a config that omits server.state_dir actually keeps its state — the value applyDefaults fills in. Exported for `billet init`'s identity refusal, which must treat an ABSENT key as this directory rather than as "no deployment to protect".
func DefaultUntrustedDNS ¶
func DefaultUntrustedDNS() []string
DefaultUntrustedDNS is what an isolated guest resolves through when the operator names no resolver: Cloudflare and Google, both public, both reachable under the policy that blocks the gateway resolver.
Two of them, from different operators, because a runner that cannot resolve fails every job on the host and the second one costs nothing.
func LoopbackAddr ¶
LoopbackAddr reports whether an address accepts only from this machine.
SHARED WITH THE SERVER'S OWN DECISION: the wire is served without TLS on exactly the addresses this returns true for, so config validation and the listener must not answer differently. A wildcard is NOT loopback.
func SplitRepository ¶ added in v0.10.0
SplitRepository splits owner/name into its halves, reporting false for anything that is not exactly two non-empty segments.
func TierTargetPolicyErrors ¶ added in v0.10.0
func TierTargetPolicyErrors(where string, t Tier, target GitHubTarget) []error
TierTargetPolicyErrors reports what a tier may not be under its target.
A REPOSITORY TARGET IS UNTRUSTED-ONLY. A trusted tier is a non-default, workflow-restricted runner group, and a repository has no runner groups: its runners are its own, GitHub offers nothing to restrict a pool with, and so billet has no policy to read before a mint. Trust, a group, a workflow allowlist and cache interception (which requires trust) are each refused by name. And a tier's cache scope stays inside its target's owner — and its repository, for a repository target — because a cache is a trust boundary of the deployment and site, and one target's jobs must not be handed another owner's bytes.
Exported because the server applies it at Run as well: alloc.New re-applies the pool rules on a catalogue that never went through Parse, and this is the rule the layer holding the target's scope can enforce for the same reason.
func ValidateNodeName ¶
ValidateNodeName is the ONE rule for a node identifier, wherever it appears: node.name, nodes[].name and tiers[].node all name hosts in a single namespace, so validating them differently lets the same string be a legal host here and an illegal one there.
A whitespace-only pin is the concrete case: treated as "pinned" here it satisfies the must-name-a-node rule, while a consumer that trims it sees no pin at all — a placement decision changed by whitespace.
func ValidateRelease ¶
func ValidateRelease(r *ReleaseConfig) []error
ValidateRelease reports everything wrong with a release block.
EXPORTED SO EVERY READER APPLIES THE SAME RULES. config is a leaf package and the rollout planner is not, so the rule has to be callable from both — the same arrangement the capacity rules already have, and for the same reason: a rule enforced in one of two entry points has a second entry point that does not enforce it.
Types ¶
type BackupConfig ¶
type BackupConfig struct {
// S3 is an S3-compatible bucket. Nil means billet uploads nothing and the
// archive directory is the seam.
S3 *BackupS3Config `yaml:"s3,omitempty"`
}
BackupConfig is where this deployment's archives go when they leave the disk they protect.
OPTIONAL, AND THE DIRECTORY REMAINS THE CONTRACT. `billet local backup --out <dir>` writes a manifest of digests and sizes precisely so that somebody else's tooling can carry it; an operator who already has restic, rclone or a NAS needs nothing here. What this adds is the half that matters on the day the machine is new: `billet local restore --from s3://…` fetches, verifies and restores in one command, where an upload-only answer would have them installing another tool mid-outage.
WHAT BILLET DELIBERATELY DOES NOT BECOME is a backup tool. There is no dedupe, no incremental, no catalogue and no retention: the bucket does retention (versioning and a lifecycle rule), the manifest does verification, and billet never issues a delete — so the credential sitting on the one host that holds the App key cannot destroy the history it just wrote.
type BackupS3Config ¶
type BackupS3Config struct {
// Bucket receives one object per archive entry.
Bucket string `yaml:"bucket"`
// Region is the SIGNING region. With no Endpoint it also selects the AWS
// endpoint, so a typo there is a request signed for somewhere else.
Region string `yaml:"region"`
// Prefix isolates one deployment inside a shared bucket. Archives land under
// <prefix>/<deployment-id>/<created-at>/, so IAM can be scoped by prefix and
// two deployments cannot read each other's credentials.
Prefix string `yaml:"prefix,omitempty"`
// Endpoint overrides the AWS endpoint billet derives from Region, for an
// S3-compatible store: Ceph RGW — which billet's own reference hardware
// already runs — MinIO, or R2. Addressing is PATH style against it
// (<endpoint>/<bucket>/<key>), because virtual-host style is what MinIO does
// not do by default.
Endpoint string `yaml:"endpoint,omitempty"`
// KMSKeyID selects a customer-managed key for server-side encryption. Empty
// uses SSE-S3 (AES256), which every S3-compatible store supports.
//
// AN ARCHIVE IS TWO PRIVATE KEYS AND A LEDGER, so what encrypts it at rest is
// a real decision rather than a detail — and the one thing billet can enforce
// from here is that it asks for encryption at all.
KMSKeyID string `yaml:"kms_key_id,omitempty"`
}
BackupS3Config names the bucket and how to reach it.
type ByteSize ¶
type ByteSize int64
ByteSize is a byte count that reads and writes human units in YAML, so tiers can say `memory: 32GiB` instead of a nine-digit number.
Both IEC (KiB/MiB/GiB/TiB, powers of 1024) and SI (KB/MB/GB/TB, powers of 1000) suffixes are accepted and are NOT treated as equivalent — 1GB is 1_000_000_000 and 1GiB is 1_073_741_824. Memory sizing runs close enough to a machine's real limits that silently conflating them would matter.
Parsing is exact integer arithmetic on a deliberately restricted grammar. An earlier version used strconv.ParseFloat, which accepts "NaN", "Inf", hexadecimal floats, and exponent notation, and which loses precision above 2^53 — converting any of those to int64 is implementation-defined and can produce a negative size. A negative or wrapped ceiling silently disables the capacity check that stops billet overcommitting the machine, so this parser rejects anything it cannot represent exactly. MarshalYAML must work on values so a ByteSize field formats and marshals without the caller taking its address. Mixed receivers are correct here.
const ( // DefaultBuildKitCacheMountLimit bounds one BuildKit cache mount when a tier // does not choose a tighter policy. DefaultBuildKitCacheMountLimit ByteSize = 20 * GiB // MaxBuildKitCacheMountLimit is the largest volume the sticky-disk API can // create, so a larger per-mount number could never constrain anything. MaxBuildKitCacheMountLimit ByteSize = 100 * GiB )
MinMacOSGuestMemory is the smallest macOS guest Apple's hypervisor will start, and it is a MEASUREMENT rather than a recommendation.
Virtualization.framework refuses anything below it outright — "LessThanMinimalResourcesError: VM should have 4294967296 bytes of memory at minimum" — so a smaller tier is a config error, not a small guest. Recorded against the reference Mac in docs/reference/reference-hardware.md, the same way DefaultMacOSVMLimit is pinned to the refusal a third concurrent guest gets.
ENFORCED HERE AND NOWHERE ELSE, and that is forced rather than chosen: provider.Spec carries no guest OS, so the tart backend's checkSpec cannot ask this question at the launch boundary. Config validation is the only layer that knows a tier is macOS, so do not add a second half-copy under the provider — it could only guess.
func DetectHostCapacity ¶
DetectHostCapacity reports what this machine has, for a node that did not say what it will contribute.
A DEFAULT, NOT A POLICY. The node's own config wins when it sets max_vcpu or max_memory, because the operator of that machine is the one who knows what else runs on it. This is what billet assumes when nobody has said.
THERE IS NO FALLBACK VALUE ON AN UNSUPPORTED PLATFORM, deliberately. Each platform supplies detectTotalMemory in its own file, so a GOOS billet has not been taught about fails to COMPILE rather than registering a node that contributes zero — or worse, contributes a number nobody chose. A node whose contribution is unknown is how one mis-sized machine silently absorbs a fleet.
func ParseByteSize ¶
ParseByteSize parses a size like "32GiB", "512 MB", "1024", or "1.5GiB". A bare number is bytes.
Fractional values are allowed only when they land on a whole number of bytes: "1.5GiB" is exact, "0.1KiB" is not and is rejected rather than silently truncated to 102.
func (ByteSize) MarshalYAML ¶
MarshalYAML writes the human form so a rewritten config stays readable.
type CacheScope ¶
type CacheScope struct {
Owner string `yaml:"owner"`
Repository string `yaml:"repository"`
WorkflowRef string `yaml:"workflow_ref"`
}
CacheScope is the authenticated identity an intercepted Actions cache uses.
type CephConfig ¶
type CephConfig struct {
// ConfPath is the ceph.conf naming this cluster's monitors. Empty means Ceph's
// own search path, which finds /etc/ceph/ceph.conf.
ConfPath string `yaml:"conf_path,omitempty"`
// User is the RADOS identity billet authenticates as, WITHOUT the `client.`
// prefix.
//
// DEFAULTS TO billet RATHER THAN admin, which is what the rbd command picks on
// its own. An admin key can delete a pool, so defaulting to it would make a
// compromised node able to destroy the cluster it caches in rather than the
// images it was handed — and it would do so silently, because everything works.
User string `yaml:"user,omitempty"`
// KeyringPath holds that identity's secret. Empty means Ceph's own search path,
// which finds /etc/ceph/ceph.<user>.keyring.
KeyringPath string `yaml:"keyring_path,omitempty"`
// ImagePool holds golden images and the per-job root clones taken from them.
ImagePool string `yaml:"image_pool"`
// CachePool holds cache volumes: sticky disks, build state, the layer cache.
//
// SEPARATE FROM ImagePool, and validation refuses one name for both. Not for
// tuning — RBD's object size is a per-IMAGE property, so the reason ZFS wanted
// two datasets does not survive the move. It is about blast radius: a cache is
// disposable and a golden image is not, so "throw the cache away" has to be
// something an operator can do to a whole pool without taking the images with
// it, and a garbage collector walking one pool must not be able to reach the
// other.
CachePool string `yaml:"cache_pool"`
}
CephConfig points this host at the Ceph cluster its site keeps.
STORAGE BELONGS TO THE SITE, NOT TO THE COMPUTE BACKEND, which is why this is a sibling of the firecracker block rather than a field inside it. Golden images, per-job root clones and every cache in the plane are RBD images in one cluster, and the code that mounts a cache is not the code that boots a microVM. What lives here is the half a HOST holds: where the monitors are, who this machine authenticates as, and which pools that identity may use.
It replaced a `zfs_pool` key. A ZFS clone exists only on the machine that took it, so a cache written to one pinned every repository to the host that first built it — the storage half of billet being a one-machine product. RBD presents the same NVMe as a pool any node at the site can map.
func (*CephConfig) ConfPathOrDefault ¶
func (p *CephConfig) ConfPathOrDefault() string
ConfPathOrDefault names the file this configuration reaches the cluster through, so an operator reading `billet check` can see WHICH cluster answered.
An empty conf_path is not "no file" — it is Ceph's search path, which finds DefaultCephConf. Printing the empty string there tells an operator with two clusters nothing at all.
type CodeBuildConfig ¶
type CodeBuildConfig struct {
// Region is which AWS region to build in. It also selects the API endpoint,
// so an ordinary install configures no host of its own.
Region string `yaml:"region"`
// Endpoint overrides the API endpoint billet derives from Region — for a VPC
// interface endpoint, a non-commercial partition, or a test.
Endpoint string `yaml:"endpoint,omitempty"`
// Project is the CodeBuild project billet starts builds in, and it must be
// DEDICATED to this deployment and this node.
//
// THE PROJECT IS HALF THE OWNERSHIP BOUNDARY, because a CodeBuild build cannot
// be tagged: `StartBuild` has no field that becomes one, so the per-instance
// `sh.billet.owner` tag the ec2 backend filters `List` on does not exist here.
// What replaces it is this project plus per-build markers read back through
// BatchGetBuilds — and `List` feeds a loop that STOPS builds, so a project
// shared with an ordinary CodeBuild workload is a way for billet to stop
// somebody else's build.
Project string `yaml:"project"`
// FleetARN selects a reserved-capacity fleet. Empty means on-demand compute.
//
// ITS PRESENCE IS ALSO A STATEMENT ABOUT ISOLATION, which is why the provider
// reads it rather than only passing it along: AWS documents a reserved
// instance as remaining alive between builds and as sharing cached data with
// other projects in the account, by design. macOS is reserved-only, so every
// macOS build inherits that.
//
// A fleetOverride ALSO discards the project's VPC configuration — the fleet's
// own network governs — so a network reviewed on the project proves nothing
// about a build that named a fleet.
FleetARN string `yaml:"fleet_arn,omitempty"`
// EnvironmentType is the CodeBuild environment builds run in, and it is what
// this node's guest OS is DERIVED from.
EnvironmentType CodeBuildEnvironment `yaml:"environment_type"`
// ComputeTypes are the compute types billet may buy, each DECLARING what it
// holds, for the same reason node.ec2.instance_types are declared: billet
// ships no table of them, and being out of date here means starting a build
// smaller than the lease the allocator already escrowed.
//
// ORDERED, most preferred first, and placement charges the first entry that
// fits rather than the smaller tier request.
ComputeTypes []RemoteShape `yaml:"compute_types"`
// AcceptExternalBuildCeiling acknowledges that every job on this node inherits
// CodeBuild's ceilings, which billet cannot lift.
//
// NO DEFAULT, AND ITS ABSENCE IS THE REFUSAL — the same shape as
// node.firecracker.untrusted_bridge and node.ec2.untrusted_security_group_ids.
// It is not a feature flag: nothing changes when it is set. It exists so that
// the sentence "this tier cannot run a job longer than 36 hours" is read by a
// person before a tier advertises capacity, rather than discovered from a build
// that died at hour 36 with GitHub reporting a failed job.
AcceptExternalBuildCeiling bool `yaml:"accept_external_build_ceiling"`
// BuildTimeoutMinutes is the ceiling billet asks CodeBuild for, 5 to 2160.
//
// THE OPERATOR'S NUMBER, NOT BILLET'S. billet adds no deadline of its own and
// no drain or upgrade ever stops a build for taking too long; this is passed
// through as timeoutInMinutesOverride. It defaults to the maximum, so a config
// that says nothing gets the longest job CodeBuild permits.
//
// It also SIZES THE INVENTORY WINDOW, which is the one non-obvious consequence.
// CodeBuild cannot list only active builds, so `List` walks recent history and
// stops once every build it sees is older than this plus the queued ceiling —
// at which point CodeBuild has necessarily ended them. Declaring a tighter
// ceiling therefore makes this node's inventory cheaper; declaring the maximum
// is supported and costs a longer walk.
BuildTimeoutMinutes int `yaml:"build_timeout_minutes,omitempty"`
// QueuedTimeoutMinutes is how long a build may wait for capacity, 5 to 480,
// after which CodeBuild FAILS it. Defaults to the maximum.
QueuedTimeoutMinutes int `yaml:"queued_timeout_minutes,omitempty"`
// JITParameterPath is the SSM Parameter Store path prefix billet writes each
// build's single-use runner registration under.
//
// REQUIRED, because it is an IAM boundary rather than a naming preference: the
// node's policy grants ssm:PutParameter and ssm:DeleteParameter on exactly this
// path, so a value billet guessed would either be unwritable or — worse — wider
// than the grant an operator reviewed.
//
// THE REGISTRATION GOES HERE RATHER THAN INTO THE LAUNCH REQUEST because every
// StartBuild field is rendered in the console and in CloudTrail. What travels in
// the request is the parameter's NAME; the value is resolved into the build by
// CodeBuild itself, under the BUILD's service role, which is a different
// principal from this node's.
JITParameterPath string `yaml:"jit_parameter_path"`
// JITKMSKeyID selects the customer-managed key SecureString parameters are
// encrypted with. Empty uses the account's aws/ssm key.
JITKMSKeyID string `yaml:"jit_kms_key_id,omitempty"`
// LogGroup pins where a build's logs go. Empty leaves the project's own
// configuration alone.
LogGroup string `yaml:"log_group,omitempty"`
// PrivilegedMode grants the build the privilege Docker needs, and is only
// meaningful for a CONTAINER environment — an EC2 or macOS environment is the
// machine, so there is nothing to privilege and setting it is refused rather
// than ignored.
//
// A GitHub Actions job routinely runs `docker build` and service containers, so
// a container-environment tier that leaves this off produces jobs that fail on
// their first Docker step. It is not defaulted on, because it is a real
// privilege grant and billet does not hand one out on somebody's behalf.
PrivilegedMode bool `yaml:"privileged_mode,omitempty"`
// UntrustedVPCID, UntrustedSubnetIDs and UntrustedSecurityGroupIDs name the
// isolated network a fork pull-request build runs in, and THEIR ABSENCE IS THE
// REFUSAL — the same shape as node.ec2.untrusted_security_group_ids and
// node.firecracker.untrusted_bridge. A build container isolates the kernel, not
// the network, and a subnet somebody already had usually reaches more than they
// are picturing.
//
// UNLIKE ec2, THE NETWORK LIVES ON THE PROJECT, because StartBuild has no VPC
// override — CodeBuild sets a build's network from the project's vpcConfig (and a
// fleetOverride discards even that). So these three fields are what the project
// billet launches into was created with, and the provider VERIFIES the project
// carries exactly this network before it starts an untrusted build. Declaring
// them is only meaningful on an on-demand container node: reserved capacity is
// shared between builds and macOS is reserved-only, so an untrusted build is
// refused there whatever the network says, and declaring a network beside
// fleet_arn or a reserved environment_type is refused as dead config.
UntrustedVPCID string `yaml:"untrusted_vpc_id,omitempty"`
UntrustedSubnetIDs []string `yaml:"untrusted_subnets,omitempty"`
UntrustedSecurityGroupIDs []string `yaml:"untrusted_security_group_ids,omitempty"`
}
CodeBuildConfig configures the AWS CodeBuild backend: one build per job, in one project.
LIKE node.ec2, EVERY LOAD-BEARING FIELD IS A DECISION SOMEBODY HAS TO MAKE and none of them is defaulted. billet cannot pick a project, and it will not pick a compute type on somebody's account.
UNLIKE node.ec2, ONE FIELD EXISTS PURELY SO A LIMIT CANNOT BE A SURPRISE. CodeBuild caps a build at 36 hours and fails a queued build after at most 8, and billet can lift neither — so accept_external_build_ceiling has no default and its absence is a refusal. The alternative is an operator meeting the ceiling for the first time as a 36-hour build that died, on a backend documented as the fallback that keeps CI working.
func (*CodeBuildConfig) HasUntrustedNetwork ¶
func (b *CodeBuildConfig) HasUntrustedNetwork() bool
HasUntrustedNetwork reports whether all three untrusted-network fields are set, which is the only shape that admits untrusted work. A PARTIAL set is not "half configured" — it is a config error CheckCodeBuild refuses, the same rule as ec2's use_vpc needing both a subnet and a group.
func (*CodeBuildConfig) InventoryWindowMinutes ¶
func (b *CodeBuildConfig) InventoryWindowMinutes() int
InventoryWindowMinutes is how far back `List` and `Find` must look before an absence is conclusive.
DERIVED FROM THE DECLARED CEILINGS RATHER THAN CHOSEN, because it is the only bound available: CodeBuild offers no way to list active builds and retains a year of history, so what makes the walk finite is that the service itself ends a build once these two elapse. A build older than their sum cannot still be running.
The slack is deliberate and generous. It covers the gap between billet asking and CodeBuild acting, and getting it wrong in the short direction means reading a RUNNING build as absent — which frees capacity for compute that is still executing somebody's job. Getting it wrong long costs one extra page.
func (*CodeBuildConfig) LogGroupName ¶
func (b *CodeBuildConfig) LogGroupName() string
LogGroupName is the CloudWatch group this node's builds write to.
DERIVED WHEN NOTHING NAMES ONE, and there is exactly one right answer to derive: CodeBuild's own default group for a project is /aws/codebuild/<project>. Both the launch path and the IAM renderer need this, and they must not disagree — the build role's grant is scoped to a group ARN, so a policy naming one group while the build writes to another is a role that cannot write its own logs.
EXPORTED FROM config FOR THE SAME REASON CheckCodeBuild IS: two derivations of one value is one derivation that is wrong, and config is the leaf both sides already read.
func (*CodeBuildConfig) Prepare ¶
func (b *CodeBuildConfig) Prepare()
Prepare normalizes and defaults a CodeBuild block, and it is the ONE place that does either.
EXPORTED AND CALLED FROM BOTH SIDES, which is the rule CheckCodeBuild already follows and for a sharper reason. Load prepares before it validates; the provider's exported constructor cannot assume its configuration came through Load, and the first version reproduced only part of this by hand — it trimmed the scalars and missed the compute-type NAMES, which validation checks trimmed and the launch sends raw, and the timeout DEFAULTS, without which a caller that omitted them passed validation (zero means "not stated") and sent a zero override AWS refuses. Two of the shapes this repository keeps finding, in one function: a check that examines a copy the consumer does not use, and a default applied on one of two entry points.
IT MUST RUN BEFORE VALIDATION on both paths, because the defaults it fills in are what the range checks then judge.
type CodeBuildEnvironment ¶
type CodeBuildEnvironment string
CodeBuildEnvironment is a CodeBuild environment type billet is willing to run a job in.
A CLOSED SET DRAWN FROM WHAT StartBuild ACCEPTS, minus the ones that cannot run a GitHub Actions job. It is not the full enum on purpose — see checkCodeBuildEnvironment for what each exclusion costs.
const ( // CodeBuildLinuxContainer is the ordinary x86-64 container environment. // Docker inside the job needs privileged_mode. CodeBuildLinuxContainer CodeBuildEnvironment = "LINUX_CONTAINER" // CodeBuildARMContainer is the arm64 container environment. CodeBuildARMContainer CodeBuildEnvironment = "ARM_CONTAINER" // CodeBuildLinuxGPUContainer is the GPU container environment. Its default // account quota is ZERO, so a tier on it advertises capacity nothing can run // until the quota is raised. CodeBuildLinuxGPUContainer CodeBuildEnvironment = "LINUX_GPU_CONTAINER" // CodeBuildLinuxEC2 runs directly on an EC2 instance rather than in a // container, so Docker works without privileged_mode. Reserved capacity only. CodeBuildLinuxEC2 CodeBuildEnvironment = "LINUX_EC2" // CodeBuildARMEC2 is the arm64 form of the same. CodeBuildARMEC2 CodeBuildEnvironment = "ARM_EC2" // CodeBuildMacARM is AWS-managed Apple silicon, and the whole reason this // backend reaches macOS at all. RESERVED CAPACITY ONLY — on-demand fleets do // not offer macOS — so it requires fleet_arn. CodeBuildMacARM CodeBuildEnvironment = "MAC_ARM" )
func (CodeBuildEnvironment) Container ¶
func (e CodeBuildEnvironment) Container() bool
Container reports whether this environment runs the job inside a container, in which case Docker inside the job needs privileged mode. An EC2 or macOS environment IS the machine, so there is nothing to privilege.
func (CodeBuildEnvironment) GuestOS ¶
func (e CodeBuildEnvironment) GuestOS() GuestOS
GuestOS is what a build in this environment boots.
DERIVED RATHER THAN CONFIGURED, because the two would then be two authorities for one fact: an operator who wrote `environment_type: MAC_ARM` beside `guest_os: [linux]` would have a node that advertises Linux and starts macOS builds. What a node REPORTS at registration comes from here, and placement's durable check (alloc.Bind) is what a node cannot route around.
func (CodeBuildEnvironment) ReservedOnly ¶
func (e CodeBuildEnvironment) ReservedOnly() bool
ReservedOnly reports whether this environment exists only on a reserved-capacity fleet, so a config naming it without a fleet describes builds AWS will refuse.
func (CodeBuildEnvironment) Valid ¶
func (e CodeBuildEnvironment) Valid() bool
Valid reports whether this is an environment type billet runs jobs in.
type Config ¶
type Config struct {
// Server is required by `billet server`. A node reads it too when the two
// share a file on one machine: it is where a certless node learns which
// deployment it is joining.
Server *ServerConfig `yaml:"server,omitempty"`
// Node is required by `billet node`, ignored by a pure server.
Node *NodeConfig `yaml:"node,omitempty"`
// GitHub is the target named DefaultTargetName: the one organization or
// repository a single-target deployment serves. `billet server` needs it or
// at least one entry under Targets.
GitHub *GitHubConfig `yaml:"github,omitempty"`
// Targets are the further organizations and repositories this deployment
// serves, each with its own App credential. See GitHubTargets.
Targets []GitHubConfig `yaml:"targets,omitempty"`
// Tiers is the runner catalog. Each tier becomes one GitHub scale set, and
// its Label is what users put in `runs-on`.
Tiers []Tier `yaml:"tiers,omitempty"`
// Backup is where archives go when they leave this disk. Optional: absent
// means `billet local backup --out <dir>` is the whole story and an
// operator's own tooling carries the directory.
Backup *BackupConfig `yaml:"backup,omitempty"`
// Nodes describes per-host policy to the server. Separate from the Node section
// on purpose: Node is how a host describes ITSELF, while Nodes is how the control
// plane describes the FLEET — and host policy has to live server-side, because
// the limits it expresses are enforced across tiers the host never sees.
//
// Every field defaults, so a deployment wanting standard behaviour omits it.
Nodes []NodePolicy `yaml:"nodes,omitempty"`
// Sites are the places this deployment has compute in. Optional: a single-machine
// deployment never writes one.
//
// A SITE IS WHERE COMPUTE AND ITS STORAGE SHARE A FAST NETWORK — the answer to
// "which storage", which every cache needs one of.
//
// DECLARED RATHER THAN INFERRED FROM WHAT NODES SAY, because the failure a free
// string produces is silent: a node that means "home" and types "hom" would get
// its own site, with its own empty cache, and every job placed there would run
// cold while looking perfectly healthy.
Sites []SiteConfig `yaml:"sites,omitempty"`
// Images says where published guest images are fetched from. Optional: a
// deployment that omits it pulls from where billet publishes.
Images *ImagesConfig `yaml:"images,omitempty"`
// Release says how this deployment learns about new billet releases.
//
// Optional, and its absence is the behaviour every existing install already
// has: follow the signed stable channel, and update only when an operator
// asks. See ReleaseConfig — nothing here starts a rollout by itself unless a
// deployment says so in a sentence.
Release *ReleaseConfig `yaml:"release,omitempty"`
// contains filtered or unexported fields
}
Config is the whole of billet.yaml.
func Parse ¶
Parse decodes and validates a config from bytes, naming it in diagnostics.
The bytes half of Load, so a caller that already holds the config — a generator validating what it just rendered — can run it through the exact same decode, defaulting and validation as a file on disk, rather than a second copy of the rules that drifts from this one.
func (*Config) GitHubTarget ¶ added in v0.10.0
func (c *Config) GitHubTarget(name string) (GitHubTarget, bool)
GitHubTarget resolves a target by name.
func (*Config) GitHubTargets ¶ added in v0.10.0
func (c *Config) GitHubTargets() []GitHubTarget
GitHubTargets is every target this config serves, the `github:` block first as DefaultTargetName and then the `targets:` list in its written order.
EVERY READER OF cfg.GitHub GOES THROUGH HERE. A reader of the block alone serves one target and silently ignores the rest, which for a backup is a credential never captured and for a check is a target never verified.
func (*Config) MacOSFleetProvider ¶
func (c *Config) MacOSFleetProvider(node string) ProviderKind
MacOSFleetProvider names the remote backend a node reaches macOS through, or "" for a Mac somebody owns (or a node nothing describes).
Exported for `billet check`, whose policy line has to say which agreement a missing macos_vm_limit falls under rather than attribute Apple's number to a fleet AWS operates.
func (*Config) MacOSLimitForNode ¶
MacOSLimitForNode is the effective cap on concurrent macOS guests for a host.
func (*Config) NodePolicies ¶
func (c *Config) NodePolicies() map[string]NodePolicy
NodePolicies is the declared fleet policy keyed by node name. The allocator is built from it, so runtime enforcement and this package's load-time guard read the same rules rather than two copies that can drift.
Only DECLARED hosts appear; an absent host is unconstrained in guest OS and carries Apple's default macOS limit.
func (*Config) NodePolicyFor ¶
func (c *Config) NodePolicyFor(name string) (NodePolicy, bool)
NodePolicyFor returns the policy for a named host, and whether one was declared. The zero NodePolicy is the documented default — unconstrained guest OS, Apple's standard macOS limit — so the returned value is usable either way and callers only need the boolean when they care about the distinction.
func (*Config) TierByLabel ¶
TierByLabel returns the tier a `runs-on` label refers to.
func (*Config) TierTarget ¶ added in v0.10.0
func (c *Config) TierTarget(t *Tier) (GitHubTarget, bool)
TierTarget resolves the target a tier belongs to.
An empty tier target resolves to the deployment's only target, which is what applyDefaults writes down; asked here as well because a Tier reaches TierTargetPolicyErrors from alloc.New too, on a catalogue that never went through Parse.
type Contribution ¶
type Contribution struct {
// VCPU and Memory are what the allocator may place work against.
VCPU int
Memory ByteSize
// Warnings are things the operator should know but that are not errors —
// billet is doing what it was told, and what it was told looks like a typo.
Warnings []string
}
Contribution is what one node offers the deployment, and how that was decided.
type ControllerMode ¶
type ControllerMode string
ControllerMode says how many control planes this deployment runs, and therefore what a controller does when it finds the claim already held.
const ( // ControllersSingle is the default and what every deployment has today: one // control plane, and a second one is a MISTAKE that says so loudly. It exits // non-zero naming the machine that holds the claim, and `Restart=on-failure` // repeats that refusal every RestartSec until somebody fixes it. ControllersSingle ControllerMode = "single" // ControllersActivePassive says this deployment runs more than one control // plane on purpose. Whichever takes the claim first is the controller; the // others WAIT, and one of them takes over when the incumbent's database // session ends. // // BOTH HOSTS WRITE THE SAME VALUE, and that symmetry is the reason it is a // property of the deployment rather than a flag on one process. After a // failover the standby IS the controller, so a per-process spelling would // leave a file describing a role its host no longer has. // // IT IS NOT AUTOMATIC, and the diagnostic is why. If waiting were what every // refused controller did, two machines misconfigured as active would stop // being a loud restart loop and become a deployment that looks healthy and // has quietly halved itself. ControllersActivePassive ControllerMode = "active-passive" )
type EBSS3Config ¶
type EBSS3Config struct {
// Region is the signing region for both services and must match node.ec2.
Region string `yaml:"region"`
// AvailabilityZone is where cache volumes are created. EBS volumes and the
// instances consuming them must be in the same zone.
AvailabilityZone string `yaml:"availability_zone"`
// Bucket holds the atomic pointer, lease and fencing state objects.
Bucket string `yaml:"bucket"`
// Prefix isolates one deployment and site inside a bucket.
Prefix string `yaml:"prefix,omitempty"`
// KMSKeyID optionally selects one customer-managed key for EBS volumes and
// snapshots. Empty uses the account's EBS encryption default key.
//
// A PER-DEPLOYMENT key is what closes the cross-deployment READ boundary:
// the IAM tag conditions cannot stop another deployment cloning a snapshot
// and reading the cache (ec2:CreateVolume does not authorize the parent
// snapshot; the value-scoped conditions give destructive integrity only).
// With each deployment's snapshots encrypted under its own key — whose key
// policy delegates to IAM and admits no foreign role — and its role's KMS
// grants scoped to exactly that key, as `billet init iam` and the terraform
// module both do, the foreign clone fails at the KMS grant instead
// (measured with iam:SimulateCustomPolicy: every KMS action on another
// deployment's key is implicitly denied). Sharing one key between
// deployments silently reopens that boundary — as does a key whose own
// policy or grants admit other roles, which no identity policy can see.
// Leaving this EMPTY encrypts under the ACCOUNT's default EBS key — the
// AWS-managed aws/ebs unless the account configured another — and aws/ebs
// authorizes any principal in the account through EC2, so an opted-out
// deployment's snapshots stay readable no matter what keys its neighbours
// set. A key protects only snapshots created after it was set; evict or
// re-snapshot older generations to bring them under it.
KMSKeyID string `yaml:"kms_key_id,omitempty"`
}
EBSS3Config points a cloud node at its site's EBS and S3 cache storage.
IT EXISTS ONLY TO BACK node.cache. Nothing on a running node reads it by any other route, so without that listener every field here is inert and every job runs on the instance's root volume — which `billet check` refuses rather than config load, because `billet decommission` purges what the cache left behind and `billet init iam` renders its grants, and both read this block on a config whose listener is already gone.
type EC2Config ¶
type EC2Config struct {
// Region is which AWS region to launch in. It also selects the API endpoint,
// so an ordinary install configures no host of its own.
Region string `yaml:"region"`
// Endpoint overrides the API endpoint billet derives from Region — for a VPC
// interface endpoint, a non-commercial partition, or a test.
Endpoint string `yaml:"endpoint,omitempty"`
// SubnetID is where instances are launched. Its route to GitHub is the
// operator's to arrange: a private subnet needs a NAT gateway, a public one
// needs AssignPublicIP.
SubnetID string `yaml:"subnet_id"`
// SecurityGroupIDs apply to trusted work.
SecurityGroupIDs []string `yaml:"security_group_ids"`
// UntrustedSecurityGroupIDs apply to fork pull-request work, and their
// ABSENCE is what refuses it.
//
// A whole instance is a real isolation boundary, which is why this backend can
// run code billet cannot vouch for at all — but that boundary is the KERNEL,
// not the network. A fork's job in the same security group as everything else
// reaches whatever that group reaches, which on a subnet somebody already had
// is usually more than they are picturing. So untrusted work runs only once
// its network has been described separately, rather than defaulting onto the
// trusted group because nobody said otherwise.
UntrustedSecurityGroupIDs []string `yaml:"untrusted_security_group_ids,omitempty"`
// AssignPublicIP gives instances a public address, for a subnet with no NAT
// gateway. A runner that cannot reach GitHub registers and then does nothing.
AssignPublicIP bool `yaml:"assign_public_ip,omitempty"`
// InstanceProfile is the IAM role TRUSTED instances receive. OPTIONAL, and
// empty is the right answer unless a job genuinely needs AWS credentials: an
// instance profile is readable from inside the guest, so it is a credential
// handed to whatever the job runs.
//
// UNTRUSTED WORK NEVER GETS IT, whatever this says. A fork's pull request runs
// its steps directly on the instance, so it could read the role's temporary
// credentials out of the metadata service — past the isolation that lets this
// backend run untrusted work at all.
InstanceProfile string `yaml:"instance_profile,omitempty"`
// InstanceTypes are the shapes billet may buy, each DECLARING what it holds,
// because billet ships no table of EC2 instance types.
//
// A table would be out of date within a quarter — AWS adds types continuously
// — and being out of date here means launching a machine that does not fit a
// lease the allocator has already escrowed. Declaring them keeps the fleet's
// cost surface in the operator's own file, which is where a spending decision
// belongs anyway.
InstanceTypes []EC2InstanceType `yaml:"instance_types"`
// Spot buys interruptible capacity.
//
// DEFAULTS OFF, which reverses the assumption this backend was filed under.
// It exists so one `runs-on` label survives the bare-metal host going away,
// and GitHub does not requeue a job whose runner vanished mid-execution — so a
// spot reclaim is a FAILED BUILD rather than a retry. Defaulting to spot would
// make the failover path the unreliable one, which is the opposite of what a
// failover is for. An operator who would rather have a cheap build that
// sometimes dies says so here.
Spot bool `yaml:"spot,omitempty"`
// InterruptionQueueURL receives EventBridge's EC2 Spot interruption warnings.
// Required with Spot: without it a reclaim is an unexplained failed build.
InterruptionQueueURL string `yaml:"interruption_queue_url,omitempty"`
// NodeName is filled from the effective node identity before the provider is
// constructed. It is not a second operator-configured identity.
NodeName string `yaml:"-"`
}
EC2Config configures the cloud backend: one instance per job, in one subnet.
EVERY FIELD HERE IS A PLACEMENT DECISION SOMEBODY HAS TO MAKE, and none of the load-bearing ones is defaulted. billet cannot pick a subnet, and a wrong guess is either a job that cannot reach GitHub or one that can reach a production database.
type EC2InstanceType ¶
type EC2InstanceType struct {
Type string `yaml:"type" json:"type"`
VCPU int `yaml:"vcpu" json:"vcpu"`
Memory ByteSize `yaml:"memory" json:"memory"`
// PriceUSDPerHour is the operator-audited compute rate used to report the
// maximum configured exposure. It is required because the answer has to be in
// the config, not fetched from a mutable service when a job arrives. It is not
// an admission gate and cannot make an already-accepted job wait because a
// copied price went stale.
PriceUSDPerHour USDPerHour `yaml:"price_usd_per_hour" json:"price_usd_per_hour"`
}
EC2InstanceType is one shape billet may buy, and what it holds.
The vCPU and memory are DECLARED rather than looked up, because the allocator has already escrowed a size against this node before any of this is consulted: a shape that turns out smaller than the lease it was chosen for over-commits a machine nobody can see.
It carries the ORIGINAL name because the ledger column, the registration field and every existing config say `ec2`, and renaming a shipped on-disk spelling to tidy a Go identifier is a flag day for nothing. `RemoteShape` above is the alias a second remote backend uses.
type FirecrackerConfig ¶
type FirecrackerConfig struct {
// BinaryPath and JailerPath locate the firecracker and jailer binaries.
//
// BinaryPath IS NOT ONLY AN EXECUTABLE. The jailer names its chroot after this
// file AFTER RESOLVING SYMLINKS, so it also decides the directory billet
// enumerates to find out what is running here — see the firecracker provider's
// jail type, where getting it wrong reads as an empty inventory.
BinaryPath string `yaml:"binary_path,omitempty"`
JailerPath string `yaml:"jailer_path,omitempty"`
// KernelImage is the uncompressed guest kernel. It must be built with
// everything Docker needs; validate with moby's contrib/check-config.sh
// rather than a hand-maintained list.
KernelImage string `yaml:"kernel_image"`
// KernelDir is where `billet images pull` keeps the kernels it fetches.
//
// SEPARATE FROM KernelImage BECAUSE THEY ANSWER DIFFERENT QUESTIONS. KernelImage
// is the fallback for a generation that records no kernel of its own — a
// hand-built image, whose builder installs none. This is where a PAIRED kernel is
// looked up, and a generation that names one boots that one instead: the two are
// published together and a mismatch fails inside somebody's job rather than at
// launch.
//
// Empty means the default, which is where a pull puts them.
KernelDir string `yaml:"kernel_dir,omitempty"`
// ChrootBase is where the jailer builds each microVM's chroot. It defaults to
// the jailer's own default, and it must be on local storage with room for a
// hard link to the guest kernel per running job.
ChrootBase string `yaml:"chroot_base,omitempty"`
// JailUIDMin and JailUIDCount are the range of uids microVMs run as, ONE PER
// GUEST.
//
// NOT ONE ACCOUNT FOR ALL OF THEM. The jailer drops each VMM to a uid, and a
// shared one means every VMM on the host is the same user to the kernel — so a
// VMM that escapes its chroot can reach every other jail's files, signal every
// other VMM, and open every other guest's root disk. The chroot is what
// separates them while it holds; a uid of its own is what separates them when
// it does not.
//
// THEY ARE NUMBERS, NOT ACCOUNTS, and deliberately: an account a person could
// log in as is a bigger thing than an integer the kernel uses to keep two
// processes apart, and creating one per job is a deployment step per job. The
// default range is far above anything a distribution allocates.
JailUIDMin int `yaml:"jail_uid_min,omitempty"`
JailUIDCount int `yaml:"jail_uid_count,omitempty"`
// Bridge is the host bridge trusted guests attach to.
Bridge string `yaml:"bridge"`
// UntrustedBridge is the bridge fork pull-request guests attach to, and its
// ABSENCE is what refuses them.
//
// A microVM is a real isolation boundary, which is why this backend can run
// code billet cannot vouch for at all — but that boundary is the KERNEL, not
// the network. A guest on the ordinary bridge reaches whatever that bridge
// reaches, which on a machine that also holds the Ceph cluster and the
// control-plane database is everything that matters. So untrusted work runs
// only once its network has been described separately, rather than defaulting
// onto the trusted bridge because nobody said otherwise. This is the same rule,
// and the same reasoning, as node.ec2.untrusted_security_group_ids.
UntrustedBridge string `yaml:"untrusted_bridge,omitempty"`
// ImageVerifyPort is the host port a verification guest reports to. It is
// fixed so host policy can admit exactly this service instead of opening an
// arbitrary high port to every guest. Only one verification runs per host,
// enforced by the verification lock.
ImageVerifyPort int `yaml:"image_verify_port,omitempty"`
}
FirecrackerConfig configures the bare-metal microVM backend.
func (*FirecrackerConfig) Normalize ¶
func (f *FirecrackerConfig) Normalize()
Normalize fills in the defaults and trims what billet later passes verbatim.
EXPORTED FOR THE SAME REASON CheckFirecracker IS. The provider's constructor is exported and cannot assume its configuration came through Load, and a value that was trimmed for a CHECK while the caller used the raw one is the exact defect the ec2 and ceph blocks each shipped with once.
type GitHubConfig ¶
type GitHubConfig struct {
// Name is the target's name under `targets:`, what a tier's `target` names.
// Refused under `github:`, whose name is DefaultTargetName.
Name string `yaml:"name,omitempty"`
// Org is the organization this target is. Exactly one of Org and Repository.
Org string `yaml:"org,omitempty"`
// Repository is the repository this target is, as owner/name. Its runners
// belong to the repository alone: a repository has no runner groups, so a
// tier under it is untrusted only.
Repository string `yaml:"repository,omitempty"`
AppID int64 `yaml:"app_id"`
// ClientID is the App's OAuth client identifier, and it is OPTIONAL.
//
// GitHub's newer guidance prefers it over the numeric app id as the JWT
// issuer, and the scale-set client accepts either — its GitHubAppAuth
// documents ClientID as "the Client ID of the application (app id also
// works)". So this must never become required: every config written before
// the field existed keeps working.
//
// It is recorded because the manifest conversion already returns it, and
// throwing away a value GitHub handed over means a second trip through the
// browser to get it back. It is an identifier, not a secret — App.Forget
// deliberately keeps it while blanking the client SECRET beside it.
ClientID string `yaml:"client_id,omitempty"`
InstallationID int64 `yaml:"installation_id"`
// PrivateKeyPath points at the App private key PEM. This file is the single
// most sensitive thing in a billet deployment: it lives only on the control
// plane, and nodes never hold long-lived GitHub credentials.
PrivateKeyPath string `yaml:"private_key_path"`
}
GitHubConfig is one GitHub target and the App identity that manages its runners: the `github:` block, or one entry under `targets:`.
billet requests exactly two permissions per target: metadata:read and, for an organization, organization_self_hosted_runners:read+write, or for a repository, the repository permission administration:write, which is the only permission GitHub offers for registering a repository's runners. It deliberately does not request actions:read, which would expose workflow runs, logs, and artifacts.
type GitHubTarget ¶ added in v0.10.0
type GitHubTarget struct {
// Name is the target's name in config: DefaultTargetName for the `github:`
// block, or the entry's own name under `targets:`.
Name string
// Org is the organization login, for an organization target.
Org string
// Repository is owner/name, for a repository target.
Repository string
AppID int64
ClientID string
// InstallationID is the App's installation on the organization or on the
// repository's owner.
InstallationID int64
// PrivateKeyPath is where this target's App key lives, when the deployment
// keeps keys in files.
PrivateKeyPath string
}
GitHubTarget is one GitHub owner the control plane serves, with the App credential that serves it.
A VIEW over GitHubConfig rather than the block itself, so every reader sees the `github:` block and each `targets:` entry through one shape, named. The block is what the operator writes and the identity edit rewrites; this is what the server, the commands and the archive resolve a tier's credential through.
func (GitHubTarget) IsRepository ¶ added in v0.10.0
func (t GitHubTarget) IsRepository() bool
IsRepository reports whether this target is a repository.
func (GitHubTarget) KeyName ¶ added in v0.10.0
func (t GitHubTarget) KeyName(base string) string
KeyName names this target's App key in a store or a per-target file: the bare leaf for the default target, so every deployment written before targets existed keeps its key where it was, and a suffixed one for the rest.
func (GitHubTarget) Owner ¶ added in v0.10.0
func (t GitHubTarget) Owner() string
Owner is the account the target belongs to: the organization, or the repository's owner.
func (GitHubTarget) Path ¶ added in v0.10.0
func (t GitHubTarget) Path() string
Path is the target's GitHub path: `owner` for an organization, `owner/name` for a repository.
THE IDENTITY OF A TARGET ON THE WIRE AND IN THE LEDGER. It is what the scale-set client's config URL is built from, what a scale set record is keyed by and what an archive names, because a target's config NAME is the operator's label and may be renamed, while the path names the thing on GitHub the scale sets actually belong to.
func (GitHubTarget) RepositoryName ¶ added in v0.10.0
func (t GitHubTarget) RepositoryName() string
RepositoryName is the repository's own name, or empty for an organization.
func (GitHubTarget) Scope ¶ added in v0.10.0
func (t GitHubTarget) Scope() TargetScope
Scope reports whether this target is an organization or a repository.
func (GitHubTarget) Where ¶ added in v0.10.0
func (t GitHubTarget) Where() string
Where names the block this target came from, for diagnostics.
type GuestOS ¶
type GuestOS string
GuestOS classifies what a tier boots.
An explicit field rather than inferred from the label, because Apple's licensing limit is enforced against it: inferring "this is macOS" from the operator's chosen label lets a tier named `sonoma-arm64` silently escape the cap.
type IdentityBackend ¶
type IdentityBackend string
IdentityBackend names where a deployment's identity material lives.
const ( // IdentityFile is the default and what every deployment has today: the // node-wire authority and the GitHub App private key are files in // identity_dir. It is the right answer for one controller, and it is the only // answer for a deployment with no AWS account. IdentityFile IdentityBackend = "file" // IdentitySSM puts them in AWS Systems Manager Parameter Store as // SecureStrings, so an active/passive pair shares one authority instead of // two copies somebody has to keep in step. // // PARAMETER STORE RATHER THAN SECRETS MANAGER, and the deciding fact is a // deletion: DeleteParameter is immediate where DeleteSecret imposes a // seven-day recovery window unless forced. billet already speaks this service, // with signing vectors in the tree, so it is one client rather than two. IdentitySSM IdentityBackend = "aws-ssm" )
func PeekIdentityBackend ¶
func PeekIdentityBackend(data []byte) IdentityBackend
PeekIdentityBackend answers where a config says its identity material lives, WITHOUT validating anything else.
IT EXISTS FOR ONE CALLER AND THE REASON IS AN ORDERING. `billet github-app create` runs against a config that is not valid yet — it has no app_id and no installation_id, because registering the App is what produces them — so Load would refuse it, and the command has to know before the browser flow whether the key it is about to receive belongs in a file or in a store. Reading one block tolerantly is the smallest thing that answers that.
EVERY FAILURE ANSWERS `file`, which is the compatible direction: a config this cannot read is one whose key goes where every config's key has always gone, and the ordinary Load that follows will produce the real diagnostic.
type IdentityConfig ¶
type IdentityConfig struct {
Backend IdentityBackend `yaml:"backend"`
// AWSSSM configures the store when Backend selects it, and is REFUSED
// otherwise rather than ignored — the same rule the `state:` block follows,
// and for the same reason: silently ignoring a block produces a deployment
// that believes it configured something.
AWSSSM *IdentitySSMConfig `yaml:"aws_ssm,omitempty"`
}
IdentityConfig is where the deployment's identity material lives.
SEPARATE FROM `state:` BECAUSE THE TWO ARE NOT INTERCHANGEABLE, which is the same sentence identity_dir already carries. A ledger is rows and can move into a database; a private key cannot follow it there, which is why the pairing is a refusal.
type IdentitySSMConfig ¶
type IdentitySSMConfig struct {
// Region is the SIGNING region, and it also selects the endpoint: there is no
// override, because an override is a way to send a deployment's private key to
// a host of somebody's choosing.
Region string `yaml:"region"`
// Prefix isolates one deployment inside an account. Everything billet stores
// lands under it, so IAM can be scoped by path and two deployments cannot read
// each other's authority.
Prefix string `yaml:"prefix"`
// KMSKeyID names the key that encrypts the SecureStrings. Empty uses the
// account's default SSM key, which is what a deployment that has not chosen
// one gets — and which is a real choice rather than an omission, because that
// key's policy is what decides who else in the account can read them.
KMSKeyID string `yaml:"kms_key_id,omitempty"`
}
IdentitySSMConfig names the Parameter Store path this deployment's identity lives under.
type ImagesConfig ¶
type ImagesConfig struct {
// Source is the directory the manifest and its assets sit in.
//
// Empty means billet's own published images. The default lives in
// internal/imagesource, next to the one constant naming this project, so a
// move does not have to be remembered in two places.
Source string `yaml:"source,omitempty"`
// SigningIdentity is the certificate SAN pattern a valid signature must carry.
//
// REQUIRED FOR A SOURCE THAT IS NOT BILLET'S OWN, because billet's identity
// cannot vouch for what somebody else's mirror serves — and the alternative to
// requiring it is silently not verifying, which is the failure this exists to
// prevent.
SigningIdentity string `yaml:"signing_identity,omitempty"`
// SigningIssuer is the OIDC issuer that certificate must come from.
//
// A SAN says who a certificate is FOR; the issuer says who vouched for it.
// Without this, any authority able to mint a certificate carrying that name
// satisfies the policy.
SigningIssuer string `yaml:"signing_issuer,omitempty"`
}
ImagesConfig points a deployment at a source of published guest images.
CONFIGURABLE FROM THE FIRST RELEASE, AND THAT IS DELIBERATE. Retrofitting a second source onto a client that hardcoded one is the specific thing that hurt other projects distributing artifacts this way: when the single origin they baked in started rate-limiting, every consumer needed a new binary before any of them could point elsewhere. A deployment that mirrors internally, or is not on the public internet at all, must be able to say so in configuration.
type MaintenanceWindow ¶
type MaintenanceWindow struct {
// Start and End are "HH:MM" in UTC.
//
// UTC RATHER THAN LOCAL, because a fleet spans machines whose local time is
// not one thing, and a window that meant something different on each host is a
// window nobody can reason about. It is also stable across a DST transition,
// which a local window is not — and the hour that repeats or vanishes is
// exactly the hour somebody chose because it is quiet.
Start string `yaml:"start"`
End string `yaml:"end"`
}
MaintenanceWindow is a daily span, in UTC, during which a rollout may begin.
type NodeCacheConfig ¶
type NodeCacheConfig struct {
// Listen is one literal, non-loopback address guests can reach. Wildcards are
// refused because they can expose the bearer-token API on another interface.
Listen string `yaml:"listen"`
// GuestEndpoint is the HTTP origin placed in guest metadata. It must name the
// same address as Listen; the per-instance bearer token authorizes every call.
GuestEndpoint string `yaml:"guest_endpoint"`
// TLSCert and TLSKey terminate the HTTPS endpoint an EC2 guest reaches across
// the VPC. Firecracker's isolated bridge uses HTTP and refuses these fields.
TLSCert string `yaml:"tls_cert,omitempty"`
TLSKey string `yaml:"tls_key,omitempty"`
}
NodeCacheConfig exposes storage to one guest through short-lived credentials.
type NodeConfig ¶
type NodeConfig struct {
// Name identifies this node to the server and in tier pinning. Defaults to
// the hostname.
Name string `yaml:"name,omitempty"`
// ServerAddr is the control plane to dial. Nodes always initiate the
// connection, so a node needs no inbound reachability of its own.
ServerAddr string `yaml:"server_addr"`
// BootstrapAddr is where `billet node --enroll` asks to join, when the control
// plane serves enrollment on an address of its own (server.bootstrap_listen).
//
// USED ONCE AND NEVER AGAIN. A running node never touches those routes, so
// this decides nothing after the certificate is written. Unset falls back to
// server_addr, which is right for a control plane that has no separate
// enrollment address; against one that does, the node wire refuses a
// connection with no certificate and the fallback cannot work.
BootstrapAddr string `yaml:"bootstrap_addr,omitempty"`
// Provider selects the compute backend for this host.
Provider ProviderKind `yaml:"provider"`
// Site is where this machine physically is, naming one of the control
// plane's declared sites. Optional, and only meaningful once a deployment
// has more than one place.
Site string `yaml:"site,omitempty"`
// MaxVCPU and MaxMemory are what this host CONTRIBUTES, which is not what it has.
// Unset means "everything I can detect".
//
// DECLARED ON THE MACHINE rather than in the control plane's config, because the
// person running this host knows what else it does — the same reason the provider
// is declared here.
//
// Setting them ABOVE what the machine has is allowed and warned about;
// overcommitting is a decision an operator is entitled to make.
MaxVCPU int `yaml:"max_vcpu,omitempty"`
MaxMemory ByteSize `yaml:"max_memory,omitempty"`
// TLS is the certificate bundle this node presents, issued by the control
// plane's `billet ca issue`.
//
// REQUIRED TO DIAL ANYTHING BUT LOOPBACK. The wire's whole authorisation model
// is the name in this certificate, so a node without one can only talk to a
// control plane in its own machine.
TLS *NodeTLS `yaml:"tls,omitempty"`
// LockDir is where this node places the host-wide deployment lock.
//
// THE LOCK BELONGS TO THE NODE ROLE, because the node is what manages containers
// and a control plane manages none. It is exclusive per identity, so a server that
// took it would keep a node on the same machine from ever starting.
//
// THE LOCK'S SCOPE HAS TO MATCH THE DAEMON'S, and billet cannot derive that: every
// process reaching the same container runtime must meet at the same directory. The
// per-user default is wrong for a system service sharing /var/run/docker.sock, and
// for containers sharing a socket with private filesystems.
//
// It must NOT be world-writable, or any local user could hold the file and keep
// billet from starting. A directory shared between two accounts must be SETGID;
// 2770 works everywhere, while 2730 works only where a directory can be opened for
// search without reading it (Linux O_PATH, darwin and FreeBSD O_SEARCH).
LockDir string `yaml:"lock_dir,omitempty"`
// AllowUnlockedDeployment starts this node even when the host-wide lock cannot be
// placed.
//
// AN OPT-IN, BECAUSE AUTHORIZATION MUST NOT BE DERIVED FROM AN I/O FAILURE.
// Downgrading automatically would let a symlink loop, a permissions change,
// ENOLCK, descriptor exhaustion or a service manager with no HOME each silently
// switch off the protection, with a log line as the only evidence.
AllowUnlockedDeployment bool `yaml:"allow_unlocked_deployment,omitempty"`
// StateDir holds node-local data: the generation pointer store (which is
// authoritative for this node's volumes), image cache, and mTLS identity.
StateDir string `yaml:"state_dir"`
// Firecracker is required when Provider is ProviderFirecracker.
Firecracker *FirecrackerConfig `yaml:"firecracker,omitempty"`
// Tart configures the Apple Silicon backend. Optional: a node running only
// trusted tiers needs none of it.
Tart *TartConfig `yaml:"tart,omitempty"`
// EC2 is required when Provider is ProviderEC2.
EC2 *EC2Config `yaml:"ec2,omitempty"`
// CodeBuild is required when Provider is ProviderCodeBuild, and refused for
// every other backend — the same rule as node.ec2 and node.firecracker, for
// the same reason: nothing else reads it, so on another provider it is a
// project, a fleet and a parameter path that look configured and are consulted
// by nothing.
CodeBuild *CodeBuildConfig `yaml:"codebuild,omitempty"`
// EBSS3 is the cache store local to an EC2 site's instances. EBS carries
// block generations and S3 carries the fenced per-key state.
EBSS3 *EBSS3Config `yaml:"ebs_s3,omitempty"`
// Ceph is the site's storage, required when Provider is ProviderFirecracker
// and refused for every other backend.
//
// REFUSED RATHER THAN IGNORED, because nothing reads it on a host that cannot
// attach a block device: a container has nowhere to put one, and an ec2 node
// orchestrates compute in a region that cannot reach this cluster at all. A
// block of settings that looks configured and is inert is the failure billet
// refuses elsewhere — it reads as a working cache right up to the first job
// that expected one.
Ceph *CephConfig `yaml:"ceph,omitempty"`
// Cache exposes the per-job sticky-volume API on a Firecracker guest bridge or
// over TLS to EC2 guests. Optional: a node without it offers no dynamic cache
// volumes to workflows.
//
// OPTIONAL IS NOT FREE, and `billet check` says so. A node that names a store
// and no endpoint has a cache nothing can reach: an EBSS3 node is REFUSED
// there, because that block backs nothing else, and a Ceph node is reported,
// because image_pool still boots every guest. The refusal is not here because
// `billet decommission` and `billet init iam` both read a store block on a
// config that has already lost its listener.
Cache *NodeCacheConfig `yaml:"cache,omitempty"`
// RegistryMirrors are three site-local Distribution pull-through caches. One
// instance per upstream is required because proxy mode has one remote URL.
RegistryMirrors *RegistryMirrors `yaml:"registry_mirrors,omitempty"`
// MaxCustody bounds how long billet holds capacity for compute it cannot account
// for — a container adopted from a crashed run, or one an ambiguous launch may
// have left behind — before destroying it. A Go duration string.
//
// EMPTY MEANS NO BOUND, deliberately. Elapsed time is not evidence that a job
// stopped making progress: billet imposes no job limit and self-hosted runners are
// routinely configured past GitHub's six-hour default, so a bound picked by billet
// would kill legitimate long jobs for no reason visible in the logs. Billet warns
// hourly about held capacity regardless.
MaxCustody string `yaml:"max_custody,omitempty"`
// DrainTimeout bounds how long a stopping node waits for the compute it is still
// holding before letting the teardown destroy it. A Go duration string.
//
// Separate from the control plane's key: the two are restarted for different
// reasons and need not wait the same amount of time.
DrainTimeout string `yaml:"drain_timeout,omitempty"`
}
NodeConfig configures a compute host.
func (*NodeConfig) Contribution ¶
func (n *NodeConfig) Contribution(detectedVCPU int, detectedMemory ByteSize) Contribution
Contribution resolves what this node offers from what it declared and what the machine turned out to have.
PURE, AND THE DETECTED VALUES ARE ARGUMENTS, so the decision can be tested against hardware this host does not have. Detection is a syscall; which number wins is a rule, and a rule that can only be exercised on the machine running the tests is a rule that is tested on exactly one configuration.
FIELD BY FIELD, NOT ALL OR NOTHING. A host that sets max_memory to hold RAM back for a database has said nothing about its cores, and treating the pair as one decision would read that as "0 vCPU" and register a node that can never be given work.
func (*NodeConfig) DrainTimeoutDuration ¶
func (n *NodeConfig) DrainTimeoutDuration() (time.Duration, error)
DrainTimeoutDuration parses Node.DrainTimeout, reporting the default when unset.
Separate from the server's because a node and a control plane are restarted for different reasons and need not wait the same amount of time.
func (*NodeConfig) MaxCustodyDuration ¶
func (n *NodeConfig) MaxCustodyDuration() (time.Duration, error)
MaxCustodyDuration parses Node.MaxCustody, reporting zero when unset.
Parsed on demand so the config type stays a plain data shape — but validation calls it too, so a typo is reported when the file is read rather than hours later when a container needs reclaiming.
type NodePolicy ¶
type NodePolicy struct {
// Name matches Tier.Node and NodeConfig.Name.
Name string `yaml:"name"`
// Provider is the compute backend this host runs, matching NodeConfig.Provider.
// Optional, and used only to decide whether an unpinned tier could ever land
// here: without it a macOS-only Mac would appear to conflict with every x64 Linux
// tier in the deployment.
Provider ProviderKind `yaml:"provider,omitempty"`
// GuestOS is an allowlist of what may be scheduled here. Empty means
// unconstrained, which is the default and preserves the behaviour of a
// config that never mentions the node.
//
// Note the shape difference from Tier.GuestOS, which is a single value: a
// tier boots exactly one guest OS, while a host may permit several.
GuestOS []GuestOS `yaml:"guest_os,omitempty"`
// MacOSVMLimit caps concurrent macOS guests on this host, counting warm ones. Nil
// means DefaultMacOSVMLimit — an unconfigured Apple host is still bound by
// Apple's licence, so the default is the licence, not "unlimited".
//
// Raising it above DefaultMacOSVMLimit is permitted because billet cannot know
// what licence an operator has, but Apple's standard terms allow at most
// DefaultMacOSVMLimit macOS guests per Apple-branded host — exceeding that is an
// assertion about YOUR licence, not a tuning knob.
MacOSVMLimit *int `yaml:"macos_vm_limit,omitempty"`
}
NodePolicy is what one compute host is permitted to run.
A host's capabilities are not implied by its provider: an Apple Silicon machine can serve macOS guests, Linux arm64 guests, or both, and which of those an operator wants is a deployment decision rather than a property of the hardware.
func (NodePolicy) AllowsGuestOS ¶
func (p NodePolicy) AllowsGuestOS(g GuestOS) bool
AllowsGuestOS reports whether this host may run a given guest OS. An empty allowlist permits everything.
func (NodePolicy) Clone ¶
func (p NodePolicy) Clone() NodePolicy
Clone returns a deep copy, sharing nothing mutable with the receiver.
A shallow struct copy is not enough, and the difference is silent: GuestOS is a slice and MacOSVMLimit is a POINTER, so a caller holding the original could widen a host's allowlist or raise its macOS cap after the allocator was built from it — moving a licence limit out from under leases already counted.
func (NodePolicy) MacOSLimit ¶
func (p NodePolicy) MacOSLimit() int
MacOSLimit is the effective cap on concurrent macOS guests for this host.
An allowlist that excludes macOS yields 0 whatever MacOSVMLimit says, so the two fields cannot disagree about whether macOS runs here.
func (NodePolicy) Validate ¶
func (p NodePolicy) Validate(where string) []error
Validate reports every way this policy is malformed on its own terms.
Exported because internal/alloc must apply the SAME rules: its constructor accepts a catalog it cannot prove came through Load, and a second hand-written copy is how the two drift into disagreeing about which hosts are legal.
type NodeTLS ¶
type NodeTLS struct {
// CertPath is this node's certificate. Its common name is the node name the
// control plane will act on.
CertPath string `yaml:"cert"`
// KeyPath is the matching private key. A secret.
KeyPath string `yaml:"key"`
// CAPath is the deployment authority this node verifies the control plane
// against.
CAPath string `yaml:"ca"`
}
NodeTLS points at the three files `billet ca issue` produced.
Paths rather than inline PEM: a private key pasted into a config file ends up in a backup, a paste buffer, and eventually a support thread.
type PlacementPolicy ¶
type PlacementPolicy string
PlacementPolicy decides which of several suitable machines a reservation is aimed at, once preference and eligibility have narrowed the field.
const ( // PlacementPack fills a machine before starting on the next one. // // THE DEFAULT, because the failure it prevents is worse than the one it causes. // Spreading leaves every host partly used, and partly used hosts cannot hold a // LARGE tier: six 4-vCPU jobs spread across two 16-vCPU machines leave four free // on each, so an 8-vCPU job fits nowhere while eight vCPU sit idle in the fleet. // // The usual argument for spreading is contention, and it is weaker here than it // looks: billet escrows vCPU and memory and the provider enforces both as hard // per-container limits, so two jobs on one host do not take each other's cores or // RAM. What they share is disk, page cache and network. Packing is also what makes // a cloud host affordable — an instance with one job on it cannot be shut down — // and it is what Nomad does by default. PlacementPack PlacementPolicy = "pack" // PlacementSpread keeps machines as even as possible. // // For a deployment that would rather have every job on its own spindle than // fit the most work: fewer jobs contending for disk and page cache, and one // host dying takes a smaller share of what is running. The cost is // fragmentation, which is paid by whichever tier is largest. PlacementSpread PlacementPolicy = "spread" )
func (PlacementPolicy) Or ¶
func (p PlacementPolicy) Or() PlacementPolicy
Or returns the policy, or pack when nothing was chosen.
EMPTY MEANS PACK rather than being an error, because this is a tuning knob on a deployment that has more than one machine — a config written before it existed, or by someone with one host, should keep working and get the default.
func (PlacementPolicy) Validate ¶
func (p PlacementPolicy) Validate() error
Validate reports whether the policy is one billet implements.
A TYPO MUST NOT SILENTLY BECOME THE DEFAULT. "packed" or "binpack" would otherwise fall through Or() to pack and look correct, and an operator who chose spread deliberately would never learn their fleet was doing the opposite.
type PostgresStateConfig ¶
type PostgresStateConfig struct {
// DSNEnv names the ENVIRONMENT VARIABLE holding the connection string, and
// the indirection is the point: a DSN carries a password, and a secret
// written into YAML ends up in a backup, a paste buffer, and eventually a
// support thread. It is the same rule the GitHub App private key follows.
DSNEnv string `yaml:"dsn_env"`
}
PostgresStateConfig is the ledger in PostgreSQL.
type ProviderKind ¶
type ProviderKind string
ProviderKind names a compute backend.
const ( // ProviderFirecracker runs one Firecracker microVM per job on bare metal. // Requires /dev/kvm. ProviderFirecracker ProviderKind = "firecracker" // ProviderTart runs macOS and Linux arm64 guests on Apple Silicon. Requires // Tart, which is FSL-licensed and installed separately. ProviderTart ProviderKind = "tart" // ProviderEC2 launches one instance per job — on demand unless node.ec2.spot // says otherwise, because a reclaimed spot instance is a failed build that // GitHub will not requeue. Firecracker is not an option on EC2 outside .metal // instances, so here the instance itself is the isolation boundary, which is // also why this backend may run untrusted work at all. ProviderEC2 ProviderKind = "ec2" // ProviderCodeBuild runs one AWS CodeBuild build per job, started through the // API after billet has already escrowed the job. // // IT DOES NOT USE CODEBUILD'S OWN GITHUB ACTIONS RUNNER INTEGRATION, which is // webhook-only and would take over job detection, runner registration and // scheduling. billet starts an ordinary NO_SOURCE project and runs GitHub's // runner from its own JIT configuration, exactly as the ec2 backend does inside // an instance — see docs/reference/decisions/adr-007-codebuild-provider.md. // // It is how billet reaches AWS-MANAGED APPLE SILICON, through a reserved-capacity // MAC_ARM fleet, without an operator allocating an EC2 Mac Dedicated Host. It is // also the one backend carrying an EXTERNAL job ceiling: CodeBuild caps a build // at 36 hours and fails a queued one after at most 8, neither of which billet can // lift, which is why accept_external_build_ceiling has no default. And it refuses // untrusted work outright rather than gating it on a network, because a // reserved-capacity instance survives between builds and shares cached state // across projects in the account. ProviderCodeBuild ProviderKind = "codebuild" // ProviderDocker runs jobs in containers. Isolation is materially weaker than // a VM; this exists so `billet init` works on a laptop and it refuses // untrusted workloads outright. ProviderDocker ProviderKind = "docker" // ProviderSimulated starts no compute: an instance is a record that reports // itself running for a modelled duration and stopped afterwards, so a workload // can be driven through the real allocator and placer at a scale no real backend // can afford. It exists for billet's own test harness. It fabricates completions, // so a configuration that names it anywhere is REFUSED at load, and cmd/billet // never constructs it; it is in the closed set only so the ledger and the node // wire can register a simulated host in a test. ProviderSimulated ProviderKind = "simulated" )
func RemoteProviders ¶
func RemoteProviders() []ProviderKind
ShapeField names the configuration key holding a remote backend's ordered purchasable shapes, so a diagnostic about one points at the field the operator actually wrote.
ONE PLACE DECIDES THE SPELLING, because the shape validator is shared: it is called from config loading, where the key is known, and from the allocator, where a node's REGISTERED provider is the only thing that says which key its shapes came out of. Two copies of that mapping is a diagnostic naming `node.ec2.instance_types` at an operator whose file says `node.codebuild.compute_types`. RemoteProviders is every backend whose compute runs somewhere other than the node's own machine.
DERIVED FROM RunsOnHost RATHER THAN LISTED, so a third remote backend is included by the same allowlist that already decides how it is charged. A second hand-written list is how `billet status` came to report the cost exposure of an ec2 fleet and nothing at all for a codebuild one, which reads as a fleet that costs nothing.
func (ProviderKind) RunsOnHost ¶
func (p ProviderKind) RunsOnHost() bool
RunsOnHost reports whether this backend runs jobs on the machine billet is running on, so that machine's cores and memory are what it can offer.
Every backend but ec2 does. An ec2 node is an ORCHESTRATOR: it holds credentials and calls an API, and the compute appears somewhere else entirely, so what the box it runs on happens to have says nothing about what it can contribute. Reading the two alike makes a t4g.nano offer two vCPU to a fleet it could buy a hundred of, and makes an honest `max_vcpu: 512` look like a typo worth warning about on every boot.
AN ALLOWLIST RATHER THAN `!= ec2`, so a second remote backend that nobody remembers to add here is treated as remote — which loses a warning, where the other direction would invent a contribution out of the wrong machine's hardware. `codebuild` is that second remote backend, and it arrived without this function needing a line: the allowlist already answered correctly for a name it had never heard of, which is what the shape was chosen for.
`simulated` is listed as host-backed because every consequence of the answer is right for it: its capacity is what the node declares rather than a shape it buys, placement charges the tier request, and custody reads its inventory as causal, which an authoritative in-memory store is.
func (ProviderKind) ServesMacOS ¶
func (p ProviderKind) ServesMacOS() bool
ServesMacOS reports whether this backend can run a macOS guest at all.
THE ONE READER OF macOSProviders OUTSIDE VALIDATION. `billet check` used to ask `!= tart` and printed "macOS n/a (codebuild cannot run macOS guests)" beside a node whose fleet had just run an Xcode job — a second copy of the allowlist, written before the second member existed, and nothing tied it to this one.
func (ProviderKind) ShapeField ¶
func (p ProviderKind) ShapeField() string
func (ProviderKind) TestOnly ¶ added in v0.7.0
func (p ProviderKind) TestOnly() bool
TestOnly reports whether this backend exists for billet's own test harness and may not be named in a configuration.
THE ONE READER OF THAT DISTINCTION. Valid says whether billet implements a backend, and the simulated one is implemented: the allocator and the node wire register it in tests. What it may not do is reach a deployment through a file, because a backend that fabricates completions is a fleet that reports every job finished and runs none. That refusal lives in Config.Validate rather than in the per-tier and per-node rules alloc.New re-applies, so a catalogue built in code can still name it.
func (ProviderKind) Valid ¶
func (p ProviderKind) Valid() bool
Valid reports whether this is a known provider. Exported because alloc.New must reject a catalog it cannot prove came through Load.
type RegistryMirrors ¶
type RegistryMirrors struct {
DockerIO string `yaml:"docker.io" json:"docker.io"`
GHCRIO string `yaml:"ghcr.io" json:"ghcr.io"`
QuayIO string `yaml:"quay.io" json:"quay.io"`
}
RegistryMirrors names the independent public-registry caches visible at a site.
func (*RegistryMirrors) Empty ¶
func (r *RegistryMirrors) Empty() bool
Empty reports whether no mirror was configured.
type ReleaseConfig ¶
type ReleaseConfig struct {
// Channel is the signed pointer this deployment follows. Empty means stable.
Channel string `yaml:"channel,omitempty"`
// Version pins an exact release, and it NEVER MOVES.
//
// It wins over Channel, and setting both is an error rather than a precedence
// puzzle: an operator who pinned a version and also named a channel has said
// two things, and guessing which they meant is how a deployment that believes
// itself pinned quietly follows a pointer.
Version string `yaml:"version,omitempty"`
// Automatic lets the control plane start a rollout by itself when the channel
// advances, and lets the scheduled updaters on each host act on it.
//
// ON BY DEFAULT, AND THIS IS THE ONE ZERO VALUE IN THIS FILE THAT DOES NOT
// REFUSE. Every other absent field here answers "hold"; this one answers "go",
// because the failure an unattended deployment actually meets is the update
// that never happens: a runner image GitHub stops queueing to, a fix that
// shipped and never arrived. What makes that safe to default is everything
// around it — a rollout drains every host for as long as its work takes, a
// candidate is verified and rolled back on failure, and the ledger's release
// watermark refuses to let an unattended update go backwards. A deployment
// that must not move says so with `automatic: false`.
//
// A POINTER SO ABSENCE AND FALSE ARE DIFFERENT. A plain bool cannot tell "the
// operator wrote false" from "the operator wrote nothing", and only the first
// is an opt-out. Read it through AutomaticUpdates, never directly.
Automatic *bool `yaml:"automatic,omitempty"`
// MaintenanceWindow bounds when an automatic rollout may START.
//
// IT NEVER STOPS ONE. A rollout in progress waits for the work already running
// on a host for as long as it takes, and a window that could interrupt that
// would be a clock authorising a teardown — the thing this whole area exists
// to refuse. What the window decides is whether a new one begins.
MaintenanceWindow *MaintenanceWindow `yaml:"maintenance_window,omitempty"`
// SigningIdentity and SigningIssuer override what a release manifest must be
// signed by, for a deployment mirroring releases internally.
SigningIdentity string `yaml:"signing_identity,omitempty"`
SigningIssuer string `yaml:"signing_issuer,omitempty"`
}
ReleaseConfig says how this deployment learns about new billet releases.
EVERY FIELD IS OPTIONAL. A deployment that says nothing follows the signed stable channel and updates itself: the control plane starts a rollout when the channel advances, and every host converges on it. Saying `automatic: false` is the one sentence that turns that off.
func (*ReleaseConfig) AutomaticUpdates ¶ added in v0.6.0
func (r *ReleaseConfig) AutomaticUpdates() bool
AutomaticUpdates reports whether this deployment updates itself.
TRUE FOR AN ABSENT BLOCK AND AN ABSENT FIELD. The only thing that turns it off is an operator writing `automatic: false`, which is what the pointer exists to tell apart from writing nothing. Every reader — the rollout starter, the host's updater, the image refresh — asks this and never the field.
func (*ReleaseConfig) EffectiveChannel ¶
func (r *ReleaseConfig) EffectiveChannel() string
EffectiveChannel is the channel this deployment follows, or empty when it is pinned to an exact version.
func (*ReleaseConfig) OpenAt ¶
func (r *ReleaseConfig) OpenAt(t time.Time) bool
OpenAt reports whether an automatic rollout may start at a given moment.
AN ABSENT WINDOW IS ALWAYS OPEN, which is what makes the field optional rather than something every deployment has to write.
A WINDOW THAT WRAPS MIDNIGHT IS THE ORDINARY CASE, not an edge one: 22:00 to 04:00 is what a person picks, and a comparison that only handled start < end would silently never open for exactly the windows operators choose.
func (*ReleaseConfig) Pinned ¶
func (r *ReleaseConfig) Pinned() bool
Pinned reports whether this deployment follows nothing.
func (*ReleaseConfig) PinnedVersion ¶ added in v0.6.0
func (r *ReleaseConfig) PinnedVersion() string
PinnedVersion is the exact release this deployment is pinned to, or empty for one that follows a channel.
type RemoteCostNode ¶
type RemoteCostNode struct {
MaxVCPU int
MaxMemory ByteSize
Shapes []RemoteShape
Outstanding USDPerHour
}
RemoteCostNode holds the resource and shape declarations needed to bound one registered REMOTE node's compute cost.
NOT EC2-SPECIFIC, and the name said it was. Every remote backend declares ordered shapes with a price per hour — that is how placement charges the first that fits — so the arithmetic below is about shapes and ceilings rather than about which API buys them. What was ec2-specific was the QUERY that fed it, which is why a codebuild fleet's exposure was invisible in `billet status`.
type RemoteShape ¶
type RemoteShape = EC2InstanceType
RemoteShape is what a remote backend's ordered shape list holds, whatever that backend calls its shapes — an EC2 instance type, a CodeBuild compute type.
AN ALIAS RATHER THAN A DEFINED TYPE, and that is the whole point: a defined type would inherit none of the methods and none of the validation, so there would be two shape catalogues to keep in step and two validators to keep in agreement. The ledger column, the wire field and the validator are all one thing; this name exists so a codebuild config field does not have to be spelled `EC2InstanceType` and read as a copy-paste mistake.
type ServerConfig ¶
type ServerConfig struct {
// Listen is the address nodes dial. Nodes always connect outbound, so on a
// single-box deployment this stays on loopback and billet needs no open port
// reachable from anywhere else.
Listen string `yaml:"listen"`
// BootstrapListen is a SECOND address serving only the two routes a machine
// that has never enrolled needs: reading this deployment's authority, and
// asking to join.
//
// ITS ABSENCE IS A REFUSAL, not a default. Without it this control plane does
// not enroll over the network at all, and admission happens out of band —
// `billet ca issue <node>` on the server, the bundle copied to the host.
//
// It exists because those two routes cannot require a certificate, and a
// listener that admits callers who need not prove anything cannot share a
// connection budget with the fleet: an anonymous caller that completes a
// handshake and idles holds a slot, and once the budget is full a healthy
// node's connection is never accepted. So `listen` demands a certificate in
// the handshake and serves nothing else, and this address carries the rest.
//
// Both listeners present the same certificate, so whatever name a node dials
// this one by has to be covered by node_tls_hosts (or by a concrete host in
// one of the two listen addresses, which billet derives them from).
//
// Refused against a loopback `listen`: there are no certificates on a loopback
// wire, so there is nothing to enroll into.
BootstrapListen string `yaml:"bootstrap_listen,omitempty"`
// StateDir is the SHORTHAND, and it is what most deployments write: one
// directory holding the SQLite ledger, the process lock, the maintenance
// fence and the mTLS CA. It MUST be on local storage, because SQLite's WAL
// cannot work on a network filesystem and the state package fails closed if
// it detects otherwise.
//
// It means exactly `identity_dir: <dir>` plus `state: {backend: sqlite}`, and
// it stays supported. Writing it TOGETHER with `state:` is refused rather
// than merged: two spellings of one value is a mistake internal/config has
// already made three times, and it is silent every time.
StateDir string `yaml:"state_dir,omitempty"`
// IdentityDir holds what is NOT rows: the deployment identity, the node-wire
// CA and its rotation state, the process lock, and the maintenance fence.
//
// SEPARATE FROM THE LEDGER BECAUSE THE TWO ARE NOT INTERCHANGEABLE. A ledger
// can move into a database billet does not operate; a private key cannot
// follow it there, and local process coordination has nothing to do with SQL
// rows. The pairing is a refusal for the same reason: a subset of a
// deployment is one that starts, looks healthy, and is not.
//
// Required when `state:` is written out; `state_dir` supplies it otherwise.
IdentityDir string `yaml:"identity_dir,omitempty"`
// State selects the backend the ledger lives in. Absent means the shorthand
// above.
State *StateConfig `yaml:"state,omitempty"`
// Controllers says whether this deployment runs one control plane or an
// active/passive pair. Absent means `single`, which is what every deployment
// before this key had.
//
// REFUSED ON A SQLITE LEDGER. There is nothing to elect over: the ledger is a
// file on local storage that a second machine cannot open at all, so a standby
// would be a second process on one host waiting for a lock its own service
// manager already restarts it to take.
Controllers ControllerMode `yaml:"controllers,omitempty"`
// Identity says where the deployment's identity material lives. Absent means
// `file`, which is what every deployment before this key had.
Identity *IdentityConfig `yaml:"identity,omitempty"`
// MaxVCPU and MaxMemory bound what the allocator will ever hand out across every
// tier combined. Required and positive: capacity is escrowed before each listener
// advertises, so an absent ceiling lets concurrent listeners collectively
// overcommit the machine.
MaxVCPU int `yaml:"max_vcpu"`
MaxMemory ByteSize `yaml:"max_memory"`
// Placement decides which of several suitable machines a job is sent to.
// Empty means pack. Only meaningful once a deployment has more than one.
Placement PlacementPolicy `yaml:"placement,omitempty"`
// NodeTLSHosts are the names and addresses nodes will dial this control plane by.
// They become the subject names of the certificate it serves.
//
// REQUIRED WHEN listen IS A WILDCARD, which says which interfaces to accept on and
// nothing about what a node types: a certificate minted for "0.0.0.0" matches
// nothing, and the failure arrives as a handshake error on the node. A concrete
// listen address supplies itself.
NodeTLSHosts []string `yaml:"node_tls_hosts,omitempty"`
// DrainTimeout bounds how long a stopping control plane waits for the jobs it is
// already running before it destroys them. A Go duration string: "6h", "90m".
//
// A service manager's stop timeout must exceed this plus the teardown, or its own
// expiry arrives first as a SIGKILL — skipping the teardown and stranding exactly
// the compute the drain was protecting.
DrainTimeout string `yaml:"drain_timeout,omitempty"`
}
ServerConfig configures the control plane.
func (*ServerConfig) DrainTimeoutDuration ¶
func (s *ServerConfig) DrainTimeoutDuration() (time.Duration, error)
DrainTimeoutDuration parses Server.DrainTimeout, reporting the default when unset.
Parsed on demand rather than at load time, so the config type stays a plain data shape — but Validate calls it too, so a typo is reported when the file is read rather than at the shutdown that needed it.
func (*ServerConfig) IdentityBackendKind ¶
func (s *ServerConfig) IdentityBackendKind() IdentityBackend
IdentityBackendKind is where this deployment's identity material lives, resolved. Absent means the file backend.
func (*ServerConfig) IdentitySSM ¶
func (s *ServerConfig) IdentitySSM() *IdentitySSMConfig
IdentitySSM is the Parameter Store configuration, or nil.
func (*ServerConfig) LedgerBackend ¶
func (s *ServerConfig) LedgerBackend() StateBackend
LedgerBackend is the engine this config selects, resolved.
func (*ServerConfig) LedgerDSNEnv ¶
func (s *ServerConfig) LedgerDSNEnv() string
LedgerDSNEnv names the environment variable holding the PostgreSQL DSN, and is empty for any other backend.
func (*ServerConfig) LedgerPath ¶
func (s *ServerConfig) LedgerPath() string
LedgerPath is the SQLite ledger file, and is empty for any other backend.
DERIVED, NEVER CONFIGURED. It is billet.db inside identity_dir, which is what state_dir has always meant and what every reader of a ledger file already assumes — the restore planner, the writer barrier and the archive among them.
type SiteConfig ¶
type SiteConfig struct {
// Name is what a node and a tier refer to this site by.
Name string `yaml:"name"`
// Store is the storage local to this site. Ceph serves host-backed compute;
// EBS snapshots and S3 serve AWS without exposing the home cluster over a WAN.
Store SiteStoreKind `yaml:"store"`
}
SiteConfig declares one place compute runs.
A STRUCT RATHER THAN A STRING, because a site declares both placement identity and its intended storage backend — Ceph at a bare-metal site, EBS and S3 in a cloud region. The control plane validates both parts when a remote node registers, so split configs cannot create two storage authorities for one logical site.
type SiteStoreKind ¶
type SiteStoreKind string
SiteStoreKind selects the storage implementation local to one site.
const ( // SiteStoreCeph is an RBD cluster on the site's own storage network. SiteStoreCeph SiteStoreKind = "ceph" // SiteStoreEBSS3 stores block generations in EBS and fenced state in S3. SiteStoreEBSS3 SiteStoreKind = "ebs-s3" )
func (SiteStoreKind) Valid ¶
func (s SiteStoreKind) Valid() bool
Valid reports whether this is a recognized storage backend name.
type StateBackend ¶
type StateBackend string
StateBackend names the engine the control-plane ledger lives in.
const ( // StateSQLite is the default and the recommended shape for a laptop, one // owned server, or the small controller ADR-001 describes. It is explicitly // SINGLE CONTROLLER: the exclusive lock on the state directory is what stops // a second one, and there is no shared-storage story, because SQLite's WAL // cannot work on a network filesystem. StateSQLite StateBackend = "sqlite" // StatePostgres puts the ledger in a database billet does not operate, which // is what makes the controller replaceable — the scheduling state outlives // the machine, so recovery is a managed backup rather than a directory. // // IT IS NOT HIGH AVAILABILITY ON ITS OWN. Exactly one controller may make // scheduling decisions either way, and a database's ability to serialize // writes is not proof that only one process is polling GitHub. StatePostgres StateBackend = "postgres" )
type StateConfig ¶
type StateConfig struct {
Backend StateBackend `yaml:"backend"`
// Postgres configures the ledger when Backend selects it, and is REFUSED
// otherwise rather than ignored — the same way a `node.ceph` block on a
// non-firecracker backend is, and for the same reason: silently ignoring it
// produces a deployment that believes it configured something.
//
// THERE IS NO `sqlite:` BLOCK, and its absence is deliberate. The only thing
// it could carry is a path, the SQLite ledger is always billet.db inside
// identity_dir, and every part of billet that reads a ledger FILE — the
// restore planner, the writer barrier, the archive — derives it that way. A
// key that billet accepted and then did not use would be a deployment
// started against a freshly created empty ledger, which is not a failure
// anybody would see until the fleet came back empty.
Postgres *PostgresStateConfig `yaml:"postgres,omitempty"`
}
StateConfig is the versioned form of "where does the ledger live".
type TargetScope ¶ added in v0.10.0
type TargetScope string
TargetScope is what kind of GitHub owner a target is: an organization, whose runners live in runner groups, or a repository, whose runners belong to it alone.
There is no user-account scope. GitHub has three runner scopes — repository, organization, enterprise — and a personal account as a whole cannot own runners; each of its repositories can, so a personal account is served one repository target at a time.
const ( ScopeOrganization TargetScope = "organization" ScopeRepository TargetScope = "repository" )
type TartConfig ¶
type TartConfig struct {
// UntrustedIsolation names the mechanism that confines a fork pull
// request's guest, and its ABSENCE is what refuses one.
//
// The same rule, and the same reasoning, as
// node.firecracker.untrusted_bridge: a tart VM is a real kernel boundary,
// and the NETWORK is not one. Tart's default is shared NAT, where a guest
// reaches the host and can ARP-spoof the vmnet bridge to read another
// guest's traffic — so untrusted work runs only once its confinement has
// been described, rather than landing on the default because nobody said
// otherwise.
//
// STATED BY THE OPERATOR RATHER THAN DETECTED, because billet cannot prove
// this from the host. What it can see of softnet is two metadata bits — a
// setuid bit and an owner of root — which say the helper could start, not
// what its policy then permits. Naming it here is the operator asserting
// the mechanism is the one they want, exactly as naming a bridge is.
UntrustedIsolation TartIsolation `yaml:"untrusted_isolation,omitempty"`
// UntrustedDNS are the resolvers an isolated guest is given, and billet
// gives them because billet is what took the working one away.
//
// MEASURED: under softnet a guest's DHCP-assigned resolver is the vmnet
// gateway, which sits in the private address space softnet blocks. Egress
// to public addresses keeps working and TCP/443 keeps working, so nothing
// looks wrong — every job simply fails to resolve github.com. Public
// resolvers are reachable under exactly the policy that broke the gateway
// one.
UntrustedDNS []string `yaml:"untrusted_dns,omitempty"`
}
TartConfig is the Apple Silicon backend's node settings.
func (*TartConfig) Normalize ¶
func (t *TartConfig) Normalize()
Normalize trims the block and fills the resolver default.
EXPORTED FOR THE SAME REASON CheckFirecracker AND CheckTart ARE: the provider's constructor cannot assume its configuration came through Load, and a value trimmed for the CHECK while the caller launches with the raw one is the defect the ec2 and ceph blocks each shipped with once.
type TartIsolation ¶
type TartIsolation string
TartIsolation is a confinement mechanism for untrusted guests.
const IsolationSoftnet TartIsolation = "softnet"
IsolationSoftnet is tart's own userspace packet filter, which restricts a guest to public destinations and isolates guests from each other on the bridge. It is the only mechanism billet drives today; the type exists so a second one is a new value rather than a new meaning for a boolean.
type Tier ¶
type Tier struct {
Label string `yaml:"label"`
// Target names the GitHub target this tier's scale set belongs to. Defaults
// to the deployment's only target and is required when there are several,
// because a scale set exists on exactly one organization or repository and
// the credential that creates it is that target's.
Target string `yaml:"target,omitempty"`
// Trust is the authority every member of this runner pool receives before
// GitHub assigns it a job. It is explicit because scale-set JIT runners are
// pool members, not registrations bound to the assignment that caused Billet
// to create them.
Trust WorkloadTrust `yaml:"trust"`
// Workflows is the exact GitHub runner-group workflow allowlist a trusted
// pool expects. An untrusted pool needs no routing claim for its safety.
Workflows []string `yaml:"workflows,omitempty"`
// CacheScope is the one immutable Actions cache identity placed in a guest at
// launch. It is required for interception because JobStarted arrives after
// the guest already has its launch-time credentials.
CacheScope *CacheScope `yaml:"cache_scope,omitempty"`
// Provider is the single backend this tier runs on. Kept because it is what
// almost every deployment wants and what every existing config says; it is
// normalized into Providers, which is what the rest of billet reads.
Provider ProviderKind `yaml:"provider,omitempty"`
// Providers is an ORDERED preference list, most preferred first.
//
// The reason one `runs-on` label can span a machine at home and a cloud: a tier
// listing `[firecracker, ec2]` may be placed on either, so losing the bare-metal
// host does not take the label down with it.
//
// THE ORDER DECIDES, AT ESCROW — the allocator walks it most-preferred-first over
// the hosts that can serve the tier and have room, so a job reaches the cloud only
// when home is full, rather than when a cloud node polls first. server.placement
// decides only between hosts this order cannot separate.
//
// Setting both this and Provider is an error rather than a merge: guessing which
// spelling an operator meant, when the answer decides where untrusted code runs,
// is not a kindness. Every backend in allProviders is built and may appear here —
// stated without a count, because both sides of this merge had independently
// corrected the previous wording and one of them said "four" for a list of five.
Providers []ProviderKind `yaml:"providers,omitempty"`
// GuestOS defaults to linux. Set it explicitly for macOS and Windows tiers —
// licensing and capability checks key off this field, not off the label.
GuestOS GuestOS `yaml:"guest_os,omitempty"`
// Node optionally pins this tier to a named node. Required when only one
// node can serve it — macOS tiers, for example.
Node string `yaml:"node,omitempty"`
// Site optionally confines this tier to one place, the way Node confines it to one
// machine — but a site holds several machines, so it constrains without giving up
// the fallback that having several of them buys.
//
// The reason to reach for it is data rather than hardware: a job that must not
// leave a location, or one whose cache exists in only one place.
Site string `yaml:"site,omitempty"`
// RunnerGroup is the GitHub runner group this tier's scale set belongs to. Empty
// means GitHub's "default" group.
//
// Access control rather than scheduling: a runner group is how an organization
// decides which repositories may use these runners, and putting every tier in the
// default group hands them to every repository in the org.
RunnerGroup string `yaml:"runner_group,omitempty"`
// Command starts the runner inside this tier's image. Empty uses the provider's
// packaged runner service.
//
// Expressible because a container image's default command is a shell: a backend
// that launches one gets a container that exits immediately while every signal
// reports success, so the command cannot be left to the image.
Command []string `yaml:"command,omitempty"`
// Launch holds backend-specific boot details for a tier that accepts more than
// one provider. A Firecracker generation and an EC2 AMI are both images, but
// neither backend can interpret the other's name; commands can differ for the
// same reason. Single-provider tiers keep the simpler image and command fields.
Launch map[ProviderKind]TierLaunch `yaml:"launch,omitempty"`
VCPU int `yaml:"vcpu"`
Memory ByteSize `yaml:"memory"`
// Sizes expands this entry into one tier per vCPU count, most operators'
// single largest source of hand-written YAML.
//
// A real deployment wants several sizes of the same thing and each one is
// fifteen lines that differ in two numbers and a label — so `sizes: [2, 4, 8]`
// against everything else this entry says writes the other thirteen. The
// expansion happens in Parse, before defaults and before validation, so
// nothing downstream ever sees an unexpanded tier.
//
// IT IS A TEMPLATE, NOT A RANGE. Each size becomes a real, separate tier with
// its own label, its own scale set and its own escrow — because that is what
// it already was when it was written out by hand, and a shorthand that meant
// anything else would be a new scheduling concept wearing a convenience's
// clothes.
//
// Refused beside an explicit vcpu or memory: two spellings of one value is a
// mistake internal/config has already made three times, and it is silent
// every time.
Sizes []int `yaml:"sizes,omitempty"`
// MemoryPerVCPU is the proportion `sizes` shapes each tier to. It is
// meaningless without Sizes and refused there.
//
// The default is DefaultMemoryPerVCPU, which is the same 4GiB every generated
// catalogue already uses — so a config that writes `sizes` and nothing else
// gets exactly the ladder `billet init` would have written for it.
MemoryPerVCPU ByteSize `yaml:"memory_per_vcpu,omitempty"`
Disk ByteSize `yaml:"disk,omitempty"`
// SHM sizes /dev/shm. Chromium and Postgres both misbehave on the default,
// so this is a tier knob rather than an image constant.
SHM ByteSize `yaml:"shm,omitempty"`
// BuildKitCacheMountLimit bounds each persistent RUN --mount=type=cache
// record. BuildKit's ordinary GC bounds the whole worker; this catches one
// active mount that never becomes old enough for that policy to trim.
BuildKitCacheMountLimit ByteSize `yaml:"buildkit_cache_mount_limit,omitempty"`
Image string `yaml:"image,omitempty"`
// WarmPool is reserved for pre-booted idle VMs. Validation refuses a non-zero
// value until a provider implements it, because accepting an inert cost setting
// would tell an operator cold-start capacity exists when it does not.
WarmPool int `yaml:"warm_pool,omitempty"`
// Intercept routes the Actions results origin through the node-local cache proxy.
// False is the safe default because the same origin carries artifact metadata.
Intercept bool `yaml:"intercept,omitempty"`
// MaxConcurrent caps simultaneous instances of this tier, counting warm ones.
// Zero means "no per-tier cap" and is only legal for non-macOS tiers.
MaxConcurrent int `yaml:"max_concurrent,omitempty"`
// Reserved is how many simultaneous instances of this tier are always available to
// it, no matter how busy every other tier is.
//
// A FLOOR, where MaxConcurrent is a ceiling. Billet shares one budget across every
// tier and headroom is whatever is left, so a tier with steady demand can hold all
// of it while the others advertise zero and their jobs queue at GitHub. A
// reservation is deducted from what OTHER tiers may take, only while it is unmet.
//
// THE COST IS CAPACITY OTHER TIERS CANNOT USE. An idle listener keeps one discovery
// slot rather than claiming its whole floor, but the allocator still holds every
// unmet reserved slot away from competing tiers. Reserve for tiers that need a hard
// guarantee under contention. Zero is the default.
Reserved int `yaml:"reserved,omitempty"`
}
Tier is one runner shape. Its Label is what appears in `runs-on`.
func ExpandTierSizes ¶
ExpandTierSizes turns every `sizes` entry into one tier per size.
EXPORTED BECAUSE Parse IS NOT THE ONLY ENTRY POINT. `alloc.New` takes a catalogue directly and is exported too, so a caller that assembled tiers without going through Parse would otherwise hand the allocator an entry whose vcpu is zero and whose real sizes nothing ever read. alloc.New refuses such a tier by name and points here, which is the alloc.New rule — a safety-critical derivation enforced at only one of two entry points is a second entry point that does not enforce it.
IT RUNS BEFORE DEFAULTS AND BEFORE VALIDATION, so nothing downstream ever sees an unexpanded tier: applyDefaults fills in per-tier values the expansion has to have produced first (a macOS tier's inherited concurrency cap among them), and validation judges the tiers that will actually exist.
The result is a NEW slice; the caller's is not modified, because Parse is not the only caller and a function that rewrote its input would make the expansion's idempotence depend on who called it.
func (Tier) AcceptableProviders ¶
func (t Tier) AcceptableProviders() []ProviderKind
AcceptableProviders reports the backends this tier may run on, most preferred first. The single reader for that question, so `provider:` and `providers:` cannot drift apart — callers must not consult Tier.Provider directly.
CLONED, because callers keep what this returns: the allocator copies the list onto every lease it reserves, and handing out the tier's own backing array would let a caller change what future leases authorize.
func (Tier) AcceptsProvider ¶
func (t Tier) AcceptsProvider(p ProviderKind) bool
AcceptsProvider reports whether a tier may run on a backend.
func (Tier) GuestOSProviderErrors ¶
GuestOSProviderErrors reports backends that cannot host a tier's guest OS.
Split out from the fuller relational validation so alloc can apply the part that is a SAFETY invariant rather than a configuration convenience: only some backends can serve macOS at all, and runtime placement only tests list membership, so a macOS tier that fell back to a Linux backend would bind there happily.
func (Tier) ImageFor ¶
func (t Tier) ImageFor(provider ProviderKind) string
ImageFor returns the image name understood by a selected provider.
func (Tier) InterceptionErrors ¶
InterceptionErrors reports tiers that could reach a backend without the local storage and guest control the transparent Actions cache requires.
Exported because alloc.New cannot assume its catalogue came through Load.
func (Tier) LaunchErrors ¶
LaunchErrors reports incomplete or ambiguous backend-specific boot details.
func (Tier) PoolPolicyErrors ¶
PoolPolicyErrors reports unsafe or contradictory authority for a pooled scale-set tier. Exported because alloc.New cannot assume its catalogue came through Config.Load and must enforce the same boundary.
func (Tier) ProviderErrors ¶
ProviderErrors reports everything wrong with a tier's backend declaration.
One function, because `provider:` and `providers:` are two spellings of the same field and validating them separately is how they drift.
EXPORTED because alloc.New cannot assume its catalogue came through Load — a caller can construct tiers directly — and a rule only one entry point enforces is a rule with a second entry point that does not.
func (Tier) ReservationErrors ¶
ReservationErrors reports everything wrong with a tier's floor, on its own.
The cross-tier sum is checked separately, because it needs the whole catalogue and the budget.
func (Tier) ReservedMemory ¶
ReservedMemory is the memory a tier's floor holds back from other tiers.
func (Tier) ReservedVCPU ¶
ReservedVCPU is the vCPU a tier's floor holds back from other tiers.
func (Tier) RunnerCommand ¶
RunnerCommand is what starts the runner inside this tier's image.
Defaulted here rather than in each backend so every provider agrees, and so the default is stated once in a place an operator reads. The wrapper is relative because the stock image's working directory is the runner's home.
The generic default remains GitHub's `run.sh`, which is what the Docker runner image contains. RunnerCommandFor selects `./billet-runner-service` for the Firecracker image billet builds, and the full `/usr/local/bin/billet-runner` entrypoint for EC2 because that script prepares and completes the cache around the inner result-preserving wrapper.
A self-hosted runner updates itself by EXITING: the listener returns "updating" and the wrapper notices and re-execs it with the same arguments — including the JIT registration, which is what lets the restarted runner go on to take one job from its pool. Exec the listener directly and there is no loop: on a backend where each job gets its own machine, the listener exits, the machine is destroyed as though the work were finished, the job is redelivered, and the next machine does the same thing.
MEASURED, AND NOT CURRENTLY REACHABLE: a JIT configuration minted by GitHub's REST API carries `DisableUpdate = True` (and `Ephemeral = True`), so the service never sends these runners an update in the first place. The loop above is therefore insurance rather than a live requirement.
It is worth keeping as the default anyway. It costs nothing, it is what GitHub documents as the way to start a runner, and the setting it depends on is theirs to change — while the failure it prevents is silent and spends a guest per attempt.
THE REAL CONSEQUENCE OF THAT MEASUREMENT IS ELSEWHERE, and it is larger: because these runners never self-update, GitHub's 30-day rule is a HARD EXPIRY for billet. A runner more than 30 days behind a release is refused work outright, and nothing on the guest can rescue it — only republishing the image can. See internal/runnerrelease.
func (Tier) RunnerCommandFor ¶
func (t Tier) RunnerCommandFor(provider ProviderKind) []string
RunnerCommandFor returns the argv understood by a selected provider.
type TierLaunch ¶
TierLaunch is the part of a tier whose spelling belongs to one backend.
type USDPerHour ¶
type USDPerHour int64
USDPerHour is an hourly US-dollar amount stored as millionths of a dollar. EC2 publishes rates beyond cents, so cents are not enough; a float would make the estimate depend on rounding at every addition.
func ParseUSDPerHour ¶
func ParseUSDPerHour(s string) (USDPerHour, error)
ParseUSDPerHour parses unsigned decimal dollars with at most six fractional digits. Exponents and signs are deliberately outside the config grammar.
func RemoteFleetPeakHourlyExposure ¶
func RemoteFleetPeakHourlyExposure(maxVCPU int, maxMemory ByteSize, nodes []RemoteCostNode) (USDPerHour, error)
RemoteFleetPeakHourlyExposure returns the tighter of the shared deployment ceiling and the sum of every registered remote node's own ceiling.
func RemotePeakHourlyExposure ¶
func RemotePeakHourlyExposure(maxVCPU int, maxMemory ByteSize, shapes []RemoteShape) (USDPerHour, error)
RemotePeakHourlyExposure returns a conservative compute-only upper bound for a node. It takes the tighter of the highest price-per-vCPU and price-per-byte bounds. Either one bounds every possible mix of shapes.
func (*USDPerHour) Decimal ¶
func (p *USDPerHour) Decimal() string
Decimal formats dollars without a currency marker or unit.
func (*USDPerHour) ForHours ¶
func (p *USDPerHour) ForHours(hours int64) string
ForHours formats what this rate costs across a fixed number of hours.
func (*USDPerHour) MarshalYAML ¶
func (p *USDPerHour) MarshalYAML() (any, error)
MarshalYAML writes decimal dollars without losing precision.
func (*USDPerHour) String ¶
func (p *USDPerHour) String() string
func (*USDPerHour) UnmarshalYAML ¶
func (p *USDPerHour) UnmarshalYAML(node *yaml.Node) error
UnmarshalYAML accepts a quoted or bare decimal.
type WorkloadTrust ¶
type WorkloadTrust string
WorkloadTrust is the launch authority shared by a tier's runner pool.
const ( WorkloadUntrusted WorkloadTrust = "untrusted" WorkloadTrusted WorkloadTrust = "trusted" )
func (WorkloadTrust) Effective ¶
func (t WorkloadTrust) Effective() WorkloadTrust
Effective returns the restrictive migration default for an omitted trust.
func (WorkloadTrust) Valid ¶
func (t WorkloadTrust) Valid() bool
Valid reports whether the pool trust is one Billet understands.