1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170
|
# Compose file parsing
This document describes the logic parsing and merging compose file
and overrides.
## Phase 1: parse yaml document
Yaml document is parsed using de-facto standard [go-yaml](https://github.com/go-yaml/yaml)
library. This one manages anchors and aliases, which are only supported within
a yaml document (an override can't refer to another compose file anchor)
## Phase 2: key conversion
Yaml allows mapping keys to be any type, but compose only uses strings for simplicity.
A conversion is applied on the yaml tree for all mapping keys to become strings,
even the parser could have parsed those are numbers or booleans
```yaml
services:
true:
...
```
is converted to :
```yaml
services:
"true":
...
```
# Phase 3: interpolation
Compose supports a bash-style syntax to allow variables to be set on yaml values.
Interpolation is responsible to resolve those into actual values based on variables
defined as "environment" during the parsing.
```yaml
services:
foo:
image: "foo:${TAG}"
```
is converted to :
```yaml
services:
foo:
image: "foo:1.2.3"
```
Interpolation takes place early and on a per-document basis, so that the yaml
tree can be validated by json schema. If interpolation isn't applied validation
could fail as Compose specification JSON schema require some nodes to be a boolean
or number
# Phase 4: empty nodes
JSON doesn't consider empty and null to be equivalent, so does the JSON-schema.
But go-yaml parser make them both a `nil` value, so we need to patch the yaml tree
accordingly.
# Phase 5: validation
Resulting yaml tree is validated against the Compose specification JSON schema
# Phase 6: extends
A service can be defined based on another one, with the ability to override some
attributes for local usage. This is the role of the `extends` attribute.
Extended service yaml definition is cloned into a plain new yaml subtree then
the local service definition is merged as an override. This includes support
for `!reset` to remove an element from original service definition.
# Phase 7: include resources from another compose model
A compose file can use `include` to rely on compose resources defined by third-parties
as a separate compose file
Included compose definition is fully parsed (as described in this document) then included
to the compose yaml model being processed. Conflicting resources are detected and rejected
The resulting compose model is equivalent to a copy/paste of the included compose model
(fully resolved) into the local compose file.
# Phase 8: merge overrides
If loaded document is an override, the yaml tree is merged with the one from
main compose file. `!reset` can be used to remove elements.
The merge logic generally is "_append to lists, replace in mapping_" with a
few exceptions:
- shell commands always are replaced by an override
- `options` is only merged if both file declare the same `driver`, otherwise
the override fully replaces the original.
- Attributes which can be expressed both as a mapping and a sequence are converted
so that merge can apply on equivalent data structures.
# Phase 9: enforce unicity
While modeled as a list, some attributes actually require some unicity to be
applied. Volume mount definition for a service typically must be unique
regarding the target mount path. As such attribute can be defined as a single
string and set by a variable, we have to apply the "_append to list_" merge
strategy then check for unicity.
# Phase 10: validation
During the loading process, some logical rules are checked. But some involved
relations between exclusive attributes, and must be checked as a dedicated phase.
A typical example is the use of `external` in a resource definition. As such a
resource is not managed by Compose, having some resource creation attributes set
must result into an error being reported to the user
```yaml
networks:
foo:
external: true
driver: macvlan # This will trigger an error, as external network should not have any resource creation parameter set
```
# Phase 11: transform into canonical representation
Compose specification allows many attribute to have both a "short" and a "long"
syntax. It also supports use of single string or list of strings for some
repeatable attributes. Some attributes can be declared both as a list of
key=value strings or as a yaml mapping.
During loading, all those attributes are transformed into canonical
representation, so that we get a single format that will match to go structs
for binding.
# Phase 12: set-defaults
Some attributes are required by the model but optional in the compose file, as an implicit
default value is defined by the specification, like [`build.context`](https://github.com/compose-spec/compose-spec/blob/master/build.md#context)
During this phase, such unset attributes get default value assigned.
# Phase 13: extensions
Extension (`x-*` attributes) can be used in any place in the yaml document.
To make unmarshalling easier, parsing move them all into a custom `#extension`
attribute. This hack is very specific to the go binding.
# Phase 14: relative paths
Compose allows paths to be set relative to the project directory. Those get resolved
into absolute paths during this phase. This involves a few corner cases, as
- path might denote a local file, or a remote (docker host) path, with windows vs unix
filesystem syntax conflicts
- some attributes are not modeled by the Compose specification and still are paths, like
bind mount options set to the local volume driver
```yaml
volumes:
data:
driver: local
driver_opts:
type: 'none'
o: 'bind'
device: './data' # such a relative path must be resolved
```
# Phase 15: go binding
Eventually, the yaml tree can be unmarshalled into go structs. We rely on
[mapstructure](https://github.com/go-viper/mapstructure/) library for this purpose.
Decoder is configured so that custom decode function can be defined by target type,
allowing type conversions. For example, byte units (`640k`) and durations set in yaml
as plain string are actually modeled in go types as `int64`.
|