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 support only their short-form syntax. restart:, networks:, and depends_on: all parse and are surfaced as non-blocking Notices instead of being silently dropped or translated: see Notices for why none of the three has a real translation onto how Levelrail runs a service (depends_on: in particular is never used to sequence container startup order, that's reconciler-level work out of scope here). 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 ¶
- func ExpandBuildService(svc spec.Service, sourceDir string) (services map[string]spec.Service, warnings []string, err error)
- func GenerateValue(kind string, length int) (string, error)
- func ToDesiredServices(appName string, f *File) (services []store.DesiredService, warnings []string, err error)
- type Command
- type DependsOn
- type Environment
- type File
- type Healthcheck
- type MagicVar
- type Networks
- type Notice
- type NoticeLevel
- type Port
- type Service
- type UnresolvedVar
- type Volume
Constants ¶
This section is empty.
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 reads and parses the compose file svc.Build.Path points at (resolved relative to sourceDir, the same git checkout root every other build.type already resolves its own paths against), and returns one spec.Service per compose service it declares: a build:-bearing compose service becomes an ordinary build.type: dockerfile entry, an image:-only one becomes build.type: image. The caller (Pipeline.DeploySpec) is expected to splice these entries into its own Services map in svc's place and fan out exactly as it already does for any other declared service, no reconciler or build-pipeline change needed: every compose-declared service converges through the exact same one-container-per-service path every other service does.
svc.Build.Type must be spec.BuildCompose; anything else is a caller bug, not a user-facing error.
warnings carries one message per expanded service whose healthcheck: is a real, non-HTTP check (see resolveHealthcheck): the caller is expected to surface these to the operator, since that service's health is deliberately left unset rather than guessed.
func GenerateValue ¶
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: is a real, non-HTTP check (see resolveHealthcheck): the caller is expected to surface these to the operator (log line, response field, ...) since that service's health is deliberately left unset rather than guessed.
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 ¶
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.
type DependsOn ¶
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.
type Environment ¶
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
}
File is a parsed compose.yaml.
func (*File) Notices ¶
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: (NoticeLevelWarning): parsed but never used to sequence container startup order, so a service can start before what it depends on is ready. A real semantic gap, worth a warning so an operator whose service crashloops on startup isn't left guessing why.
func (*File) Validate ¶
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 ¶
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). Never executed: this platform's own health model is HTTP-path based, so resolveHealthcheck (healthcheck.go) extracts a readiness path from a curl/wget test when it can, and leaves health unset otherwise.
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 ¶
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.
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 ¶
Port is one short-form ports: entry. ContainerPort is what store.DesiredService.Port (a single container port, not a host:container pair) actually uses.
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:, never used to sequence container startup
// order (see Notices); kept only so Notices can tell it was declared.
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
}
Service is one entry under services:.
type UnresolvedVar ¶
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 ¶
UnmarshalYAML supports volumes:'s short form only: "name:/path", optionally with a trailing ":ro"/":rw". The left side is a bind mount (HostPath set, Name left empty) when it starts with "/", a real Docker Compose absolute host path; one starting with "." is rejected outright, since there's no defined working directory here to resolve a relative path against (real Compose resolves it against the compose file's own directory, which this package's direct-import path, unlike the git-sourced expand path, doesn't have). Anything else is a named volume, unchanged from before bind mounts existed.