compose

package
v0.2.1 Latest Latest
Warning

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

Go to latest
Published: Oct 12, 2026 License: Apache-2.0 Imports: 21 Imported by: 0

Documentation

Overview

Package compose parses a Docker Compose file into Levelrail's own desired-state model, in two shapes depending on the caller. The direct-import path (ToDesiredServices, via Validate) is deliberately narrow: every service needs a pre-built image (no build:), since there is no build context to build one from (a pasted file, no git checkout). The git-sourced expand path (ExpandBuildService, via ValidateForBuild) allows build: for exactly that reason: it always has a real checkout. Both paths share the same narrow scope otherwise: environment/ports/volumes accept both Compose's short and long forms (see yaml.go), but only the fields Levelrail's own model has room for: no port ranges, no UDP, no tmpfs/npipe mounts. restart: and networks: both parse and are surfaced as non-blocking Notices instead of being silently dropped or translated: see Notices for why neither has a real translation onto how Levelrail runs a service. depends_on: parses into spec.Service.DependsOn / store. DesiredService.DependsOn and IS enforced: internal/reconcile/ application.Controller waits for a dependency's container to start before creating this service's own, real Docker Compose's own default depends_on semantic (service_started, not service_healthy). command: and entrypoint: both parse and translate into store.DesiredService's own Command and Entrypoint fields. volumes: additionally accepts an absolute host path on the left side as a bind mount (ValidateForBuild rejects one; see that method's own doc comment for why), gated at the HTTP layer to AbilityRoot and, even then, against internal/bindmount's own forbidden-path list (see validateBindMountHostPath).

Index

Constants

View Source
const (
	DependsOnConditionStarted = "service_started"
	DependsOnConditionHealthy = "service_healthy"
)

Depends_on conditions from the long form. service_completed_successfully has no one-shot job model here, so it is treated as service_started.

View Source
const PullPolicyAlways = "always"

PullPolicyAlways is pull_policy: always, the only non-default value this package supports.

Variables

This section is empty.

Functions

func ExpandBuildService

func ExpandBuildService(svc spec.Service, sourceDir string) (services map[string]spec.Service, warnings []string, err error)

ExpandBuildService parses the compose file at svc.Build.Path (relative to sourceDir) and returns one spec.Service per compose service, plus a warning for each healthcheck left untranslated.

func FromDesiredServices added in v0.2.0

func FromDesiredServices(appName string, services []store.DesiredService) (string, error)

FromDesiredServices builds a compose.yaml that reconstructs appName's services as closely as this package's File/Service model allows. Secret, database, and vault env values never carry over as real values, only a ${SERVICE_SECRET_<key>} placeholder (no secret value is ever captured, the whole point of this function). Domains, HostPort/BindAddress, bind mounts, resources, health checks, and egress policy are dropped too: none of those round-trips through this package's own compose model (see compose.go's doc comment).

func GenerateValue

func GenerateValue(kind string, length int) (string, error)

GenerateValue produces a real value for a generatable MagicVar kind. length <= 0 uses each kind's own sane default.

func ToDesiredServices

func ToDesiredServices(appName string, f *File) (services []store.DesiredService, warnings []string, err error)

ToDesiredServices translates f into one store.DesiredService per compose service, named "<appName>-<serviceKey>" and linked to appName as their AppID (matching the naming convention internal/deploy's own multi-service fan-out uses). Runs Validate first, so a caller only needs to call this one function.

warnings carries one message per service whose healthcheck: could not be translated (see resolveHealthcheck), for the caller to surface.

A bind-mount volumes: entry (compose.go's own doc comment) becomes a store.ServiceBindMount, not a store.ServiceVolume: the caller must require AbilityRoot before persisting a result carrying any BindMounts, this package has no notion of a caller's abilities.

Types

type Command added in v0.2.0

type Command []string

Command is command:'s and entrypoint:'s shared string-or-list union: a list passes through as exec-form args, a bare string is Compose's own shorthand for shell form, wrapped here as ["/bin/sh", "-c", "<string>"] to match Docker's own documented interpretation of a string CMD or ENTRYPOINT.

func (*Command) UnmarshalYAML added in v0.2.0

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

UnmarshalYAML implements the string-or-list union described above.

type DependsOn added in v0.2.0

type DependsOn []string

DependsOn is a service's own depends_on:'s list-or-map union, the same shape as Networks: either a plain list of service names, or a map of name to per-dependency config (condition, restart, ...) decoded down to just the names.

func (*DependsOn) UnmarshalYAML added in v0.2.0

func (d *DependsOn) UnmarshalYAML(node *yaml.Node) error

UnmarshalYAML implements the list-or-map union described above.

type Deploy added in v0.2.0

type Deploy struct {
	Resources struct {
		Reservations struct {
			Devices []Device `yaml:"devices"`
		} `yaml:"reservations"`
	} `yaml:"resources"`
	// Replicas maps onto the same store.DesiredService.Replicas /
	// spec.Service.Replicas field a service's own top-level replicas:
	// (app.yaml) already resolves through. Zero means unset.
	Replicas int `yaml:"replicas"`
	// contains filtered or unexported fields
}

Deploy is the deploy: block; only resources.reservations.devices (GPU device reservations) and replicas are read. Every other subkey (mode, placement, update_config, rollback_config, restart_policy, endpoint_mode, labels, ...) is a Swarm-specific field with no meaning outside a Swarm cluster, so UnmarshalYAML records its presence in unsupported rather than letting it silently parse and vanish: see validateDeploy.

func (*Deploy) UnmarshalYAML added in v0.2.0

func (d *Deploy) UnmarshalYAML(node *yaml.Node) error

UnmarshalYAML decodes the known resources:/replicas: shapes normally, then walks the raw mapping a second time to record every other top-level key present, so validateDeploy can report exactly which Swarm-specific fields a service declared instead of a generic "deploy not supported".

type Device added in v0.2.0

type Device struct {
	Driver       string           `yaml:"driver"`
	Count        yamlScalarString `yaml:"count"`
	DeviceIDs    []string         `yaml:"device_ids"`
	Capabilities []string         `yaml:"capabilities"`
}

Device is one deploy.resources.reservations.devices entry.

type Environment

type Environment map[string]string

Environment is environment:'s string-or-list union: a KEY: VALUE map, or a list of "KEY=VALUE" strings (a bare "KEY" means "inherit from the host shell" in real Compose, decoded here as an empty value since there's no host shell to inherit from).

func (*Environment) UnmarshalYAML

func (e *Environment) UnmarshalYAML(node *yaml.Node) error

UnmarshalYAML implements the map-or-list union described above.

type File

type File struct {
	Version  string
	Services map[string]Service
	// Domains maps a service key to the real domain it should be
	// reachable at, from the top-level x-levelrail-domains extension
	// (Compose's own reserved x- prefix for tool-specific keys). Used
	// both to set that service's own store.DesiredService.Domains (real
	// ingress routing) and to resolve any ${SERVICE_FQDN_*} reference
	// within that same service's environment (ResolveMagicVars).
	Domains map[string]string
	// Networks lists this file's own top-level networks: names, sorted.
	// Only used by Notices to detect that custom networks were declared
	// at all; Levelrail doesn't create per-network isolation from this.
	Networks []string
	// HasSecrets/HasConfigs record whether this file declares a top-level
	// secrets: or configs: block: neither has a translation onto this
	// platform's own env-var/Levelrail-secrets model, so validate rejects
	// either outright (unlike restart:/networks:/depends_on:, which parse
	// and are merely non-blocking Notices) rather than silently dropping
	// values an operator would reasonably expect to reach a container.
	HasSecrets bool
	HasConfigs bool
}

File is a parsed compose.yaml.

func Parse

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

Parse decodes a compose.yaml document.

func (*File) ApplyProfiles added in v0.2.0

func (f *File) ApplyProfiles(active []string) ([]string, error)

ApplyProfiles drops every service whose profiles: list shares nothing with active, the same default Compose applies (a service with no profiles always runs). It returns the dropped service keys, sorted, and fails when a kept service depends on a dropped one.

func (*File) Notices

func (f *File) Notices() []Notice

Notices reports every non-blocking observation across f:

  • restart: (NoticeLevelNote): Levelrail's reconciler, not Docker's native restart policy, is the sole authority on keeping a container running (see docker.ContainerSpec's own doc comment on why every container it creates gets Docker's "no" restart policy). A declared restart: policy other than "no" parses but has no effect, so this is surfaced as a note rather than fabricating a translation that wouldn't be true.
  • networks: (NoticeLevelWarning): every service in an app already shares one flat Docker network (internal/reconcile/application's NetworkName, one per app, not per compose network), so a compose file's custom networks: can't isolate services from each other here the way they would under real Compose. That's a real semantic gap for a file that expressed isolation intent, worth a warning rather than a silent drop.
  • depends_on: (NoticeLevelNote): parses into DependsOn and IS enforced as start order; condition: service_healthy additionally waits for the dependency's readiness condition.

func (*File) Profiles added in v0.2.0

func (f *File) Profiles() []string

Profiles lists every profile name any service declares, sorted.

func (*File) Validate

func (f *File) Validate() error

Validate reports every unsupported-shape problem across all services, not just the first, so a template author can fix them in one pass. Used by the direct-import path (ToDesiredServices), which has no build context (no git checkout, just a pasted file) to build a build: block from, so it rejects one outright. See ValidateForBuild for the git-sourced deploy-spec path, which does have one.

func (*File) ValidateForBuild

func (f *File) ValidateForBuild() error

ValidateForBuild is Validate, except a service's build: block is allowed rather than rejected: used only by the git-sourced expand-a-compose-file-into-services path (ExpandBuildService), which has a real checkout to resolve a build context against, unlike the direct-import path Validate itself still guards. Bind mounts stay rejected on this path even though Validate now allows them: the expanded result is a spec.Service (toSpecService, expand.go), and spec.Volume has no host-path concept to carry one into, so allowing one through here would either drop it silently or produce a nonsensical empty-named volume downstream.

type Healthcheck

type Healthcheck struct {
	Test        healthcheckTest `yaml:"test"`
	Interval    string          `yaml:"interval"`
	Timeout     string          `yaml:"timeout"`
	Retries     int             `yaml:"retries"`
	StartPeriod string          `yaml:"start_period"`
}

Healthcheck is one service's healthcheck: block, Docker Compose's own command-based health check schema (test/interval/timeout/retries/ start_period). Docker never runs it: resolveHealthcheck (healthcheck.go) translates it into this platform's own readiness probe.

type HostAccess added in v0.2.0

type HostAccess struct {
	Privileged  bool     `yaml:"privileged"`
	NetworkMode string   `yaml:"network_mode"`
	Pid         string   `yaml:"pid"`
	Ipc         string   `yaml:"ipc"`
	Uts         string   `yaml:"uts"`
	UsernsMode  string   `yaml:"userns_mode"`
	Cgroup      string   `yaml:"cgroup"`
	CapAdd      []string `yaml:"cap_add"`
	Devices     []any    `yaml:"devices"`
	SecurityOpt []string `yaml:"security_opt"`
	VolumesFrom []string `yaml:"volumes_from"`
}

HostAccess holds the service keys that widen a container's reach into the host. None has a safe translation, so validate rejects every one that is set, citing the dockerguard rule the socket proxy would apply.

type MagicVar

type MagicVar struct {
	Token       string
	Kind        string
	Key         string
	Length      int
	Default     string
	HasDefault  bool
	Generatable bool
}

MagicVar is one parsed SERVICE_ placeholder found in an env value.

func FindMagicVars

func FindMagicVars(s string) []MagicVar

FindMagicVars returns every SERVICE_ token in s, in encounter order, including duplicates.

type Networks

type Networks []string

Networks is a service's own networks:'s list-or-map union: a plain list of network names, or a map of name to per-network config (aliases, ipv4_address, ...) this decodes down to just the name, same reasoning as rawFile.Networks above.

func (*Networks) UnmarshalYAML

func (n *Networks) UnmarshalYAML(node *yaml.Node) error

UnmarshalYAML implements the list-or-map union described above.

type Notice

type Notice struct {
	Level   NoticeLevel
	Message string
}

Notice is one non-blocking observation about a parsed compose file.

type NoticeLevel

type NoticeLevel string

NoticeLevel distinguishes a purely informational Notice from an operator-facing Warning about a semantic gap. Neither ever fails Validate: both describe compose keywords that parsed successfully but don't behave the way they would under real Docker Compose.

const (
	NoticeLevelNote    NoticeLevel = "note"
	NoticeLevelWarning NoticeLevel = "warning"
)

The two NoticeLevel values Notices ever produces.

type Port

type Port struct {
	HostPort      int
	ContainerPort int
}

Port is one short-form ports: entry. ContainerPort is what store.DesiredService.Port (a single container port, not a host:container pair) actually uses.

func (*Port) UnmarshalYAML

func (p *Port) UnmarshalYAML(node *yaml.Node) error

UnmarshalYAML supports both of ports:'s forms: the short scalar form ("container" or "host:container") and the long mapping form (target/published/protocol/mode keys). mode: is accepted but ignored, since Levelrail has no host-vs-ingress publishing distinction to carry it onto.

type Service

type Service struct {
	Image       string
	Build       *rawBuild
	Environment Environment
	Ports       []Port
	Volumes     []Volume
	Labels      map[string]string
	Networks    Networks
	Restart     string
	Healthcheck *Healthcheck
	// DependsOn is depends_on:, enforced by the reconciler as a start-
	// order guarantee (see this file's own doc comment and Notices).
	DependsOn DependsOn
	// Command overrides the image's own default CMD
	// (store.DesiredService.Command), parsed from command:'s own
	// string-or-list union (Command's own UnmarshalYAML in yaml.go): a
	// plain string is shell-wrapped as ["/bin/sh", "-c", "<string>"],
	// matching Compose's own documented behavior for that form.
	Command Command
	// Entrypoint overrides the image's own default ENTRYPOINT
	// (store.DesiredService.Entrypoint), parsed with the same
	// string-or-list union as Command.
	Entrypoint Command
	// PullPolicy is pull_policy:, normalized by normalizePullPolicy into
	// exactly the two states store.DesiredService.PullPolicy
	// distinguishes: PullPolicyAlways forces a fresh pull even when the
	// image is already present locally, empty means today's existing
	// pull-if-absent behavior. Real Compose's other recognized spellings
	// ("missing", "if_not_present", an unset field) all normalize to
	// empty too; "never" and "build" are rejected, see
	// normalizePullPolicy.
	PullPolicy string
	// Deploy carries the GPU device reservation and replicas count
	// (gpu.go).
	Deploy *Deploy
	// Secrets/Configs are this service's own secrets:/configs: references
	// (short string form or long {source, target,...} map form, either
	// decodes fine into []any since only their presence matters here),
	// the per-service counterpart to File.HasSecrets/HasConfigs; same
	// rejection reasoning, see validateComposeServices.
	Secrets []any
	Configs []any
	// Profiles gates the service behind compose profiles (ApplyProfiles).
	Profiles   []string
	HostAccess HostAccess
	// DependsOnHealthy lists the DependsOn entries declared with
	// condition: service_healthy, which wait for readiness, not just start.
	DependsOnHealthy []string
}

Service is one entry under services:.

type UnresolvedVar

type UnresolvedVar struct {
	Service string
	EnvKey  string
	Token   string
}

UnresolvedVar is one SERVICE_ token ResolveMagicVars couldn't resolve: not a generatable kind, and no bash-style ${...:-default} fallback.

func ResolveMagicVars

func ResolveMagicVars(
	f *File,
	generate func(kind, key string, length int) (string, error),
	persist func(serviceKey, envKey, value string) error,
) (secretEnv map[string][]string, unresolved []UnresolvedVar, err error)

ResolveMagicVars scans every service's environment and command for SERVICE_ placeholders, mutating f in place: a token with a bash-style default substitutes that default as a literal value; a generatable token (PASSWORD/USER/BASE64/HEX/REALBASE64) found in Environment is removed from it entirely and returned via secretEnv instead, since its real value belongs in secret storage, not a literal desired-state column. Command has no equivalent secret-storage indirection (a Command entry can't be swapped for a decrypted-at-create-time secret the way an env var can), so a generatable token found there is always reported as unresolved instead, same as a default-less non-generatable token.

generate is called once per unique (kind, key) pair even when referenced by several services, so they all resolve to the same value (e.g. an app service's DB_PASSWORD and its sibling postgres service's own POSTGRES_PASSWORD both referencing SERVICE_PASSWORD_DB). persist is then called once per (service, env key) that ended up secret-backed, since secret storage is keyed per real service, not per magic-var key: the caller is expected to write that same value into whichever per-service secret store its later container-create step reads from.

A magic-var token embedded inside a larger string (not the entire env value, e.g. a composite DATABASE_URL) can still splice in a generated value: the whole assembled string is then secret-backed (persisted and added to secretEnv), the same as a whole-value generatable token, since it now contains one. A default-less, non-generatable token in that position is unresolved.

func (UnresolvedVar) String

func (u UnresolvedVar) String() string

type Volume

type Volume struct {
	Name          string
	HostPath      string
	ContainerPath string
	// ReadOnly mounts read-only inside the container, from an optional
	// trailing ":ro" on the short-form entry.
	ReadOnly bool
}

Volume is one short-form "name:/container/path" entry, either a named Docker volume (Name set, HostPath empty) or a bind mount of a real host directory (HostPath set, Name empty): exactly one of the two is ever set, see Volume.UnmarshalYAML (yaml.go) for how the left side of the entry decides which.

func (*Volume) UnmarshalYAML

func (v *Volume) UnmarshalYAML(node *yaml.Node) error

UnmarshalYAML supports both of volumes:'s forms: the short scalar form ("name:/path", optionally with a trailing ":ro"/":rw") and the long mapping form (type/source/target/read_only keys). See parseVolumeShortForm and parseVolumeLongForm.

Jump to

Keyboard shortcuts

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