fusefile

package
v0.26.0 Latest Latest
Warning

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

Go to latest
Published: Aug 16, 2026 License: MIT Imports: 18 Imported by: 0

Documentation

Overview

package fusefile is the canonical authoring format for a fuse environment. a Fusefile is parsed and compiled client-side into the orchestrator wire (CreateEnvironmentRequest); the orchestrator never sees a Fusefile.

Index

Constants

View Source
const (
	// ReasonCacheDisabled means the Fusefile has no `cache: {enabled: true}`.
	ReasonCacheDisabled = "caching is not enabled"
	// ReasonStepOptedOut means the step set `cache: false`.
	ReasonStepOptedOut = "step opted out with cache: false"
	// ReasonParentUncacheable means an earlier step is uncacheable, so this
	// step has no known parent layer to build on.
	ReasonParentUncacheable = "an earlier step is uncacheable"
	// ReasonGPU means the environment requests a GPU. GPU environments run on
	// the qemu backend, whose snapshot endpoints hard-501 because a vfio
	// device cannot be checkpointed, so there is nowhere to store a layer.
	ReasonGPU = "gpu environments cannot be snapshotted"
)

reasons a step is not cacheable. these are stable strings so callers (the `--plan` renderer, and per-step hit/miss reporting) can branch on them instead of matching prose.

View Source
const BuildPhaseExitStatus = 90

BuildPhaseExitStatus is the status the generated startup script exits with when a build step fails.

The two phases travel to the guest as one opaque script: the orchestrator runs it as a single `sh -lc` with both streams sent to io.Discard, so the exit status is the only channel left to say which phase died. The script therefore traps a failure in the build phase and remaps it to this fixed status, then clears the trap before the run command, so a run failure still reports its own status unchanged.

90 is outside the statuses a shell assigns on its own behalf (126 for a command that is not executable, 127 for one that is not found, 128+n for a signal), so it is unlikely to collide with what a build step would return. A build step that genuinely exits 90 is indistinguishable from the trap, and the failing step's own status is not preserved: that is the honest cost of having one integer to carry two facts.

View Source
const MaxCopyBytes = 512 << 10

MaxCopyBytes caps the total size of everything the `copy` block ships.

It is a different number from MaxFilesBytes because it bounds a different transport. `files` is embedded in the startup script, which reaches the guest as one shell argument and is therefore bounded by MAX_ARG_STRLEN. A copy entry instead rides the create request's own files map and is uploaded per file, so what bounds it is the orchestrator's 1 MiB request body (api.MaxEnvironmentBodyBytes): 512 KiB of sources inflate to roughly 683 KiB of base64, which still leaves room in the same body for the manifest, the startup script, the secrets map, and the guest paths themselves.

The client enforces it while it walks (cli/copy.go), so `copy: {from: .}` on a repo fails naming the size and the limit instead of posting a body that was never going to be accepted. The orchestrator re-enforces it, because a client it did not compile is not one it can trust.

View Source
const MaxFilesBytes = 64 << 10

MaxFilesBytes caps the total decoded size of Fusefile.Files.

The binding constraint is not the 1 MiB create body (api.MaxEnvironmentBodyBytes) but the way the script is delivered: the orchestrator runs it as `sh -lc <script>` and the host agent joins that into one shell word before handing it to ssh, so the whole script ends up as a single argv element. Linux caps one argument at MAX_ARG_STRLEN, 128 KiB, and overflowing it fails the boot with a bare E2BIG rather than anything an author could act on.

64 KiB of files becomes roughly 88 KiB of base64 once wrapped, which leaves around 40 KiB for the prelude, setup, and run. Exceeding the cap is a compile error rather than a runtime failure, so the author learns at `fuse up` time which file pushed them over.

View Source
const MinIdleTimeout = time.Minute

MinIdleTimeout is the smallest meaningful resources.idle_timeout. Idle expiry is detected on the orchestrator's reconcile loop (30s default) and requires two consecutive observations, so anything shorter than this promises a precision the loop cannot deliver.

Variables

This section is empty.

Functions

func BaseKey added in v0.11.0

func BaseKey(image string, files []File) string

BaseKey derives the root of the chain from the base image reference and the file block. An empty image means the host's baked base rootfs, which still gets a distinct key rather than an empty one so the first step's preimage is never ambiguous.

Files belong here rather than in any step's key because they are written before the first setup step runs, so they are part of the state that step builds on. Leaving them out would let an edited `files:` entry serve a layer baked against the old contents.

func KindSupportsMIG added in v0.7.0

func KindSupportsMIG(kind string) bool

KindSupportsMIG reports whether a GPU kind can plausibly run a MIG profile. An empty kind (unknown at request time) and any unrecognized kind return true so the scheduler's per-device MIGCapable flag remains the source of truth; only a kind on the known non-MIG denylist returns false. Matching is case-insensitive and substring-based so "NVIDIA V100" and "v100-sxm2" both resolve.

func ResolveFiles added in v0.12.0

func ResolveFiles(f *Fusefile, baseDir string) error

ResolveFiles reads every Files entry that names a Source into its Content, resolving Source relative to baseDir (the Fusefile's directory).

It is separate from Compile so Compile stays a pure function of the Fusefile: only this step touches the filesystem, and a caller that already holds the content (a test, or a future non-file authoring path) can skip it. Callers that never set Source can skip it too; Compile validates either way.

func SetupScriptRange added in v0.19.0

func SetupScriptRange(f *Fusefile, from, to int, includeFiles bool) string

SetupScriptRange renders setup steps [from, to) as one runnable script.

It exists because `fuse build` runs the cacheable part of the setup phase one step at a time, snapshotting between them, rather than as the single blob compileBuildScript emits. Every one of those execs is a fresh shell, so every one of them needs the prelude again. Without it a step would run without strict mode and, far worse, in whatever directory the shell happened to start in rather than the workspace, which fails quietly rather than loudly.

includeFiles writes the file block ahead of the steps, and belongs only on the first exec of a cold build. Files are folded into BaseKey, so any layer artifact the build seeded from already contains them.

SetupScriptRange(f, 0, len(f.BuildSteps()), true) is compileBuildScript(f) byte for byte. That equivalence is what lets the stepped path and the single-blob path coexist without drifting, and there is a test pinning it.

func StartupScriptFrom added in v0.19.0

func StartupScriptFrom(f *Fusefile, from int) string

StartupScriptFrom renders the boot script for an environment seeded from a layer artifact that already covers build steps [0, from).

It is compileStartupScript with a head start: the steps the artifact baked in are dropped, and everything after them still runs, followed by the run command. from == 0 makes it compileStartupScript exactly, which is the no-cache-hit case and is pinned by a test.

Files are rewritten even though the artifact already carries the copy made when it was baked, for the same reason compileRunScript rewrites them: they are authoring inputs that may have been edited since the bake, and a few KiB is cheap. Setup is the expensive work a cache hit exists to skip; files are not.

func ValidEnvKey added in v0.21.0

func ValidEnvKey(s string) bool

ValidEnvKey reports whether s is a well-formed environment variable name.

func ValidExposeName added in v0.14.0

func ValidExposeName(s string) bool

ValidExposeName reports whether s is a well-formed expose[].as name.

func ValidGPUProfile added in v0.7.0

func ValidGPUProfile(s string) bool

ValidGPUProfile reports whether s is a well-formed MIG profile name ("1g.10gb", "2g.20gb", ...). Shared with the API layer's request validation so raw SDK callers are held to the same vocabulary as Fusefile authors.

func ValidLabel added in v0.11.0

func ValidLabel(s string) bool

ValidLabel reports whether s is a well-formed placement label key or value. Shared with the API layer so raw SDK callers and host registration are held to the same vocabulary as Fusefile authors.

func ValidName added in v0.22.0

func ValidName(s string) bool

ValidName reports whether s is a well-formed top-level name.

func Validate added in v0.12.0

func Validate(f *Fusefile) error

Validate reports every structural rule violation in f, joined into a single error.

Types

type Cache added in v0.11.0

type Cache struct {
	Enabled bool `yaml:"enabled,omitempty"`
}

Cache is the top-level opt-in for the setup layer cache. caching is off by default: a layer is a rootfs captured mid-provisioning, so opting in is a deliberate act.

type Command added in v0.20.0

type Command struct {
	// Shell is the shell-form command; set when `run` was a yaml scalar.
	Shell string
	// Argv is the exec-form argv; set when `run` was a yaml sequence.
	Argv []string
}

Command is the polymorphic `run` field. It accepts either a scalar shell command (the legacy form, interpreted by sh -lc byte for byte) or a list of strings (the exec form, each element shell-quoted into a single command line so no argument is subject to word splitting, globbing, or metacharacter reinterpretation). Exactly one of Shell or Argv is populated.

func (Command) IsEmpty added in v0.20.0

func (c Command) IsEmpty() bool

IsEmpty reports whether the command is unset: neither the shell form nor the exec form was provided. The compiler treats an empty command the same as an omitted `run` (no line emitted, setup-only or build-only scripts unaffected).

func (Command) MarshalYAML added in v0.20.0

func (c Command) MarshalYAML() (any, error)

MarshalYAML renders the command back to its source shape: a scalar for the shell form, a sequence for the exec form. This keeps a round-trip through the custom unmarshaler lossless (e.g. `fuse init`'s scaffold).

func (Command) Render added in v0.20.0

func (c Command) Render() string

Render produces the shell line emitted for the command. The shell form is passed through byte for byte so a scalar `run` compiles to exactly the script it always did; the exec form is built from shellQuote'd elements so an argument value (spaces, quotes, glob characters) can never be reinterpreted by the shell.

func (*Command) UnmarshalYAML added in v0.20.0

func (c *Command) UnmarshalYAML(node *yaml.Node) error

UnmarshalYAML accepts either a scalar (shell form) or a sequence of strings (exec form). KnownFields(true) does not propagate into a custom unmarshaler, so other node kinds -- including a mapping, which would look like an object form this type does not have -- are rejected here rather than silently dropped. An empty sequence is a decode error: an argv of zero arguments is not a command. A non-string element (an int, bool, float, or null scalar) is a decode error too: yaml.v3 silently coerces such scalars into strings when decoding into []string, so each element's tag is checked explicitly.

type Compiled

type Compiled struct {
	Spec            ResourceSpec
	ManifestJSON    []byte
	StartupScript   string
	RequiredSecrets []string
	Expose          []ExposeSpec

	// Healthcheck is the environment-level readiness probe, or nil when the
	// author declared none. It does not travel in the manifest: the manifest
	// describes services, and this probe is about the environment.
	Healthcheck *HealthcheckSpec

	// Desktop is the graphical session's geometry, or nil when the author
	// declared none. Like Healthcheck it rides beside the manifest, not in
	// it: the manifest describes services, and the desktop is about the
	// environment.
	Desktop *DesktopSpec

	// Copy is the compiled `copy` block: one entry per authored entry, with
	// its guest path resolved against the workspace. The sources are named,
	// never read. Expanding a directory into files is the CLI's job
	// (cli/copy.go), so that this compiler stays a pure function of the
	// Fusefile and `fuse compile` keeps working with no filesystem behind it.
	Copy []CopySpec

	// BuildScript is the setup phase alone, for `fuse build` to run through
	// the exec path (600s) rather than the startup-script path (30s).
	// StartupScript still carries setup+run, so `fuse up` is unchanged.
	BuildScript string

	// RunScript is the run command alone, for a boot whose setup phase is
	// already baked into a seed rootfs (`fuse up --from-build`). Replaying
	// setup there would redo the work the artifact exists to skip.
	RunScript string

	// StartupTimeoutSeconds carries Fusefile.StartupTimeout to the wire.
	// Zero means the author did not ask for one and the orchestrator's
	// default applies.
	StartupTimeoutSeconds int64
}

Compiled is the result of compiling a Fusefile: the resource spec, the manifest json to upload to the guest, the startup script to run, the secrets the environment needs at create time, and any ports to expose.

func Compile

func Compile(f *Fusefile) (*Compiled, error)

Compile turns the human-friendly Fusefile.Resources into a ResourceSpec.

type CopyEntry added in v0.21.0

type CopyEntry struct {
	// From is a file or directory on the authoring machine, resolved
	// relative to the Fusefile's directory rather than the process working
	// directory, so `fuse up ./repro/Fusefile` copies the same sources it
	// would from inside that directory. A directory is expanded
	// client-side into one entry per regular file; a symlink is an error
	// rather than something silently followed out of the tree.
	From string `yaml:"from"`

	// To is where it lands in the guest. An absolute path is taken as
	// written; a relative one resolves against the workspace, the same
	// directory setup and run already start in. For a directory source this
	// is the directory the tree lands under.
	To string `yaml:"to"`
}

CopyEntry ships one local file or directory into the guest before setup and run execute.

It is the directory-capable sibling of File, and deliberately not sugar over it: the two use different transports because they have to. A File is base64-encoded into the generated startup script, which reaches the guest as a single shell argument and is therefore capped at MaxFilesBytes by Linux's MAX_ARG_STRLEN. A source tree does not fit in 64 KiB. A copy entry instead rides the create request's own files map, which the orchestrator uploads into the guest one file at a time (Environment.Upload) before it runs the startup script. Same "lands before setup" ordering, no argv ceiling, and MaxCopyBytes rather than MaxFilesBytes as the bound.

What copy gives up in exchange is the mode bit: the upload wire carries a path and a body and nothing else, so an executable copied in arrives without its executable bit and the author has to `chmod` it in `setup`. File keeps its `mode` because the script it compiles into can chmod on the author's behalf.

type CopySpec added in v0.21.0

type CopySpec struct {
	// From is the authored source path, carried through unchanged. It is
	// still relative to the Fusefile's directory, because only the client
	// that reads the Fusefile knows where that is.
	From string

	// To is the resolved absolute guest path. A file source lands exactly
	// here; a directory source lands its tree under here.
	To string
}

CopySpec is one compiled copy entry: an authoring-side source and the absolute guest path it lands at.

type Desktop added in v0.24.0

type Desktop struct {
	// Width is the display width in pixels.
	Width int `yaml:"width"`
	// Height is the display height in pixels.
	Height int `yaml:"height"`
}

Desktop is the geometry of the environment's graphical session. Both fields are required: a desktop with a guessed dimension would silently shift every coordinate a computer-use model emits, which presents as model failure rather than as the config error it is. The display number is fixed at :1 and deliberately not authorable until something needs a second display.

type DesktopSpec added in v0.24.0

type DesktopSpec struct {
	Width  int
	Height int
}

DesktopSpec is the compiled desktop block. It mirrors internal/api.DesktopSpec field-for-field, the same arrangement HealthcheckSpec uses.

type EnvValue

type EnvValue struct {
	Value  string `yaml:"value,omitempty"`
	Secret string `yaml:"secret,omitempty"`
}

EnvValue is either a literal value or a secret reference. exactly one is set.

type Expose

type Expose struct {
	Port int    `yaml:"port"`
	As   string `yaml:"as,omitempty"`
}

Expose publishes a guest port to the outside world.

type ExposeSpec

type ExposeSpec struct {
	Port int
	As   string
}

ExposeSpec requests that a guest port be published as a reachable endpoint. Mirrors Fusefile's Expose entries one-for-one.

type File added in v0.12.0

type File struct {
	// Path is where the file lands in the guest. A relative path resolves
	// against the workspace.
	Path string `yaml:"path"`
	// Source is a path on the authoring machine, relative to the Fusefile.
	Source string `yaml:"source,omitempty"`
	// Content is a literal file body.
	Content string `yaml:"content,omitempty"`
	// Mode is an octal permission string, e.g. "0755". Empty leaves the
	// mode to the guest's umask.
	Mode string `yaml:"mode,omitempty"`
}

File is one file materialized inside the guest before setup runs.

Exactly one of Source (a path on the authoring machine, resolved relative to the Fusefile's directory by ResolveFiles) and Content (a literal) is set. Content is what the compiler emits; Source is sugar that ResolveFiles reads into Content, which keeps Compile a pure function of the Fusefile.

This is a transport for config and small code, not for model weights or datasets: entries are base64-encoded into the startup script, which travels in the create request body, so MaxFilesBytes caps their total size. Fetch large artifacts from inside `setup` instead.

type Fusefile

type Fusefile struct {
	Version int `yaml:"version"`

	// Name is the task id this Fusefile boots under, and through it the
	// environment's identity: the orchestrator names the vm by prefixing the
	// task id, so `name: sandbox` is the environment `fuse-sandbox`.
	//
	// It is optional and `fuse up --task-id` still wins, but declaring it is
	// what makes the name a property of the project rather than of whatever
	// directory the file happens to sit in. Without it the CLI falls back to
	// the parent directory's name, so two checkouts of the same repo derive
	// the same id and the second `up` collides.
	//
	// Held to a DNS label for the same reason expose[].as is: it becomes part
	// of a vm name rather than free text.
	Name string `yaml:"name,omitempty"`

	Image     string      `yaml:"image,omitempty"`
	Resources Resources   `yaml:"resources,omitempty"`
	Placement Placement   `yaml:"placement,omitempty"`
	Cache     Cache       `yaml:"cache,omitempty"`
	Files     []File      `yaml:"files,omitempty"`
	Copy      []CopyEntry `yaml:"copy,omitempty"`

	// Env is set for every build step and for Run. it reuses the same
	// value-or-secret grammar as services.<name>.env, so there is one env
	// shape in the file. services do not inherit it: a service runs in its
	// own container and takes its environment from its own env map.
	//
	// a value never reaches the generated script. the compiler carries the
	// block in the manifest instead; the orchestrator resolves any secret
	// reference and renders the result to a file under /fuse, and the script
	// sources that path. the script is handed to the guest as a single
	// `sh -lc` argument, so anything interpolated into its text is readable
	// in the host's process table for the length of the boot.
	//
	// a key is written into that file unquoted, as a shell identifier, so
	// validate holds it to ^[A-Za-z_][A-Za-z0-9_]*$.
	Env map[string]EnvValue `yaml:"env,omitempty"`

	// Build is the work that prepares the environment: it runs to completion
	// before Run, in the same workspace, and a failure in it is reported as a
	// build failure rather than as a failure of the task itself. It is the
	// first-class spelling of the phase.
	Build []Step `yaml:"build,omitempty"`

	// Setup is a deprecated alias for Build, kept accepted so every Fusefile
	// written before the rename keeps compiling byte for byte. Setting both is
	// a validation error rather than a concatenation: which half runs first
	// would be a guess, and running steps in an order the author did not write
	// is worse than making them pick a spelling.
	//
	// Nothing downstream reads either field directly. BuildSteps resolves the
	// alias in one place so the compiler, the layer keys, and the CLI cannot
	// disagree about which one won.
	Setup []Step `yaml:"setup,omitempty"`

	Services  map[string]Service `yaml:"services,omitempty"`
	Run       Command            `yaml:"run,omitempty"`
	Workspace string             `yaml:"workspace,omitempty"`
	Expose    []Expose           `yaml:"expose,omitempty"`
	Secrets   []string           `yaml:"secrets,omitempty"`

	// Healthcheck is the environment-level readiness probe. It is optional
	// and there is at most one: the point of the block is that the whole
	// sandbox has a single verdict.
	Healthcheck *HealthProbe `yaml:"healthcheck,omitempty"`

	// Desktop declares that the environment boots a graphical session and at
	// what geometry. It requires an image that carries the desktop stack
	// (see fc-bake-desktop-rootfs.sh); on any other image the declaration is
	// inert and the computer surface answers 503. Nil means no desktop, and
	// stays nil all the way down, the same contract Healthcheck has.
	Desktop *Desktop `yaml:"desktop,omitempty"`

	// StartupTimeout bounds the generated startup script (build + run) as a
	// go duration, e.g. "45s". Empty means the orchestrator's default. The
	// orchestrator rejects a value above its configured ceiling rather than
	// clamping it, so an author always knows what bound they got.
	//
	// This is not the knob for a genuinely long setup phase: the ceiling is
	// bounded by the control plane's HTTP write timeout because create is
	// synchronous. Bake long work into an image with `fuse build` instead.
	StartupTimeout string `yaml:"startup_timeout,omitempty"`
}

Fusefile is the v1 authoring contract.

func Decode added in v0.12.0

func Decode(data []byte) (*Fusefile, error)

Decode decodes a Fusefile from yaml with a strict decoder without validating it. Parse is the normal entry point; Decode is separate so a caller that wants to report structural and compile problems together (`fuse validate`) can run Validate and Compile over the same decoded file.

func Parse

func Parse(data []byte) (*Fusefile, error)

Parse decodes a Fusefile from yaml bytes using a strict decoder (unknown fields are rejected) and then validates the result. It returns the parsed Fusefile only if it is structurally valid.

func (*Fusefile) BuildSteps added in v0.21.0

func (f *Fusefile) BuildSteps() []Step

BuildSteps returns the effective build steps: Build when the author wrote `build:`, and the deprecated `setup:` alias otherwise. Validate rejects setting both, so at most one of the two is populated in a file that got as far as being compiled.

Every consumer of the phase goes through here (the generated scripts, the layer keys, the CLI's step labels), so the alias is resolved exactly once and nothing downstream has to know which key the author typed.

type HealthCheck added in v0.13.0

type HealthCheck struct {
	Test     []string `yaml:"test,omitempty"`
	Interval string   `yaml:"interval,omitempty"`
	Timeout  string   `yaml:"timeout,omitempty"`
	Retries  int      `yaml:"retries,omitempty"`
}

HealthCheck is a compose-native container healthcheck. Interval and Timeout are go durations (e.g. "10s").

type HealthProbe added in v0.21.0

type HealthProbe struct {
	// HTTP probes a port inside the guest. Successful means a response with
	// a 2xx or 3xx status.
	HTTP *HealthProbeHTTP `yaml:"http,omitempty"`

	// Exec is an argv run inside the guest. Successful means exit status 0.
	// It is an argv rather than a shell string so an argument can never be
	// reinterpreted by a shell, matching the exec form of `run`.
	Exec []string `yaml:"exec,omitempty"`

	// Interval is how long the guest agent waits between attempts, as a go
	// duration ("5s"). Empty means the guest agent's default.
	Interval string `yaml:"interval,omitempty"`

	// Timeout bounds one attempt, as a go duration ("2s"). Empty means the
	// guest agent's default. It may not exceed Interval: attempts are run one
	// at a time, so a timeout longer than the interval would silently stretch
	// the interval instead of overlapping attempts.
	Timeout string `yaml:"timeout,omitempty"`

	// Retries is how many consecutive failed attempts flip the verdict from
	// passing to failing. Zero means the guest agent's default. A single
	// failed attempt is not a verdict: a probe that is retried is the only
	// kind worth reporting on.
	Retries int `yaml:"retries,omitempty"`

	// StartPeriod is a grace window measured from the first attempt during
	// which failures do not count against Retries, as a go duration ("10s").
	// It covers the gap between "the process was started" and "the process
	// is serving", which is exactly the gap this probe exists to close. Once
	// the probe passes once, the window is over for good: a service that came
	// up and then fell over is failing, not still starting.
	StartPeriod string `yaml:"start_period,omitempty"`
}

HealthProbe is the environment-level readiness probe: one verdict for whether the sandbox as a whole is doing its job, visible to the control plane.

It is deliberately NOT the service-level HealthCheck above, and the two are not interchangeable. That one is compose-native, governs a single container, and is evaluated by the guest's `docker compose`; a container it marks unhealthy never signals the orchestrator. This one is evaluated by the guest agent (fused) over the environment as a whole, and its verdict travels back to the fleet, which is what makes `fuse up --wait-healthy` mean anything.

Exactly one of HTTP and Exec is set. Two probes would produce two verdicts, and a block whose whole purpose is a single answer must not have to reconcile them.

type HealthProbeHTTP added in v0.21.0

type HealthProbeHTTP struct {
	// Port is the guest-side port to dial, 1 to 65535.
	Port int `yaml:"port"`
	// Path is the request path, which must start with "/". Empty means "/".
	Path string `yaml:"path,omitempty"`
}

HealthProbeHTTP is an HTTP GET against a port inside the guest. The probe runs in-guest, so the port needs no `expose` entry: this asks whether the app is serving, not whether anyone outside can reach it.

type HealthcheckHTTP added in v0.21.0

type HealthcheckHTTP struct {
	Port int
	Path string
}

HealthcheckHTTP is the compiled form of an in-guest HTTP probe.

type HealthcheckSpec added in v0.21.0

type HealthcheckSpec struct {
	HTTP *HealthcheckHTTP
	Exec []string

	IntervalSeconds    int64
	TimeoutSeconds     int64
	Retries            int
	StartPeriodSeconds int64
}

HealthcheckSpec is the compiled environment-level probe. It mirrors internal/api.HealthcheckSpec field-for-field so the CLI can copy it onto the wire without importing the api package, the same arrangement ResourceSpec and ExposeSpec use.

Durations are carried as seconds rather than as go duration strings because the wire is language-neutral, and a zero means the author omitted the field and the guest agent's default applies. That is the same convention StartupTimeoutSeconds uses, so "unset" reads the same way everywhere.

type InputDigester added in v0.11.0

type InputDigester func(patterns []string) (string, error)

InputDigester hashes a step's declared `inputs` patterns and returns the digest to fold into that step's key. It is supplied by the caller because this package deliberately does no filesystem IO: only the CLI knows the directory the Fusefile was read from. It must return "" for an empty pattern list.

type LayerKey added in v0.11.0

type LayerKey struct {
	// Index is the step's position in the build phase (Fusefile.BuildSteps).
	Index int
	// Script is the shell fragment this step emits, hashed byte for byte.
	Script string
	// Key is the step's content-addressed layer key, hex sha256. Empty when
	// the step is not cacheable.
	Key string
	// ParentKey is the key this step was chained onto: the previous step's
	// key, or the base key for the first step. Empty when not cacheable.
	ParentKey string
	// InputsDigest is the digest of the step's declared inputs, "" when the
	// step declares none.
	InputsDigest string
	// Cacheable reports whether this step can produce or restore a layer.
	Cacheable bool
	// Reason explains why Cacheable is false; empty when it is true.
	Reason string
}

LayerKey is the derived cache identity of one build step.

func LayerKeys added in v0.11.0

func LayerKeys(f *Fusefile, inputs InputDigester, opts LayerOptions) ([]LayerKey, error)

LayerKeys derives the layer key of every build step, in order.

The key is a chained hash, so it invalidates in exactly one direction: edit step N and steps N..end all get new keys, while steps before N are untouched. That is also why a miss cascades: step N+1's key is defined in terms of step N's, so a step whose parent is unknown cannot be keyed at all and neither can anything after it.

layer_key(i) = sha256(
    "fusefile-layer/v2\n" +
    parent_key(i) + "\n" +   // layer_key(i-1), or the base key for i == 0
    step_script(i) + "\n" +  // the fragment the step emits, byte for byte
    inputs_digest(i) + "\n" +
    workspace)               // setup runs after `cd <workspace>`

Deliberately excluded: secret values (a layer must not be keyed on material it is not allowed to bake in, so a step that reads secrets opts out instead), the task id, every `resources` field (none of them change the filesystem), and `run:` (it is the entrypoint, not a layer).

Arch is excluded too, but for a different reason: it is a real constraint, just not a derivable one. An ext4 rootfs genuinely is not portable across architectures, so an arm64 artifact must never be served to an amd64 boot. The problem is that nothing here knows the answer. The key is derived client-side, before any host has been scheduled, so the only arch available at this point is the operator's laptop, which has no relationship to the host the build lands on. Folding that in would produce a key that asserts something false: an operator on an arm64 Mac building on an amd64 host would label the artifact arm64, and a later genuinely-arm64 lookup would hit that key and be handed amd64 bytes. A key that lies is worse than a key that misses. So arch is recorded on the artifact from the host that actually built it, and applied as a separate equality filter at lookup time alongside the tenant and the key.

LayerKeys is pure apart from the digester it is handed, so it can be tested without touching a disk.

type LayerOptions added in v0.11.0

type LayerOptions struct {
	// BaseKey identifies the base rootfs the first step builds on. Empty
	// means derive it from Fusefile.Image via BaseKey.
	//
	// Ideally this is a digest of the resolved rootfs bytes; the host agent
	// does not expose one yet (it resolves `image` to a file by name with no
	// content hash), so today it is a digest of the image reference. That is
	// a weaker binding: republishing the same tag with new contents does not
	// invalidate. Wiring a real rootfs digest is backend work.
	BaseKey string
}

LayerOptions carries the key component that is not derivable from the Fusefile itself.

type Placement added in v0.11.0

type Placement struct {
	// Host pins the environment to an exact host id. A pin is not an
	// override: the pinned host still has to be active, run the right
	// backend, and fit the request.
	Host string `yaml:"host,omitempty"`

	// Labels must all match the host's operator-declared labels (AND).
	Labels map[string]string `yaml:"labels,omitempty"`
}

Placement constrains which host in a self-hosted fleet may run the environment. Every field is a hard gate, not a preference: a request that matches no host is rejected immediately, never queued.

Region is deliberately absent; it lives under Resources for historical reasons and a second spelling of the same selector would be worse than the block split.

type ResourceSpec

type ResourceSpec struct {
	CPUs      int32
	RamMB     int32
	StorageGB int32
	Region    string
	// Arch restricts scheduling to hosts of this CPU architecture ("amd64",
	// "arm64"). Not yet authorable in a Fusefile; carried for wire parity
	// so compile output mirrors the orchestrator's spec shape.
	Arch              string
	MaxRuntimeSeconds int64
	// IdleTimeoutSeconds destroys the environment after this many seconds
	// with no exec and no attach session. Zero means no idle expiry.
	IdleTimeoutSeconds int64
	Image              string
	GPUs               int32
	GPUKind            string
	GPUProfile         string
	// HostID pins the environment to an exact host id (placement.host).
	HostID string
	// Labels are the placement label selectors (placement.labels); every
	// pair must match the host's declared labels.
	Labels map[string]string
}

ResourceSpec mirrors internal/api.ResourceSpec field-for-field so the CLI can copy Compiled.Spec into the sdk/api spec without importing the api package.

Image selects the VM's base rootfs at create time (a name resolved by the firecracker host agent to a pre-baked rootfs file; see FUSEFILE_PLAN.md Phase 7). It lives here, not in the manifest json, because rootfs selection happens at Provider.Create — before the guest boots and long before the manifest is ever uploaded to it.

type Resources

type Resources struct {
	CPUs    VCPUs  `yaml:"cpus,omitempty"`
	GPU     int    `yaml:"gpu,omitempty"`      // device count: whole GPUs, or MIG instances when gpu_profile is set
	GPUKind string `yaml:"gpu_kind,omitempty"` // optional match, e.g. "a100"
	// GPUProfile requests fractional GPU allocation: a MIG profile in
	// nvidia mig-parted vocabulary (e.g. "1g.10gb", "2g.20gb"). When set,
	// `gpu` counts MIG instances of this profile rather than whole
	// devices (decision D5). Empty means whole-device allocation.
	GPUProfile string `yaml:"gpu_profile,omitempty"`
	Memory     string `yaml:"memory,omitempty"` // e.g. "2GB", "2G", "2GiB", "512MB"
	// Disk sizes the guest root disk, e.g. "10GB". It is the preferred
	// spelling; Storage is a permanent alias kept for files written before
	// the rename. Setting both is allowed only if they mean the same size.
	Disk       string `yaml:"disk,omitempty"`
	Storage    string `yaml:"storage,omitempty"`     // alias for Disk, never removed
	Region     string `yaml:"region,omitempty"`      // schedules only onto a host registered in this region; empty matches any
	MaxRuntime string `yaml:"max_runtime,omitempty"` // go duration
	// IdleTimeout destroys the environment after this long with no exec
	// and no attach session. Go duration. "idle" means exactly that: no
	// control-plane activity. In-guest CPU or network traffic is not
	// observed. Empty means no idle expiry.
	IdleTimeout string `yaml:"idle_timeout,omitempty"` // go duration
}

Resources is the human-friendly hardware spec; compiled to ResourceSpec.

type Service

type Service struct {
	Image string              `yaml:"image"`
	Ports []int               `yaml:"ports,omitempty"`
	Env   map[string]EnvValue `yaml:"env,omitempty"`

	// Command, Restart, HealthCheck, and DependsOn map one-to-one onto their
	// compose-native counterparts; the guest's `docker compose up` (decision
	// D1) is what interprets them, so no new runtime semantics are
	// introduced here. They govern the compose container, not the VM: a
	// failing healthcheck marks the service unhealthy inside the guest, it
	// does not signal the orchestrator that the environment is dead.
	Command []string `yaml:"command,omitempty"`
	// Restart must be one of the compose-native policies: "no", "always",
	// "on-failure", "unless-stopped".
	Restart     string       `yaml:"restart,omitempty"`
	HealthCheck *HealthCheck `yaml:"healthcheck,omitempty"`
	DependsOn   []string     `yaml:"depends_on,omitempty"`
}

Service is one in-vm service; compiled to manifest.services and a compose unit.

type Step added in v0.11.0

type Step struct {
	Run    string   `yaml:"run"`
	Inputs []string `yaml:"inputs,omitempty"`
	Cache  *bool    `yaml:"cache,omitempty"`

	// Workdir scopes this one step to a directory. a relative path resolves
	// against Fusefile.Workspace, since every step starts there. the step is
	// emitted as a subshell, so the directory change does not leak into the
	// next step; only the directory is scoped, not the shell (see the note on
	// setupScripts). empty means the step runs in the workspace, unchanged.
	Workdir string `yaml:"workdir,omitempty"`
}

Step is one build step, under either spelling of the phase (`build:` or the deprecated `setup:`). it accepts two yaml forms: a bare scalar ("apt-get update -qq"), equivalent to {run: ...} and unchanged from v1's list of strings; and a mapping ({run: npm ci, inputs: [package.json]}), which adds inputs, cache, and workdir.

Cache is a pointer so "unset" is distinguishable from an explicit "cache: false": a step that reads secrets or writes outside the rootfs must opt out, and an unset field must not be read as an opt-out.

func (*Step) UnmarshalYAML added in v0.11.0

func (s *Step) UnmarshalYAML(node *yaml.Node) error

UnmarshalYAML decodes either a bare scalar (the legacy form, kept working byte for byte) or the mapping form.

type VCPUs added in v0.14.0

type VCPUs int32

VCPUs is a whole vCPU count. It accepts a yaml integer or a whole-valued float, so `cpus: 2` and `cpus: 2.0` both mean two vCPUs, and rejects a genuine fraction: firecracker takes an integer vcpu_count, qemu an integer -smp, and neither host agent writes a cgroup quota, so there is nothing in the stack that could honor `cpus: 0.5`.

func (*VCPUs) UnmarshalYAML added in v0.14.0

func (c *VCPUs) UnmarshalYAML(node *yaml.Node) error

UnmarshalYAML decodes the vCPU count through float64 so a whole-valued float is accepted and a fraction is rejected with a reason.

Jump to

Keyboard shortcuts

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