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:, 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 ¶
- Constants
- 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 ¶
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 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: 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.
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.
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). Docker never runs it: resolveHealthcheck (healthcheck.go) translates it into this platform's own readiness probe.
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.
func (*Port) UnmarshalYAML ¶
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:, 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
// 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
}
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 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.