Documentation
¶
Overview ¶
Package spec parses and validates the app.yaml file: the one declarative file a user writes in their repo, per the app spec design. Validation happens in two layers. Structural shape (required fields, enums, string patterns, unknown-key rejection) is checked against the embedded JSON Schema (schema/app.schema.json), kept as the single source of truth so CI and the deploy-time check share one definition, not two hand-maintained copies that drift. Rules the schema can't express (a field's validity depending on another field's value) are checked by Validate after parsing.
Index ¶
- Constants
- func DiscoverPath(dir string, brandedName string) (string, error)
- func IsPendingImage(image string) bool
- func NormalizeRepoPath(field, p string) (string, error)
- func SchemaJSON() ([]byte, error)
- func ValidateDocument(data []byte) (*Spec, []Issue)
- func ValidateLabels(labels map[string]string) error
- type Backup
- type Build
- type Database
- type Egress
- type EgressAllow
- type EnvVar
- type ExecCommand
- type GPU
- type Health
- type Hooks
- type Issue
- type Job
- type Probe
- type Resources
- type Service
- type Spec
- type StatusCodes
- type VaultRef
- type Volume
Constants ¶
const ( MaxLabels = 32 MaxLabelKeyLength = 255 MaxLabelValueLength = 4096 )
Sanity limits on custom labels: a genuine safety bound against unbounded container metadata growth, not a business threshold, so a fixed constant is appropriate here (the project's "no hardcoded thresholds" rule targets values a deployment might reasonably need to tune, like a retention window or a rate limit; a handful of operator hand-written monitoring/tooling labels per service isn't that). Docker itself doesn't publish an official label count/size limit, so these are chosen generously enough for real use (a monitoring agent or log shipper typically wants a handful of keys) while still bounding how much metadata one service can accumulate.
const ( BuildDockerfile = "dockerfile" BuildCompose = "compose" BuildRailpack = "railpack" BuildStatic = "static" BuildImage = "image" )
Build input types: the app spec's build.type values, matching the BuildKit-based build detection (Dockerfile, Compose, Railpack, static) plus one non-build type: a prebuilt image already sitting in a registry, deployed as-is with no build step at all (see Build.Image and internal/deploy's deployImage).
const ( StrategyRolling = "rolling" StrategyRecreate = "recreate" StrategyBlueGreen = "blue-green" )
Deploy strategies, part of the app spec. Blue-green is the effective default, since it's easier to get right than rolling with a single replica, applied by DefaultStrategy when Strategy is empty, not baked into the schema itself.
const ( EngineFake = "" // zero value only, never valid; see Validate EnginePostgres = "postgres" EngineRedis = "redis" EngineMySQL = "mysql" EngineMongoDB = "mongodb" EngineMariaDB = "mariadb" EngineKeyDB = "keydb" EngineClickHouse = "clickhouse" EngineDragonfly = "dragonfly" )
Supported managed database engines: Postgres and Redis shipped as first-class resources in the initial release, MySQL, MongoDB, MariaDB, KeyDB, ClickHouse, and Dragonfly joined once internal/reconcile/database's controller grew matching engine cases.
const ( // TriggerModePush deploys on every push to the git source's // configured branch. The default, matching pre-trigger-mode // behavior. TriggerModePush = "push" // TriggerModeRelease deploys only on a tag ref push, or (GitHub // only) a "release" webhook event with action "published". TriggerModeRelease = "release" )
Git source deploy trigger modes (store.GitSource.TriggerMode): which pushes to a connected repo actually trigger a deploy.
const ( JobModeFresh = "fresh" JobModeExec = "exec" JobCatchUpSkip = "skip" JobCatchUpRunOnce = "run-once" )
Job modes and catch-up values accepted in app.yaml.
const DefaultReplicas = 1
DefaultReplicas is used when a service doesn't set replicas.
const EgressModeAllowlist = "allowlist"
EgressModeAllowlist is the only meaningful Egress.Mode value today; see Egress's own doc comment for why the block is opt-in rather than defaulting to deny-all.
const GPUAll = -1
GPUAll is GPU.Count's "every GPU on the node" value.
const InstanceLabelKey = ReservedLabelPrefix + "instance"
InstanceLabelKey is the Docker label key every container, network, and volume this control-plane instance creates gets stamped with, set to its own persistent instance ID (store.GetOrCreateInstanceID). Two separate control-plane instances (two installs, or a dev and a prod instance) can end up sharing one Docker daemon; without this, a brand-labeled or brand-namespaced resource alone doesn't say which instance created it, so one instance's stale-resource cleanup could mistake the other's live resource for its own garbage and remove it. Under ReservedLabelPrefix so no operator-supplied label can collide with or spoof it (ValidateLabels below already rejects that prefix).
const PendingImageTag = ":pending"
PendingImageTag is the tag an app is created with when its first image has not been built yet (the CLI's own git-build path, which must send some image for an app POST that requires one). It never names a real registry image, so the application reconciler treats it as "not built yet" rather than something to pull.
const ReservedLabelPrefix = "platform-reserved."
ReservedLabelPrefix is the Docker label key namespace no operator- supplied custom label may use, keeping it open for this platform's own bookkeeping labels (none exist yet: today a service's containers are found purely by name pattern, internal/reconcile/application's ContainerName/ownsContainer, not by label). It's a fixed, brand- independent string on purpose, not derived from brand.ShortName: brand.ShortName is runtime-configurable (APP_BRAND_SHORT_NAME), and a validation boundary keyed to a value that can change between deploys would make a previously-valid app.yaml unpredictably valid or invalid depending on unrelated brand config. It also has to hold for cmd/levelrail-cli, which validates a local app.yaml with no server connection and therefore no brand config to consult at all. This package stays decoupled from internal/brand for the same reason DiscoverPath (discover.go) does.
Variables ¶
This section is empty.
Functions ¶
func DiscoverPath ¶
DiscoverPath finds the app spec file in dir, checking candidate filenames in order and returning the first one that exists. The spec file is discovered by a list of candidate filenames (app.yaml, deploy.yaml, plus the branded one) so a product rename does not break existing users later. The generic names are checked first for exactly that reason: if the product is ever renamed, brandedName changes, but a repo with an app.yaml already committed keeps working without the user touching anything.
brandedName is the caller-supplied branded filename stem (derived from brand.Brand, not imported here, so this package stays decoupled from internal/brand); pass "" to skip it, e.g. in tests.
func IsPendingImage ¶ added in v0.2.0
IsPendingImage reports whether image is a PendingImageTag placeholder.
func NormalizeRepoPath ¶ added in v0.2.0
NormalizeRepoPath cleans a repository-relative path typed by an operator. An empty result means the repository root. It rejects absolute paths, backslashes, NUL bytes and any ".." that would leave the repository.
func SchemaJSON ¶ added in v0.2.0
SchemaJSON returns the embedded app.yaml JSON Schema, for tooling that composes it into larger documents.
func ValidateDocument ¶ added in v0.2.0
ValidateDocument runs the same two layers Parse does (schema, then Validate's semantic rules) but never stops at the first problem: every schema violation comes back at once, located by path and line, for a live editor to annotate while the operator is still typing. The returned Spec is non-nil only when there are no issues. Deploy time still calls Parse directly; this exists only to give that same check friendlier, earlier feedback, not a second source of truth.
func ValidateLabels ¶
ValidateLabels checks labels against the rules above: reserved prefix, empty keys, and the sanity limits. Exported so internal/api's validateAppResource can run the identical check on API-sourced labels (which bypass Parse/Validate entirely, they're never described in app.yaml), the same "define once here, both layers call it" shape StrategyRolling/StrategyRecreate/StrategyBlueGreen already establish for the API layer to reference rather than re-derive.
Types ¶
type Build ¶
type Build struct {
Type string `yaml:"type"`
Path string `yaml:"path,omitempty"`
// BaseDirectory scopes the build context to a subdirectory of the
// repo, e.g. "apps/web" in a monorepo. Empty means the repo root.
// Meaningful for dockerfile, railpack, and static; not meaningful
// for image (nothing gets built) or compose (the compose file's own
// context: field already scopes each service).
BaseDirectory string `yaml:"baseDirectory,omitempty"`
// Image is a full registry reference (e.g.
// "ghcr.io/org/app:v1.2.3"), only meaningful for build.type: image:
// a CI pipeline (or anything else) already built and pushed this
// exact image, so there is nothing for this control plane to build,
// only to deploy as-is. See internal/deploy's deployImage.
Image string `yaml:"image,omitempty"`
// RegistryCredential names a store.RegistryCredential (by its Name,
// not ID: app.yaml is hand-written, an opaque ID would be hostile
// to author) to authenticate with when pulling Image from a private
// registry. Empty means an unauthenticated (public) pull.
RegistryCredential string `yaml:"registryCredential,omitempty"`
// Args are Dockerfile build-time ARG values (e.g. a base image
// version, a build-time feature flag), passed through as
// --build-arg equivalents to BuildKit. Only meaningful for
// build.type: dockerfile.
Args map[string]string `yaml:"args,omitempty"`
}
Build describes how a service's image gets built.
func ResolveGitBuild ¶ added in v0.2.0
ResolveGitBuild turns a git source's persisted build settings into the Build a deploy runs. baseDirectory is the build context (empty is the repository root); buildPath is the Dockerfile (or static output directory) relative to the repository root, never to baseDirectory. The returned Build.Path is relative to the context, which is what the deploy pipeline joins onto it.
type Database ¶
type Database struct {
Engine string `yaml:"engine"`
Version string `yaml:"version,omitempty"`
Backup *Backup `yaml:"backup,omitempty"`
// EphemeralInPreviews opts this database into a disposable,
// preview-scoped instance of its own (a full container, its own
// volume, its own credentials) rather than sharing whatever this
// database resolves to in production: one per pull request, created
// alongside the preview and destroyed with it, with no restore path
// once torn down. Off by default, like every other opt-in toggle in
// this codebase; only meaningful for a database attached to an app
// with preview environments enabled (store.GitSource.PreviewEnabled).
EphemeralInPreviews bool `yaml:"ephemeralInPreviews,omitempty"`
// IsolatedInPreviews opts this database into an isolated credential
// per preview, created on this same existing database (no new
// container, no new volume) rather than the whole disposable
// instance EphemeralInPreviews creates: a Postgres role, or a Redis
// ACL user scoped to its own key prefix. Ignored when
// EphemeralInPreviews is also set, since an ephemeral instance
// already has its own credentials. Postgres and Redis only today.
// Off by default.
IsolatedInPreviews bool `yaml:"isolatedInPreviews,omitempty"`
}
Database is one entry under databases:.
type Egress ¶ added in v0.2.0
type Egress struct {
Mode string `yaml:"mode"`
Allow []EgressAllow `yaml:"allow,omitempty"`
}
Egress is a service's outbound network policy.
type EgressAllow ¶ added in v0.2.0
EgressAllow is one host+port pair a service's outbound traffic may reach when Egress.Mode is allowlist. Host is a bare hostname, resolved (and re-resolved on an interval, DNS records aren't static) by the egress sidecar rather than pinned to an IP at deploy time.
type EnvVar ¶
type EnvVar struct {
// Value is a literal value, set when the YAML node was a plain
// string scalar rather than a mapping.
Value string
// From references another resource's computed value, e.g.
// "postgres.main.url". Mutually exclusive with Value, Secret, and
// Vault.
From string
// Secret means the operator provides this at deploy time via
// the platform's own envelope-encrypted secret storage, never
// written to app.yaml or the git repo. Mutually exclusive with
// Vault: a given env var resolves its value from exactly one of
// the two secret sources, never both.
Secret bool
// Required, only meaningful alongside Secret: fail the deploy if no
// value has been provided, rather than starting the container with
// the variable unset.
Required bool
// Vault means this value is resolved live from an external
// HashiCorp Vault instance at container-create time
// (internal/reconcile/application's resolveEnv), never stored by
// this platform at all: the alternative to Secret above, not a
// variant of it. nil means not vault-backed.
Vault *VaultRef
}
EnvVar is one entry in a service's env block. app.yaml allows two shapes for the same field, per the app spec's own example, which shows both:
DATABASE_URL: { from: postgres.main.url }
API_KEY: { secret: true, required: true }
A plain string scalar is also accepted as shorthand for a literal value, since forcing every simple env var into object form would make the common case more verbose than the shape it's modeled on (Docker Compose's own env shorthand).
func (*EnvVar) UnmarshalYAML ¶
UnmarshalYAML implements the string-or-object union described on EnvVar, using yaml.v3's node-based Unmarshaler interface: the node's Kind tells a scalar apart from a mapping before deciding which shape to decode into, rather than trying one and falling back on error.
type ExecCommand ¶ added in v0.2.0
type ExecCommand []string
ExecCommand is health.*.exec: a list is run as argv, a string through /bin/sh -c.
func (*ExecCommand) UnmarshalJSON ¶ added in v0.2.0
func (e *ExecCommand) UnmarshalJSON(data []byte) error
UnmarshalJSON accepts a shell string or an argv array.
func (*ExecCommand) UnmarshalYAML ¶ added in v0.2.0
func (e *ExecCommand) UnmarshalYAML(node *yaml.Node) error
UnmarshalYAML accepts a shell string or an argv list.
type GPU ¶ added in v0.2.0
type GPU struct {
Count int `yaml:"count,omitempty" json:"count,omitempty"`
Devices []string `yaml:"devices,omitempty" json:"devices,omitempty"`
}
GPU is resources.gpu: `all`, a count, or {count, devices} where devices are NVIDIA indexes or UUIDs (devices wins over count).
func (*GPU) UnmarshalJSON ¶ added in v0.2.0
UnmarshalJSON accepts "all", a number, or an object.
type Health ¶
type Health struct {
Readiness *Probe `yaml:"readiness,omitempty"`
Liveness *Probe `yaml:"liveness,omitempty"`
// ReadyTimeout overrides how long a fresh deploy waits for Readiness
// to pass before the deploy is marked ReadinessFailed (internal/
// reconcile/application.defaultReadyBudget, 60s, when unset). A slow
// cold start (a JVM app, a big migration, a slow external connection)
// can legitimately take longer than 60s without ever being unhealthy.
ReadyTimeout string `yaml:"readyTimeout,omitempty"`
}
Health holds a service's readiness and liveness probe configuration.
type Hooks ¶
type Hooks struct {
PreDeploy string `yaml:"preDeploy,omitempty"`
PostDeploy string `yaml:"postDeploy,omitempty"`
}
Hooks are the two deploy-lifecycle commands a service can declare. Both run via "sh -c" inside the newly created container (see internal/reconcile/application.Controller's own doc comment for the full timing and failure-handling contract): PreDeploy before the container's readiness probe and before any old container is retired, PostDeploy after the whole replica set has cut over. Either field may be set alone.
type Issue ¶ added in v0.2.0
type Issue struct {
Path string `json:"path"`
Line int `json:"line"`
Message string `json:"message"`
}
Issue is one problem found in an app.yaml document, located by dotted path and source line when known. Mirrors internal/pipeline.Issue's shape, the live-validation convention this codebase already follows for its own declarative YAML format.
type Job ¶ added in v0.2.0
type Job struct {
Name string `yaml:"name"`
Schedule string `yaml:"schedule"`
Command []string `yaml:"command"`
Timezone string `yaml:"timezone,omitempty"`
// Mode is "fresh" (default, a new container from the service's image
// and spec) or "exec" (inside the running container).
Mode string `yaml:"mode,omitempty"`
// Concurrency is "allow", "forbid" (default) or "replace".
Concurrency string `yaml:"concurrency,omitempty"`
Timeout string `yaml:"timeout,omitempty"`
Retries int `yaml:"retries,omitempty"`
RetryBackoff string `yaml:"retryBackoff,omitempty"`
CatchUp string `yaml:"catchUp,omitempty"`
CatchUpLookback string `yaml:"catchUpLookback,omitempty"`
CPU float64 `yaml:"cpu,omitempty"`
Memory string `yaml:"memory,omitempty"`
WarnAfter string `yaml:"warnAfter,omitempty"`
// Enabled defaults to true when omitted.
Enabled *bool `yaml:"enabled,omitempty"`
}
Job is one entry under a service's jobs:. Durations use Go syntax ("30s", "10m", "1h"); Memory the same "512Mi"/"1Gi" shape as resources.
type Probe ¶
type Probe struct {
Path string `yaml:"path,omitempty"`
Scheme string `yaml:"scheme,omitempty" json:"scheme,omitempty"`
Host string `yaml:"host,omitempty" json:"host,omitempty"`
TLSSkipVerify bool `yaml:"tls_skip_verify,omitempty" json:"tls_skip_verify,omitempty"`
FollowRedirects *bool `yaml:"follow_redirects,omitempty" json:"follow_redirects,omitempty"`
ExpectedStatus StatusCodes `yaml:"expected_status,omitempty" json:"expected_status,omitempty"`
Exec ExecCommand `yaml:"exec,omitempty" json:"exec,omitempty"`
Interval string `yaml:"interval,omitempty"`
Timeout string `yaml:"timeout,omitempty"`
Failures int `yaml:"failures,omitempty"`
}
Probe is a single health check: an HTTP(S) request to Path, or a command run inside the container when Exec is set.
type Resources ¶
type Resources struct {
Memory string `yaml:"memory,omitempty"`
CPU float64 `yaml:"cpu,omitempty"`
// SwapMemory caps memory plus swap combined, the same "512Mi"/"1Gi"
// shape as Memory (Docker's own MemorySwap semantics); only
// meaningful alongside Memory, see Validate.
SwapMemory string `yaml:"swapMemory,omitempty"`
// CPUSet pins the container to specific host CPUs, Docker's own
// cpuset-cpus format (e.g. "0-3" or "0,2").
CPUSet string `yaml:"cpuSet,omitempty"`
// GPU requests NVIDIA GPUs; the service is placed only on a node
// with a usable GPU. nil means no GPU.
GPU *GPU `yaml:"gpu,omitempty"`
}
Resources holds a service's resource limits.
type Service ¶
type Service struct {
Build Build `yaml:"build"`
// Image is shorthand for build: {type: image, image: ...}; Parse folds it
// into Build, so consumers only ever read Build.
Image string `yaml:"image,omitempty"`
Domains []string `yaml:"domains,omitempty"`
Port int `yaml:"port,omitempty"`
// HostPort pins the host-side port Docker binds Port to, 0 meaning
// auto-assign (the same zero-value-means-unset convention Port
// itself uses). Its storage home is store.DesiredService.HostPort
// (migrations/0056_service_host_port.sql).
HostPort int `yaml:"host_port,omitempty"`
// BindAddress picks which network interface Port (and HostPort, if
// pinned) binds to on the host: "private" (loopback only, the
// default when empty), "public" (every interface, an explicit opt-
// in), or a literal IP. See internal/bindaddr.Resolve for the exact
// rules; EffectiveBindAddress applies the default.
BindAddress string `yaml:"bind_address,omitempty"`
Health *Health `yaml:"health,omitempty"`
Resources *Resources `yaml:"resources,omitempty"`
Env map[string]EnvVar `yaml:"env,omitempty"`
Replicas int `yaml:"replicas,omitempty"`
Strategy string `yaml:"strategy,omitempty"`
// Labels are arbitrary operator-supplied Docker labels applied to the
// service's container at create time, an escape hatch for tooling
// this platform doesn't know about (a monitoring agent or log
// shipper that keys off container labels, a homegrown script, and
// so on). See ValidateLabels (labels.go) for what's rejected:
// notably, any key under ReservedLabelPrefix, kept open for this
// platform's own bookkeeping labels.
Labels map[string]string `yaml:"labels,omitempty"`
// Volumes are named Docker volumes or host-directory bind mounts this
// service's container mounts (see Volume's own doc comment for how
// the two are distinguished): named volumes were previously a
// database-only capability, bind mounts a compose-import-only one. A
// named volume's Name is a logical name scoped to this service, not a
// global Docker volume name (see internal/deploy's translation into
// store.ServiceVolume for the actual, platform-prefixed name); two
// services can each declare a volume named "data" without colliding.
Volumes []Volume `yaml:"volumes,omitempty"`
// Hooks are shell commands the reconciler runs inside this service's
// own container at defined points in a deploy (internal/reconcile/
// application's controller). Nil means neither is configured, the
// same "declarative, resolved before storing" shape Health/Resources
// already follow.
Hooks *Hooks `yaml:"hooks,omitempty"`
// Command overrides the image's own default CMD
// (store.DesiredService.Command), nil meaning the image's own
// default: the app.yaml equivalent of
// internal/compose.Service.Command, which this mirrors. A plain argv
// list, never shell-interpreted.
Command []string `yaml:"command,omitempty"`
// Egress restricts this service's outbound network traffic to an
// explicit host+port allowlist, enforced by
// internal/reconcile/application's egress sidecar. Nil (the default,
// and the only state every service had before this field existed)
// means today's open-egress behavior, unchanged: this is deliberately
// opt-in, not deny-by-default, since Levelrail runs arbitrary
// third-party apps for existing users rather than AX's own
// tightly-scoped agent sandboxes, and defaulting every existing app
// to deny-all egress on upgrade would be a breaking change.
Egress *Egress `yaml:"egress,omitempty"`
// LoadBalancer configures balancing across this service's replicas
// (internal/loadbalancer). Nil keeps the plain single-upstream route.
LoadBalancer *loadbalancer.Config `yaml:"loadbalancer,omitempty"`
// DependsOn names sibling services: keys (within this same Spec, or
// within the same compose file for a compose-expanded service, see
// internal/compose.ExpandBuildService) this service waits on before
// the reconciler starts its own containers: internal/reconcile/
// application.Controller does not create a container for this
// service until every named dependency has at least one running
// container. This matches real Docker Compose's own default
// depends_on: semantic (service_started, not service_healthy): a
// start-order guarantee, not a health-based wait. See Validate for
// the reference and cycle checks.
DependsOn []string `yaml:"dependsOn,omitempty"`
// Jobs are cron-scheduled commands run for this service, each in a
// fresh container of its image (or exec'd into the running one), kept
// in step with this list on every deploy. Nil leaves jobs created
// through the dashboard or CLI alone; an explicit empty list removes
// every spec-declared job.
Jobs []Job `yaml:"jobs,omitempty"`
}
Service is one entry under services:.
func (*Service) EffectiveBindAddress ¶ added in v0.2.0
EffectiveBindAddress returns svc.BindAddress, or bindaddr.Default (private, loopback-only) if unset. Mirrors EffectiveStrategy/ EffectiveReplicas' own "resolve the default here, once" shape.
func (*Service) EffectiveReplicas ¶
EffectiveReplicas returns svc.Replicas, or DefaultReplicas if unset. Schema validation guarantees Replicas is never negative when set; zero means "not specified in app.yaml", not "zero replicas".
func (*Service) EffectiveStrategy ¶
EffectiveStrategy returns svc.Strategy, or the default (blue-green, since it's easier to get right than rolling with a single replica) if unset.
type Spec ¶
type Spec struct {
Version int `yaml:"version"`
Services map[string]Service `yaml:"services"`
Databases map[string]Database `yaml:"databases,omitempty"`
}
Spec is a fully parsed, schema-valid app.yaml.
func Parse ¶
Parse validates raw app.yaml bytes against the embedded JSON Schema, then unmarshals into a Spec and runs the semantic checks the schema can't express. Both layers must pass for Parse to succeed; a caller never sees a Spec that's schema-valid but semantically broken, or vice versa.
func (*Spec) LoadBalancerFor ¶ added in v0.2.0
func (s *Spec) LoadBalancerFor(service string) (*loadbalancer.Config, error)
LoadBalancerFor returns a service's loadbalancer: block, or nil when the service has none.
func (*Spec) Validate ¶
Validate checks rules the JSON Schema can't express: values whose validity depends on another field, or on the rest of the document, not just their own shape. Parse always runs this after schema validation succeeds, callers building a Spec by hand (tests, future tooling) should call it too before trusting a Spec.
type StatusCodes ¶ added in v0.2.0
type StatusCodes string
StatusCodes is health.*.expected_status: "200-399", 204, or a list mixing codes and ranges. It is kept in its normalized string form.
func (*StatusCodes) UnmarshalJSON ¶ added in v0.2.0
func (s *StatusCodes) UnmarshalJSON(data []byte) error
UnmarshalJSON accepts a string, a number, or an array of either.
func (*StatusCodes) UnmarshalYAML ¶ added in v0.2.0
func (s *StatusCodes) UnmarshalYAML(node *yaml.Node) error
UnmarshalYAML accepts a scalar or a sequence of scalars.
type VaultRef ¶ added in v0.2.0
VaultRef is app.yaml's { vault: { path, key } } env var shape: Path is the Vault KV secret's path, Key is the field name within that secret's data. Both required together, mirroring internal/store's own VaultEnvRef which this parses into (see internal/deploy's vaultEnvRefs).
type Volume ¶
type Volume struct {
Name string `yaml:"name,omitempty"`
HostPath string `yaml:"hostPath,omitempty"`
Path string `yaml:"path"`
ReadOnly bool `yaml:"readOnly,omitempty"`
}
Volume is one entry under a service's volumes:. Exactly one of Name (a named Docker volume) or HostPath (a bind mount of a real host directory, gated the same as internal/compose's own bind-mount support: see validateBindMountHostPath) is set; see Validate. ReadOnly is only meaningful alongside HostPath, matching store.ServiceBindMount.