├── .gitignore ├── .pre-commit-config.yaml ├── .travis.yml ├── LICENSE ├── Makefile ├── dobi.yaml ├── dockerfiles └── Dockerfile.build ├── glide.lock ├── glide.yaml ├── interpolation ├── interpolation.go └── interpolation_test.go ├── loader ├── example1.env ├── example2.env ├── full-example.yml ├── loader.go └── loader_test.go ├── schema ├── bindata.go ├── data │ └── config_schema_v3.0.json ├── schema.go └── schema_test.go ├── script └── watch ├── template ├── template.go └── template_test.go └── types └── types.go /.gitignore: -------------------------------------------------------------------------------- 1 | .dobi 2 | .glide 3 | vendor 4 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | - repo: git://github.com/pre-commit/pre-commit-hooks 2 | sha: 'v0.5.1' 3 | hooks: 4 | - id: check-merge-conflict 5 | - id: end-of-file-fixer 6 | files: '(\.(go|md|sh|yml|yaml|json|ini|rst)|Dockerfile.*)$' 7 | exclude: '^vendor/' 8 | - id: trailing-whitespace 9 | files: '(\.(go|md|sh|yml|yaml|json|ini|rst)|Dockerfile.*)$' 10 | args: ['--no-markdown-linebreak-ext'] 11 | exclude: '^vendor/' 12 | - id: check-yaml 13 | exclude: '^vendor/' 14 | - id: check-json 15 | exclude: '^vendor/' 16 | 17 | - repo: git://github.com/dnephin/pre-commit-golang 18 | sha: HEAD 19 | hooks: 20 | - id: go-fmt 21 | exclude: '^vendor/' 22 | args: ['-s'] 23 | - id: go-lint 24 | exclude: '^(vendor/|schema/bindata\.go)' 25 | - id: go-vet 26 | exclude: '^vendor/' 27 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | 3 | services: 4 | - docker 5 | 6 | branches: 7 | only: 8 | - master 9 | 10 | before_install: 11 | - curl -L https://github.com/dnephin/dobi/releases/download/v0.8/dobi-linux > ./dobi 12 | - chmod +x ./dobi 13 | - ./dobi deps 14 | 15 | script: 16 | - ./dobi test 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | https://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | Copyright 2016 Docker, Inc. 180 | 181 | Licensed under the Apache License, Version 2.0 (the "License"); 182 | you may not use this file except in compliance with the License. 183 | You may obtain a copy of the License at 184 | 185 | https://www.apache.org/licenses/LICENSE-2.0 186 | 187 | Unless required by applicable law or agreed to in writing, software 188 | distributed under the License is distributed on an "AS IS" BASIS, 189 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 190 | See the License for the specific language governing permissions and 191 | limitations under the License. 192 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SCHEMA_GO := schema/bindata.go 2 | SCHEMA_JSON := schema/data/config_schema_v3.0.json 3 | 4 | test: 5 | go test ./{loader,schema,template,interpolation} 6 | 7 | schema: $(SCHEMA_GO) 8 | 9 | $(SCHEMA_GO): $(SCHEMA_JSON) 10 | go generate ./schema 11 | -------------------------------------------------------------------------------- /dobi.yaml: -------------------------------------------------------------------------------- 1 | 2 | meta: 3 | project: compose-file 4 | 5 | 6 | mount=source: 7 | bind: . 8 | path: /go/src/github.com/aanand/compose-file 9 | 10 | 11 | image=builder: 12 | image: compose-file-dev 13 | context: dockerfiles/ 14 | dockerfile: Dockerfile.build 15 | 16 | 17 | job=shell: 18 | use: builder 19 | interactive: true 20 | mounts: [source] 21 | command: bash 22 | 23 | job=test-unit: 24 | use: builder 25 | mounts: [source] 26 | command: "bash -c 'go test -v $(glide novendor)'" 27 | 28 | job=deps: 29 | use: builder 30 | mounts: [source] 31 | command: "glide install" 32 | artifact: vendor/ 33 | 34 | job=generate: 35 | use: builder 36 | mounts: [source] 37 | command: "go generate ./schema" 38 | artifact: schema/bindata.go 39 | 40 | job=watch: 41 | use: builder 42 | mounts: [source] 43 | command: "script/watch" 44 | interactive: true 45 | 46 | 47 | alias=test: 48 | tasks: [test-unit] 49 | -------------------------------------------------------------------------------- /dockerfiles/Dockerfile.build: -------------------------------------------------------------------------------- 1 | FROM golang:1.6-alpine 2 | 3 | RUN apk add -U git bash curl tree 4 | RUN export GLIDE=v0.12.0; \ 5 | export SRC=https://github.com/Masterminds/glide/releases/download/; \ 6 | curl -sL ${SRC}/${GLIDE}/glide-${GLIDE}-linux-amd64.tar.gz | \ 7 | tar -xz linux-amd64/glide && \ 8 | mv linux-amd64/glide /usr/bin/glide && \ 9 | chmod +x /usr/bin/glide 10 | 11 | RUN go get github.com/dnephin/filewatcher && \ 12 | cp /go/bin/filewatcher /usr/bin/ && \ 13 | rm -rf /go/src/* /go/pkg/* /go/bin/* 14 | 15 | RUN go get github.com/jteeuwen/go-bindata/... && \ 16 | cp /go/bin/go-bindata /usr/bin/ && \ 17 | rm -rf /go/src/* /go/pkg/* /go/bin/* 18 | 19 | WORKDIR /go/src/github.com/aanand/compose-file 20 | ENV PS1="# " 21 | ENV CGO_ENABLED=0 22 | -------------------------------------------------------------------------------- /glide.lock: -------------------------------------------------------------------------------- 1 | hash: d1342a159901519de64c9e27f95ccac882ab8893d2c0320581da07e40f8729a5 2 | updated: 2016-10-28T17:35:38.600076399Z 3 | imports: 4 | - name: github.com/docker/docker 5 | version: 6b644ecc1977c479212676757bff05664fcb655a 6 | subpackages: 7 | - opts 8 | - pkg/mount 9 | - pkg/signal 10 | - runconfig/opts 11 | - name: github.com/docker/engine-api 12 | version: 4290f40c056686fcaa5c9caf02eac1dde9315adf 13 | subpackages: 14 | - types 15 | - types/blkiodev 16 | - types/container 17 | - types/filters 18 | - types/mount 19 | - types/network 20 | - types/registry 21 | - types/strslice 22 | - types/swarm 23 | - types/versions 24 | - name: github.com/docker/go-connections 25 | version: 1494b6df4050e60923d68cd8cc6a19e7af9f1c01 26 | subpackages: 27 | - nat 28 | - name: github.com/docker/go-units 29 | version: eb879ae3e2b84e2a142af415b679ddeda47ec71c 30 | - name: github.com/mattn/go-shellwords 31 | version: 525bedee691b5a8df547cb5cf9f86b7fb1883e24 32 | - name: github.com/mitchellh/mapstructure 33 | version: f3009df150dadf309fdee4a54ed65c124afad715 34 | - name: github.com/Sirupsen/logrus 35 | version: 3ec0642a7fb6488f65b06f9040adc67e3990296a 36 | - name: github.com/spf13/pflag 37 | version: 5ccb023bc27df288a957c5e994cd44fd19619465 38 | - name: github.com/stretchr/testify 39 | version: c5d7a69bf8a2c9c374798160849c071093e41dd1 40 | subpackages: 41 | - assert 42 | - name: github.com/xeipuuv/gojsonpointer 43 | version: e0fe6f68307607d540ed8eac07a342c33fa1b54a 44 | - name: github.com/xeipuuv/gojsonreference 45 | version: e02fc20de94c78484cd5ffb007f8af96be030a45 46 | - name: github.com/xeipuuv/gojsonschema 47 | version: 93e72a773fade158921402d6a24c819b48aba29d 48 | - name: golang.org/x/sys 49 | version: c200b10b5d5e122be351b67af224adc6128af5bf 50 | subpackages: 51 | - unix 52 | - name: gopkg.in/yaml.v2 53 | version: a83829b6f1293c91addabc89d0571c246397bbf4 54 | testImports: 55 | - name: github.com/davecgh/go-spew 56 | version: 6cf5744a041a0022271cefed95ba843f6d87fd51 57 | subpackages: 58 | - spew 59 | - name: github.com/pmezard/go-difflib 60 | version: 792786c7400a136282c1664665ae0a8db921c6c2 61 | subpackages: 62 | - difflib 63 | -------------------------------------------------------------------------------- /glide.yaml: -------------------------------------------------------------------------------- 1 | package: . 2 | import: 3 | - package: github.com/mattn/go-shellwords 4 | - package: github.com/stretchr/testify 5 | - package: github.com/xeipuuv/gojsonreference 6 | - package: github.com/xeipuuv/gojsonschema 7 | - package: gopkg.in/yaml.v2 8 | - package: github.com/docker/go-units 9 | - package: github.com/mitchellh/mapstructure 10 | - package: github.com/docker/docker 11 | version: ~1.12.3 12 | subpackages: 13 | - runconfig/opts 14 | -------------------------------------------------------------------------------- /interpolation/interpolation.go: -------------------------------------------------------------------------------- 1 | package interpolation 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/aanand/compose-file/template" 7 | "github.com/aanand/compose-file/types" 8 | ) 9 | 10 | func Interpolate(config types.Dict, section string, mapping template.Mapping) (types.Dict, error) { 11 | out := types.Dict{} 12 | 13 | for name, item := range config { 14 | if item == nil { 15 | out[name] = nil 16 | continue 17 | } 18 | interpolatedItem, err := interpolateSectionItem(name, item.(types.Dict), section, mapping) 19 | if err != nil { 20 | return nil, err 21 | } 22 | out[name] = interpolatedItem 23 | } 24 | 25 | return out, nil 26 | } 27 | 28 | func interpolateSectionItem( 29 | name string, 30 | item types.Dict, 31 | section string, 32 | mapping template.Mapping, 33 | ) (types.Dict, error) { 34 | 35 | out := types.Dict{} 36 | 37 | for key, value := range item { 38 | interpolatedValue, err := recursiveInterpolate(value, mapping) 39 | if err != nil { 40 | return nil, fmt.Errorf( 41 | "Invalid interpolation format for %#v option in %s %#v: %#v", 42 | key, section, name, err.Template, 43 | ) 44 | } 45 | out[key] = interpolatedValue 46 | } 47 | 48 | return out, nil 49 | 50 | } 51 | 52 | func recursiveInterpolate( 53 | value interface{}, 54 | mapping template.Mapping, 55 | ) (interface{}, *template.InvalidTemplateError) { 56 | 57 | switch value := value.(type) { 58 | 59 | case string: 60 | return template.Substitute(value, mapping) 61 | 62 | case types.Dict: 63 | out := types.Dict{} 64 | for key, elem := range value { 65 | interpolatedElem, err := recursiveInterpolate(elem, mapping) 66 | if err != nil { 67 | return nil, err 68 | } 69 | out[key] = interpolatedElem 70 | } 71 | return out, nil 72 | 73 | case []interface{}: 74 | out := make([]interface{}, len(value)) 75 | for i, elem := range value { 76 | interpolatedElem, err := recursiveInterpolate(elem, mapping) 77 | if err != nil { 78 | return nil, err 79 | } 80 | out[i] = interpolatedElem 81 | } 82 | return out, nil 83 | 84 | default: 85 | return value, nil 86 | 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /interpolation/interpolation_test.go: -------------------------------------------------------------------------------- 1 | package interpolation 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | 8 | "github.com/aanand/compose-file/types" 9 | ) 10 | 11 | var defaults = map[string]string{ 12 | "USER": "jenny", 13 | "FOO": "bar", 14 | } 15 | 16 | func defaultMapping(name string) (string, bool) { 17 | val, ok := defaults[name] 18 | return val, ok 19 | } 20 | 21 | func TestInterpolate(t *testing.T) { 22 | services := types.Dict{ 23 | "servicea": types.Dict{ 24 | "image": "example:${USER}", 25 | "volumes": []interface{}{"$FOO:/target"}, 26 | "logging": types.Dict{ 27 | "driver": "${FOO}", 28 | "options": types.Dict{ 29 | "user": "$USER", 30 | }, 31 | }, 32 | }, 33 | } 34 | expected := types.Dict{ 35 | "servicea": types.Dict{ 36 | "image": "example:jenny", 37 | "volumes": []interface{}{"bar:/target"}, 38 | "logging": types.Dict{ 39 | "driver": "bar", 40 | "options": types.Dict{ 41 | "user": "jenny", 42 | }, 43 | }, 44 | }, 45 | } 46 | result, err := Interpolate(services, "service", defaultMapping) 47 | assert.NoError(t, err) 48 | assert.Equal(t, expected, result) 49 | } 50 | 51 | func TestInvalidInterpolation(t *testing.T) { 52 | services := types.Dict{ 53 | "servicea": types.Dict{ 54 | "image": "${", 55 | }, 56 | } 57 | _, err := Interpolate(services, "service", defaultMapping) 58 | assert.EqualError(t, err, `Invalid interpolation format for "image" option in service "servicea": "${"`) 59 | } 60 | -------------------------------------------------------------------------------- /loader/example1.env: -------------------------------------------------------------------------------- 1 | # passed through 2 | FOO=1 3 | 4 | # overridden in example2.env 5 | BAR=1 6 | 7 | # overridden in full-example.yml 8 | BAZ=1 9 | -------------------------------------------------------------------------------- /loader/example2.env: -------------------------------------------------------------------------------- 1 | BAR=2 2 | -------------------------------------------------------------------------------- /loader/full-example.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | foo: 5 | cap_add: 6 | - ALL 7 | 8 | cap_drop: 9 | - NET_ADMIN 10 | - SYS_ADMIN 11 | 12 | cgroup_parent: m-executor-abcd 13 | 14 | # String or list 15 | command: bundle exec thin -p 3000 16 | # command: ["bundle", "exec", "thin", "-p", "3000"] 17 | 18 | container_name: my-web-container 19 | 20 | depends_on: 21 | - db 22 | - redis 23 | 24 | deploy: 25 | mode: replicated 26 | replicas: 6 27 | labels: [FOO=BAR] 28 | update_config: 29 | parallelism: 3 30 | delay: 10s 31 | failure_action: continue 32 | monitor: 60s 33 | max_failure_ratio: 0.3 34 | resources: 35 | limits: 36 | cpus: '0.001' 37 | memory: 50M 38 | reservations: 39 | cpus: '0.0001' 40 | memory: 20M 41 | restart_policy: 42 | condition: on_failure 43 | delay: 5s 44 | max_attempts: 3 45 | window: 120s 46 | placement: 47 | constraints: [node=foo] 48 | 49 | devices: 50 | - "/dev/ttyUSB0:/dev/ttyUSB0" 51 | 52 | # String or list 53 | # dns: 8.8.8.8 54 | dns: 55 | - 8.8.8.8 56 | - 9.9.9.9 57 | 58 | # String or list 59 | # dns_search: example.com 60 | dns_search: 61 | - dc1.example.com 62 | - dc2.example.com 63 | 64 | domainname: foo.com 65 | 66 | # String or list 67 | # entrypoint: /code/entrypoint.sh -p 3000 68 | entrypoint: ["/code/entrypoint.sh", "-p", "3000"] 69 | 70 | # String or list 71 | # env_file: .env 72 | env_file: 73 | - ./example1.env 74 | - ./example2.env 75 | 76 | # Mapping or list 77 | # Mapping values can be strings, numbers or null 78 | # Booleans are not allowed - must be quoted 79 | environment: 80 | RACK_ENV: development 81 | SHOW: 'true' 82 | SESSION_SECRET: 83 | BAZ: 3 84 | # environment: 85 | # - RACK_ENV=development 86 | # - SHOW=true 87 | # - SESSION_SECRET 88 | 89 | # Items can be strings or numbers 90 | expose: 91 | - "3000" 92 | - 8000 93 | 94 | external_links: 95 | - redis_1 96 | - project_db_1:mysql 97 | - project_db_1:postgresql 98 | 99 | # Mapping or list 100 | # Mapping values must be strings 101 | # extra_hosts: 102 | # somehost: "162.242.195.82" 103 | # otherhost: "50.31.209.229" 104 | extra_hosts: 105 | - "somehost:162.242.195.82" 106 | - "otherhost:50.31.209.229" 107 | 108 | hostname: foo 109 | 110 | healthcheck: 111 | test: echo "hello world" 112 | interval: 10s 113 | timeout: 1s 114 | retries: 5 115 | 116 | # Any valid image reference - repo, tag, id, sha 117 | image: redis 118 | # image: ubuntu:14.04 119 | # image: tutum/influxdb 120 | # image: example-registry.com:4000/postgresql 121 | # image: a4bc65fd 122 | # image: busybox@sha256:38a203e1986cf79639cfb9b2e1d6e773de84002feea2d4eb006b52004ee8502d 123 | 124 | ipc: host 125 | 126 | # Mapping or list 127 | # Mapping values can be strings, numbers or null 128 | labels: 129 | com.example.description: "Accounting webapp" 130 | com.example.number: 42 131 | com.example.empty-label: 132 | # labels: 133 | # - "com.example.description=Accounting webapp" 134 | # - "com.example.number=42" 135 | # - "com.example.empty-label" 136 | 137 | links: 138 | - db 139 | - db:database 140 | - redis 141 | 142 | logging: 143 | driver: syslog 144 | options: 145 | syslog-address: "tcp://192.168.0.42:123" 146 | 147 | mac_address: 02:42:ac:11:65:43 148 | 149 | # network_mode: "bridge" 150 | # network_mode: "host" 151 | # network_mode: "none" 152 | # Use the network mode of an arbitrary container from another service 153 | # network_mode: "service:db" 154 | # Use the network mode of another container, specified by name or id 155 | # network_mode: "container:some-container" 156 | network_mode: "container:0cfeab0f748b9a743dc3da582046357c6ef497631c1a016d28d2bf9b4f899f7b" 157 | 158 | networks: 159 | some-network: 160 | aliases: 161 | - alias1 162 | - alias3 163 | other-network: 164 | ipv4_address: 172.16.238.10 165 | ipv6_address: 2001:3984:3989::10 166 | other-other-network: 167 | 168 | pid: "host" 169 | 170 | ports: 171 | - 3000 172 | - "3000-3005" 173 | - "8000:8000" 174 | - "9090-9091:8080-8081" 175 | - "49100:22" 176 | - "127.0.0.1:8001:8001" 177 | - "127.0.0.1:5000-5010:5000-5010" 178 | 179 | privileged: true 180 | 181 | read_only: true 182 | 183 | restart: always 184 | 185 | security_opt: 186 | - label=level:s0:c100,c200 187 | - label=type:svirt_apache_t 188 | 189 | stdin_open: true 190 | 191 | stop_grace_period: 20s 192 | 193 | stop_signal: SIGUSR1 194 | 195 | # String or list 196 | # tmpfs: /run 197 | tmpfs: 198 | - /run 199 | - /tmp 200 | 201 | tty: true 202 | 203 | ulimits: 204 | # Single number or mapping with soft + hard limits 205 | nproc: 65535 206 | nofile: 207 | soft: 20000 208 | hard: 40000 209 | 210 | user: someone 211 | 212 | volumes: 213 | # Just specify a path and let the Engine create a volume 214 | - /var/lib/mysql 215 | # Specify an absolute path mapping 216 | - /opt/data:/var/lib/mysql 217 | # Path on the host, relative to the Compose file 218 | - .:/code 219 | - ./static:/var/www/html 220 | # User-relative path 221 | - ~/configs:/etc/configs/:ro 222 | # Named volume 223 | - datavolume:/var/lib/mysql 224 | 225 | working_dir: /code 226 | 227 | networks: 228 | # Entries can be null, which specifies simply that a network 229 | # called "{project name}_some-network" should be created and 230 | # use the default driver 231 | some-network: 232 | 233 | other-network: 234 | driver: overlay 235 | 236 | driver_opts: 237 | # Values can be strings or numbers 238 | foo: "bar" 239 | baz: 1 240 | 241 | ipam: 242 | driver: overlay 243 | # driver_opts: 244 | # # Values can be strings or numbers 245 | # com.docker.network.enable_ipv6: "true" 246 | # com.docker.network.numeric_value: 1 247 | config: 248 | - subnet: 172.16.238.0/24 249 | # gateway: 172.16.238.1 250 | - subnet: 2001:3984:3989::/64 251 | # gateway: 2001:3984:3989::1 252 | 253 | external-network: 254 | # Specifies that a pre-existing network called "external-network" 255 | # can be referred to within this file as "external-network" 256 | external: true 257 | 258 | other-external-network: 259 | # Specifies that a pre-existing network called "my-cool-network" 260 | # can be referred to within this file as "other-external-network" 261 | external: 262 | name: my-cool-network 263 | 264 | volumes: 265 | # Entries can be null, which specifies simply that a volume 266 | # called "{project name}_some-volume" should be created and 267 | # use the default driver 268 | some-volume: 269 | 270 | other-volume: 271 | driver: flocker 272 | 273 | driver_opts: 274 | # Values can be strings or numbers 275 | foo: "bar" 276 | baz: 1 277 | 278 | external-volume: 279 | # Specifies that a pre-existing volume called "external-volume" 280 | # can be referred to within this file as "external-volume" 281 | external: true 282 | 283 | other-external-volume: 284 | # Specifies that a pre-existing volume called "my-cool-volume" 285 | # can be referred to within this file as "other-external-volume" 286 | external: 287 | name: my-cool-volume 288 | -------------------------------------------------------------------------------- /loader/loader.go: -------------------------------------------------------------------------------- 1 | package loader 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path" 7 | "reflect" 8 | "regexp" 9 | "sort" 10 | "strings" 11 | 12 | "github.com/aanand/compose-file/interpolation" 13 | "github.com/aanand/compose-file/schema" 14 | "github.com/aanand/compose-file/types" 15 | "github.com/docker/docker/runconfig/opts" 16 | units "github.com/docker/go-units" 17 | shellwords "github.com/mattn/go-shellwords" 18 | "github.com/mitchellh/mapstructure" 19 | yaml "gopkg.in/yaml.v2" 20 | ) 21 | 22 | var ( 23 | fieldNameRegexp = regexp.MustCompile("[A-Z][a-z0-9]+") 24 | ) 25 | 26 | // ParseYAML reads the bytes from a file, parses the bytes into a mapping 27 | // structure, and returns it. 28 | func ParseYAML(source []byte) (types.Dict, error) { 29 | var cfg interface{} 30 | if err := yaml.Unmarshal(source, &cfg); err != nil { 31 | return nil, err 32 | } 33 | cfgMap, ok := cfg.(map[interface{}]interface{}) 34 | if !ok { 35 | return nil, fmt.Errorf("Top-level object must be a mapping") 36 | } 37 | converted, err := convertToStringKeysRecursive(cfgMap, "") 38 | if err != nil { 39 | return nil, err 40 | } 41 | return converted.(types.Dict), nil 42 | } 43 | 44 | // Load reads a ConfigDetails and returns a fully loaded configuration 45 | func Load(configDetails types.ConfigDetails) (*types.Config, error) { 46 | if len(configDetails.ConfigFiles) < 1 { 47 | return nil, fmt.Errorf("No files specified") 48 | } 49 | if len(configDetails.ConfigFiles) > 1 { 50 | return nil, fmt.Errorf("Multiple files are not yet supported") 51 | } 52 | 53 | configDict := getConfigDict(configDetails) 54 | 55 | if services, ok := configDict["services"]; ok { 56 | if servicesDict, ok := services.(types.Dict); ok { 57 | forbidden := getProperties(servicesDict, types.ForbiddenProperties) 58 | 59 | if len(forbidden) > 0 { 60 | return nil, &ForbiddenPropertiesError{Properties: forbidden} 61 | } 62 | } 63 | } 64 | 65 | if err := schema.Validate(configDict); err != nil { 66 | return nil, err 67 | } 68 | 69 | cfg := types.Config{} 70 | version := configDict["version"].(string) 71 | if version != "3" && version != "3.0" { 72 | return nil, fmt.Errorf(`Unsupported Compose file version: %#v. The only version supported is "3" (or "3.0")`, version) 73 | } 74 | 75 | if services, ok := configDict["services"]; ok { 76 | servicesConfig, err := interpolation.Interpolate(services.(types.Dict), "service", os.LookupEnv) 77 | if err != nil { 78 | return nil, err 79 | } 80 | 81 | servicesList, err := loadServices(servicesConfig, configDetails.WorkingDir) 82 | if err != nil { 83 | return nil, err 84 | } 85 | 86 | cfg.Services = servicesList 87 | } 88 | 89 | if networks, ok := configDict["networks"]; ok { 90 | networksConfig, err := interpolation.Interpolate(networks.(types.Dict), "network", os.LookupEnv) 91 | if err != nil { 92 | return nil, err 93 | } 94 | 95 | networksMapping, err := loadNetworks(networksConfig) 96 | if err != nil { 97 | return nil, err 98 | } 99 | 100 | cfg.Networks = networksMapping 101 | } 102 | 103 | if volumes, ok := configDict["volumes"]; ok { 104 | volumesConfig, err := interpolation.Interpolate(volumes.(types.Dict), "volume", os.LookupEnv) 105 | if err != nil { 106 | return nil, err 107 | } 108 | 109 | volumesMapping, err := loadVolumes(volumesConfig) 110 | if err != nil { 111 | return nil, err 112 | } 113 | 114 | cfg.Volumes = volumesMapping 115 | } 116 | 117 | return &cfg, nil 118 | } 119 | 120 | func GetUnsupportedProperties(configDetails types.ConfigDetails) []string { 121 | unsupported := map[string]bool{} 122 | 123 | for _, service := range getServices(getConfigDict(configDetails)) { 124 | serviceDict := service.(types.Dict) 125 | for _, property := range types.UnsupportedProperties { 126 | if _, isSet := serviceDict[property]; isSet { 127 | unsupported[property] = true 128 | } 129 | } 130 | } 131 | 132 | return sortedKeys(unsupported) 133 | } 134 | 135 | func sortedKeys(set map[string]bool) []string { 136 | var keys []string 137 | for key := range set { 138 | keys = append(keys, key) 139 | } 140 | sort.Strings(keys) 141 | return keys 142 | } 143 | 144 | func GetDeprecatedProperties(configDetails types.ConfigDetails) map[string]string { 145 | return getProperties(getServices(getConfigDict(configDetails)), types.DeprecatedProperties) 146 | } 147 | 148 | func getProperties(services types.Dict, propertyMap map[string]string) map[string]string { 149 | output := map[string]string{} 150 | 151 | for _, service := range services { 152 | if serviceDict, ok := service.(types.Dict); ok { 153 | for property, description := range propertyMap { 154 | if _, isSet := serviceDict[property]; isSet { 155 | output[property] = description 156 | } 157 | } 158 | } 159 | } 160 | 161 | return output 162 | } 163 | 164 | type ForbiddenPropertiesError struct { 165 | Properties map[string]string 166 | } 167 | 168 | func (e *ForbiddenPropertiesError) Error() string { 169 | return "Configuration contains forbidden properties" 170 | } 171 | 172 | // TODO: resolve multiple files into a single config 173 | func getConfigDict(configDetails types.ConfigDetails) types.Dict { 174 | return configDetails.ConfigFiles[0].Config 175 | } 176 | 177 | func getServices(configDict types.Dict) types.Dict { 178 | if services, ok := configDict["services"]; ok { 179 | if servicesDict, ok := services.(types.Dict); ok { 180 | return servicesDict 181 | } 182 | } 183 | 184 | return types.Dict{} 185 | } 186 | 187 | func transform(source map[string]interface{}, target interface{}) error { 188 | data := mapstructure.Metadata{} 189 | config := &mapstructure.DecoderConfig{ 190 | DecodeHook: mapstructure.ComposeDecodeHookFunc( 191 | transformHook, 192 | mapstructure.StringToTimeDurationHookFunc()), 193 | Result: target, 194 | Metadata: &data, 195 | } 196 | decoder, err := mapstructure.NewDecoder(config) 197 | if err != nil { 198 | return err 199 | } 200 | err = decoder.Decode(source) 201 | // TODO: log unused keys 202 | return err 203 | } 204 | 205 | func transformHook( 206 | source reflect.Type, 207 | target reflect.Type, 208 | data interface{}, 209 | ) (interface{}, error) { 210 | switch target { 211 | case reflect.TypeOf(types.External{}): 212 | return transformExternal(source, target, data) 213 | case reflect.TypeOf(make(map[string]string, 0)): 214 | return transformMapStringString(source, target, data) 215 | case reflect.TypeOf(types.UlimitsConfig{}): 216 | return transformUlimits(source, target, data) 217 | case reflect.TypeOf(types.UnitBytes(0)): 218 | return loadSize(data) 219 | } 220 | switch target.Kind() { 221 | case reflect.Struct: 222 | return transformStruct(source, target, data) 223 | } 224 | return data, nil 225 | } 226 | 227 | // keys needs to be converted to strings for jsonschema 228 | // TODO: don't use types.Dict 229 | func convertToStringKeysRecursive(value interface{}, keyPrefix string) (interface{}, error) { 230 | if mapping, ok := value.(map[interface{}]interface{}); ok { 231 | dict := make(types.Dict) 232 | for key, entry := range mapping { 233 | str, ok := key.(string) 234 | if !ok { 235 | var location string 236 | if keyPrefix == "" { 237 | location = "at top level" 238 | } else { 239 | location = fmt.Sprintf("in %s", keyPrefix) 240 | } 241 | return nil, fmt.Errorf("Non-string key %s: %#v", location, key) 242 | } 243 | var newKeyPrefix string 244 | if keyPrefix == "" { 245 | newKeyPrefix = str 246 | } else { 247 | newKeyPrefix = fmt.Sprintf("%s.%s", keyPrefix, str) 248 | } 249 | convertedEntry, err := convertToStringKeysRecursive(entry, newKeyPrefix) 250 | if err != nil { 251 | return nil, err 252 | } 253 | dict[str] = convertedEntry 254 | } 255 | return dict, nil 256 | } 257 | if list, ok := value.([]interface{}); ok { 258 | var convertedList []interface{} 259 | for index, entry := range list { 260 | newKeyPrefix := fmt.Sprintf("%s[%d]", keyPrefix, index) 261 | convertedEntry, err := convertToStringKeysRecursive(entry, newKeyPrefix) 262 | if err != nil { 263 | return nil, err 264 | } 265 | convertedList = append(convertedList, convertedEntry) 266 | } 267 | return convertedList, nil 268 | } 269 | return value, nil 270 | } 271 | 272 | func loadServices(servicesDict types.Dict, workingDir string) ([]types.ServiceConfig, error) { 273 | var services []types.ServiceConfig 274 | 275 | for name, serviceDef := range servicesDict { 276 | serviceConfig, err := loadService(name, serviceDef.(types.Dict), workingDir) 277 | if err != nil { 278 | return nil, err 279 | } 280 | services = append(services, *serviceConfig) 281 | } 282 | 283 | return services, nil 284 | } 285 | 286 | func loadService(name string, serviceDict types.Dict, workingDir string) (*types.ServiceConfig, error) { 287 | serviceConfig := &types.ServiceConfig{} 288 | if err := transform(serviceDict, serviceConfig); err != nil { 289 | return nil, err 290 | } 291 | serviceConfig.Name = name 292 | 293 | if err := resolveEnvironment(serviceConfig, serviceDict, workingDir); err != nil { 294 | return nil, err 295 | } 296 | 297 | if err := resolveVolumePaths(serviceConfig.Volumes, workingDir); err != nil { 298 | return nil, err 299 | } 300 | 301 | return serviceConfig, nil 302 | } 303 | 304 | func resolveEnvironment(serviceConfig *types.ServiceConfig, serviceDict types.Dict, workingDir string) error { 305 | environment := make(map[string]string) 306 | 307 | if envFileVal, ok := serviceDict["env_file"]; ok { 308 | envFiles := loadStringOrListOfStrings(envFileVal) 309 | 310 | var envVars []string 311 | 312 | for _, file := range envFiles { 313 | filePath := path.Join(workingDir, file) 314 | fileVars, err := opts.ParseEnvFile(filePath) 315 | if err != nil { 316 | return err 317 | } 318 | envVars = append(envVars, fileVars...) 319 | } 320 | 321 | for k, v := range opts.ConvertKVStringsToMap(envVars) { 322 | environment[k] = v 323 | } 324 | } 325 | 326 | for k, v := range serviceConfig.Environment { 327 | environment[k] = v 328 | } 329 | 330 | serviceConfig.Environment = environment 331 | 332 | return nil 333 | } 334 | 335 | func resolveVolumePaths(volumes []string, workingDir string) error { 336 | for i, mapping := range volumes { 337 | parts := strings.SplitN(mapping, ":", 2) 338 | if len(parts) == 1 { 339 | continue 340 | } 341 | 342 | if strings.HasPrefix(parts[0], ".") { 343 | parts[0] = path.Join(workingDir, parts[0]) 344 | } 345 | parts[0] = expandUser(parts[0]) 346 | 347 | volumes[i] = strings.Join(parts, ":") 348 | } 349 | 350 | return nil 351 | } 352 | 353 | // TODO: make this more robust 354 | func expandUser(path string) string { 355 | if strings.HasPrefix(path, "~") { 356 | return strings.Replace(path, "~", os.Getenv("HOME"), 1) 357 | } 358 | return path 359 | } 360 | 361 | func transformUlimits( 362 | source reflect.Type, 363 | target reflect.Type, 364 | data interface{}, 365 | ) (interface{}, error) { 366 | switch value := data.(type) { 367 | case int: 368 | return types.UlimitsConfig{Single: value}, nil 369 | case types.Dict: 370 | ulimit := types.UlimitsConfig{} 371 | ulimit.Soft = value["soft"].(int) 372 | ulimit.Hard = value["hard"].(int) 373 | return ulimit, nil 374 | default: 375 | return data, fmt.Errorf("invalid type %T for ulimits", value) 376 | } 377 | } 378 | 379 | func loadNetworks(source types.Dict) (map[string]types.NetworkConfig, error) { 380 | networks := make(map[string]types.NetworkConfig) 381 | err := transform(source, &networks) 382 | if err != nil { 383 | return networks, err 384 | } 385 | for name, network := range networks { 386 | if network.External.External && network.External.Name == "" { 387 | network.External.Name = name 388 | networks[name] = network 389 | } 390 | } 391 | return networks, nil 392 | } 393 | 394 | func loadVolumes(source types.Dict) (map[string]types.VolumeConfig, error) { 395 | volumes := make(map[string]types.VolumeConfig) 396 | err := transform(source, &volumes) 397 | if err != nil { 398 | return volumes, err 399 | } 400 | for name, volume := range volumes { 401 | if volume.External.External && volume.External.Name == "" { 402 | volume.External.Name = name 403 | volumes[name] = volume 404 | } 405 | } 406 | return volumes, nil 407 | } 408 | 409 | func transformStruct( 410 | source reflect.Type, 411 | target reflect.Type, 412 | data interface{}, 413 | ) (interface{}, error) { 414 | structValue, ok := data.(map[string]interface{}) 415 | if !ok { 416 | // FIXME: this is necessary because of convertToStringKeysRecursive 417 | structValue, ok = data.(types.Dict) 418 | if !ok { 419 | panic(fmt.Sprintf( 420 | "transformStruct called with non-map type: %T, %s", data, data)) 421 | } 422 | } 423 | 424 | var err error 425 | for i := 0; i < target.NumField(); i++ { 426 | field := target.Field(i) 427 | fieldTag := field.Tag.Get("compose") 428 | 429 | yamlName := toYAMLName(field.Name) 430 | value, ok := structValue[yamlName] 431 | if !ok { 432 | continue 433 | } 434 | 435 | structValue[yamlName], err = convertField( 436 | fieldTag, reflect.TypeOf(value), field.Type, value) 437 | if err != nil { 438 | return nil, fmt.Errorf("field %s: %s", yamlName, err.Error()) 439 | } 440 | } 441 | return structValue, nil 442 | } 443 | 444 | func transformMapStringString( 445 | source reflect.Type, 446 | target reflect.Type, 447 | data interface{}, 448 | ) (interface{}, error) { 449 | switch value := data.(type) { 450 | case map[string]interface{}: 451 | return toMapStringString(value), nil 452 | case types.Dict: 453 | return toMapStringString(value), nil 454 | case map[string]string: 455 | return value, nil 456 | default: 457 | return data, fmt.Errorf("invalid type %T for map[string]string", value) 458 | } 459 | } 460 | 461 | func convertField( 462 | fieldTag string, 463 | source reflect.Type, 464 | target reflect.Type, 465 | data interface{}, 466 | ) (interface{}, error) { 467 | switch fieldTag { 468 | case "": 469 | return data, nil 470 | case "healthcheck": 471 | return loadHealthcheck(data) 472 | case "list_or_dict_equals": 473 | return loadMappingOrList(data, "="), nil 474 | case "list_or_dict_colon": 475 | return loadMappingOrList(data, ":"), nil 476 | case "list_or_struct_map": 477 | return loadListOrStructMap(data, target) 478 | case "string_or_list": 479 | return loadStringOrListOfStrings(data), nil 480 | case "list_of_strings_or_numbers": 481 | return loadListOfStringsOrNumbers(data), nil 482 | case "shell_command": 483 | return loadShellCommand(data) 484 | case "size": 485 | return loadSize(data) 486 | case "-": 487 | return nil, nil 488 | } 489 | return data, nil 490 | } 491 | 492 | func transformExternal( 493 | source reflect.Type, 494 | target reflect.Type, 495 | data interface{}, 496 | ) (interface{}, error) { 497 | switch value := data.(type) { 498 | case bool: 499 | return map[string]interface{}{"external": value}, nil 500 | case types.Dict: 501 | return map[string]interface{}{"external": true, "name": value["name"]}, nil 502 | case map[string]interface{}: 503 | return map[string]interface{}{"external": true, "name": value["name"]}, nil 504 | default: 505 | return data, fmt.Errorf("invalid type %T for external", value) 506 | } 507 | } 508 | 509 | func toYAMLName(name string) string { 510 | nameParts := fieldNameRegexp.FindAllString(name, -1) 511 | for i, p := range nameParts { 512 | nameParts[i] = strings.ToLower(p) 513 | } 514 | return strings.Join(nameParts, "_") 515 | } 516 | 517 | func loadListOrStructMap(value interface{}, target reflect.Type) (interface{}, error) { 518 | if list, ok := value.([]interface{}); ok { 519 | mapValue := map[interface{}]interface{}{} 520 | for _, name := range list { 521 | mapValue[name] = nil 522 | } 523 | return mapValue, nil 524 | } 525 | 526 | return value, nil 527 | } 528 | 529 | func loadListOfStringsOrNumbers(value interface{}) []string { 530 | list := value.([]interface{}) 531 | result := make([]string, len(list)) 532 | for i, item := range list { 533 | result[i] = fmt.Sprint(item) 534 | } 535 | return result 536 | } 537 | 538 | func loadStringOrListOfStrings(value interface{}) []string { 539 | if list, ok := value.([]interface{}); ok { 540 | result := make([]string, len(list)) 541 | for i, item := range list { 542 | result[i] = fmt.Sprint(item) 543 | } 544 | return result 545 | } 546 | return []string{value.(string)} 547 | } 548 | 549 | func loadMappingOrList(mappingOrList interface{}, sep string) map[string]string { 550 | if mapping, ok := mappingOrList.(types.Dict); ok { 551 | return toMapStringString(mapping) 552 | } 553 | if list, ok := mappingOrList.([]interface{}); ok { 554 | result := make(map[string]string) 555 | for _, value := range list { 556 | parts := strings.SplitN(value.(string), sep, 2) 557 | if len(parts) == 1 { 558 | result[parts[0]] = "" 559 | } else { 560 | result[parts[0]] = parts[1] 561 | } 562 | } 563 | return result 564 | } 565 | panic(fmt.Errorf("expected a map or a slice, got: %#v", mappingOrList)) 566 | } 567 | 568 | func loadShellCommand(value interface{}) (interface{}, error) { 569 | if str, ok := value.(string); ok { 570 | return shellwords.Parse(str) 571 | } 572 | return value, nil 573 | } 574 | 575 | func loadHealthcheck(value interface{}) (interface{}, error) { 576 | if str, ok := value.(string); ok { 577 | return append([]string{"CMD-SHELL"}, str), nil 578 | } 579 | return value, nil 580 | } 581 | 582 | func loadSize(value interface{}) (int64, error) { 583 | switch value := value.(type) { 584 | case int: 585 | return int64(value), nil 586 | case string: 587 | return units.RAMInBytes(value) 588 | } 589 | panic(fmt.Errorf("invalid type for size %T", value)) 590 | } 591 | 592 | func toMapStringString(value map[string]interface{}) map[string]string { 593 | output := make(map[string]string) 594 | for key, value := range value { 595 | output[key] = toString(value) 596 | } 597 | return output 598 | } 599 | 600 | func toString(value interface{}) string { 601 | if value == nil { 602 | return "" 603 | } 604 | return fmt.Sprint(value) 605 | } 606 | -------------------------------------------------------------------------------- /loader/loader_test.go: -------------------------------------------------------------------------------- 1 | package loader 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "sort" 8 | "testing" 9 | "time" 10 | 11 | "github.com/aanand/compose-file/types" 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | func buildConfigDetails(source types.Dict) types.ConfigDetails { 16 | workingDir, err := os.Getwd() 17 | if err != nil { 18 | panic(err) 19 | } 20 | 21 | return types.ConfigDetails{ 22 | WorkingDir: workingDir, 23 | ConfigFiles: []types.ConfigFile{ 24 | {Filename: "filename.yml", Config: source}, 25 | }, 26 | Environment: nil, 27 | } 28 | } 29 | 30 | var sampleYAML = ` 31 | version: "3" 32 | services: 33 | foo: 34 | image: busybox 35 | networks: 36 | with_me: 37 | bar: 38 | image: busybox 39 | environment: 40 | - FOO=1 41 | networks: 42 | - with_ipam 43 | volumes: 44 | hello: 45 | driver: default 46 | driver_opts: 47 | beep: boop 48 | networks: 49 | default: 50 | driver: bridge 51 | driver_opts: 52 | beep: boop 53 | with_ipam: 54 | ipam: 55 | driver: default 56 | config: 57 | - subnet: 172.28.0.0/16 58 | ` 59 | 60 | var sampleDict = types.Dict{ 61 | "version": "3", 62 | "services": types.Dict{ 63 | "foo": types.Dict{ 64 | "image": "busybox", 65 | "networks": types.Dict{"with_me": nil}, 66 | }, 67 | "bar": types.Dict{ 68 | "image": "busybox", 69 | "environment": []interface{}{"FOO=1"}, 70 | "networks": []interface{}{"with_ipam"}, 71 | }, 72 | }, 73 | "volumes": types.Dict{ 74 | "hello": types.Dict{ 75 | "driver": "default", 76 | "driver_opts": types.Dict{ 77 | "beep": "boop", 78 | }, 79 | }, 80 | }, 81 | "networks": types.Dict{ 82 | "default": types.Dict{ 83 | "driver": "bridge", 84 | "driver_opts": types.Dict{ 85 | "beep": "boop", 86 | }, 87 | }, 88 | "with_ipam": types.Dict{ 89 | "ipam": types.Dict{ 90 | "driver": "default", 91 | "config": []interface{}{ 92 | types.Dict{ 93 | "subnet": "172.28.0.0/16", 94 | }, 95 | }, 96 | }, 97 | }, 98 | }, 99 | } 100 | 101 | var sampleConfig = types.Config{ 102 | Services: []types.ServiceConfig{ 103 | { 104 | Name: "foo", 105 | Image: "busybox", 106 | Environment: map[string]string{}, 107 | Networks: map[string]*types.ServiceNetworkConfig{ 108 | "with_me": nil, 109 | }, 110 | }, 111 | { 112 | Name: "bar", 113 | Image: "busybox", 114 | Environment: map[string]string{"FOO": "1"}, 115 | Networks: map[string]*types.ServiceNetworkConfig{ 116 | "with_ipam": nil, 117 | }, 118 | }, 119 | }, 120 | Networks: map[string]types.NetworkConfig{ 121 | "default": { 122 | Driver: "bridge", 123 | DriverOpts: map[string]string{ 124 | "beep": "boop", 125 | }, 126 | }, 127 | "with_ipam": { 128 | Ipam: types.IPAMConfig{ 129 | Driver: "default", 130 | Config: []*types.IPAMPool{ 131 | { 132 | Subnet: "172.28.0.0/16", 133 | }, 134 | }, 135 | }, 136 | }, 137 | }, 138 | Volumes: map[string]types.VolumeConfig{ 139 | "hello": { 140 | Driver: "default", 141 | DriverOpts: map[string]string{ 142 | "beep": "boop", 143 | }, 144 | }, 145 | }, 146 | } 147 | 148 | func TestParseYAML(t *testing.T) { 149 | dict, err := ParseYAML([]byte(sampleYAML)) 150 | if !assert.NoError(t, err) { 151 | return 152 | } 153 | assert.Equal(t, sampleDict, dict) 154 | } 155 | 156 | func TestLoad(t *testing.T) { 157 | actual, err := Load(buildConfigDetails(sampleDict)) 158 | if !assert.NoError(t, err) { 159 | return 160 | } 161 | assert.Equal(t, serviceSort(sampleConfig.Services), serviceSort(actual.Services)) 162 | assert.Equal(t, sampleConfig.Networks, actual.Networks) 163 | assert.Equal(t, sampleConfig.Volumes, actual.Volumes) 164 | } 165 | 166 | func TestParseAndLoad(t *testing.T) { 167 | actual, err := loadYAML(sampleYAML) 168 | if !assert.NoError(t, err) { 169 | return 170 | } 171 | assert.Equal(t, serviceSort(sampleConfig.Services), serviceSort(actual.Services)) 172 | assert.Equal(t, sampleConfig.Networks, actual.Networks) 173 | assert.Equal(t, sampleConfig.Volumes, actual.Volumes) 174 | } 175 | 176 | func TestInvalidTopLevelObjectType(t *testing.T) { 177 | _, err := loadYAML("1") 178 | assert.Error(t, err) 179 | assert.Contains(t, err.Error(), "Top-level object must be a mapping") 180 | 181 | _, err = loadYAML("\"hello\"") 182 | assert.Error(t, err) 183 | assert.Contains(t, err.Error(), "Top-level object must be a mapping") 184 | 185 | _, err = loadYAML("[\"hello\"]") 186 | assert.Error(t, err) 187 | assert.Contains(t, err.Error(), "Top-level object must be a mapping") 188 | } 189 | 190 | func TestNonStringKeys(t *testing.T) { 191 | _, err := loadYAML(` 192 | version: "3" 193 | 123: 194 | foo: 195 | image: busybox 196 | `) 197 | assert.Error(t, err) 198 | assert.Contains(t, err.Error(), "Non-string key at top level: 123") 199 | 200 | _, err = loadYAML(` 201 | version: "3" 202 | services: 203 | foo: 204 | image: busybox 205 | 123: 206 | image: busybox 207 | `) 208 | assert.Error(t, err) 209 | assert.Contains(t, err.Error(), "Non-string key in services: 123") 210 | 211 | _, err = loadYAML(` 212 | version: "3" 213 | services: 214 | foo: 215 | image: busybox 216 | networks: 217 | default: 218 | ipam: 219 | config: 220 | - 123: oh dear 221 | `) 222 | assert.Error(t, err) 223 | assert.Contains(t, err.Error(), "Non-string key in networks.default.ipam.config[0]: 123") 224 | 225 | _, err = loadYAML(` 226 | version: "3" 227 | services: 228 | dict-env: 229 | image: busybox 230 | environment: 231 | 1: FOO 232 | `) 233 | assert.Error(t, err) 234 | assert.Contains(t, err.Error(), "Non-string key in services.dict-env.environment: 1") 235 | } 236 | 237 | func TestSupportedVersion(t *testing.T) { 238 | _, err := loadYAML(` 239 | version: "3" 240 | services: 241 | foo: 242 | image: busybox 243 | `) 244 | assert.NoError(t, err) 245 | 246 | _, err = loadYAML(` 247 | version: "3.0" 248 | services: 249 | foo: 250 | image: busybox 251 | `) 252 | assert.NoError(t, err) 253 | } 254 | 255 | func TestUnsupportedVersion(t *testing.T) { 256 | _, err := loadYAML(` 257 | version: "2" 258 | services: 259 | foo: 260 | image: busybox 261 | `) 262 | assert.Error(t, err) 263 | assert.Contains(t, err.Error(), "version") 264 | 265 | _, err = loadYAML(` 266 | version: "2.0" 267 | services: 268 | foo: 269 | image: busybox 270 | `) 271 | assert.Error(t, err) 272 | assert.Contains(t, err.Error(), "version") 273 | } 274 | 275 | func TestInvalidVersion(t *testing.T) { 276 | _, err := loadYAML(` 277 | version: 3 278 | services: 279 | foo: 280 | image: busybox 281 | `) 282 | assert.Error(t, err) 283 | assert.Contains(t, err.Error(), "version must be a string") 284 | } 285 | 286 | func TestV1Unsupported(t *testing.T) { 287 | _, err := loadYAML(` 288 | foo: 289 | image: busybox 290 | `) 291 | assert.Error(t, err) 292 | } 293 | 294 | func TestNonMappingObject(t *testing.T) { 295 | _, err := loadYAML(` 296 | version: "3" 297 | services: 298 | - foo: 299 | image: busybox 300 | `) 301 | assert.Error(t, err) 302 | assert.Contains(t, err.Error(), "services must be a mapping") 303 | 304 | _, err = loadYAML(` 305 | version: "3" 306 | services: 307 | foo: busybox 308 | `) 309 | assert.Error(t, err) 310 | assert.Contains(t, err.Error(), "services.foo must be a mapping") 311 | 312 | _, err = loadYAML(` 313 | version: "3" 314 | networks: 315 | - default: 316 | driver: bridge 317 | `) 318 | assert.Error(t, err) 319 | assert.Contains(t, err.Error(), "networks must be a mapping") 320 | 321 | _, err = loadYAML(` 322 | version: "3" 323 | networks: 324 | default: bridge 325 | `) 326 | assert.Error(t, err) 327 | assert.Contains(t, err.Error(), "networks.default must be a mapping") 328 | 329 | _, err = loadYAML(` 330 | version: "3" 331 | volumes: 332 | - data: 333 | driver: local 334 | `) 335 | assert.Error(t, err) 336 | assert.Contains(t, err.Error(), "volumes must be a mapping") 337 | 338 | _, err = loadYAML(` 339 | version: "3" 340 | volumes: 341 | data: local 342 | `) 343 | assert.Error(t, err) 344 | assert.Contains(t, err.Error(), "volumes.data must be a mapping") 345 | } 346 | 347 | func TestNonStringImage(t *testing.T) { 348 | _, err := loadYAML(` 349 | version: "3" 350 | services: 351 | foo: 352 | image: ["busybox", "latest"] 353 | `) 354 | assert.Error(t, err) 355 | assert.Contains(t, err.Error(), "services.foo.image must be a string") 356 | } 357 | 358 | func TestValidEnvironment(t *testing.T) { 359 | config, err := loadYAML(` 360 | version: "3" 361 | services: 362 | dict-env: 363 | image: busybox 364 | environment: 365 | FOO: "1" 366 | BAR: 2 367 | BAZ: 2.5 368 | QUUX: 369 | list-env: 370 | image: busybox 371 | environment: 372 | - FOO=1 373 | - BAR=2 374 | - BAZ=2.5 375 | - QUUX= 376 | `) 377 | assert.NoError(t, err) 378 | 379 | expected := map[string]string{ 380 | "FOO": "1", 381 | "BAR": "2", 382 | "BAZ": "2.5", 383 | "QUUX": "", 384 | } 385 | 386 | assert.Equal(t, 2, len(config.Services)) 387 | 388 | for _, service := range config.Services { 389 | assert.Equal(t, expected, service.Environment) 390 | } 391 | } 392 | 393 | func TestInvalidEnvironmentValue(t *testing.T) { 394 | _, err := loadYAML(` 395 | version: "3" 396 | services: 397 | dict-env: 398 | image: busybox 399 | environment: 400 | FOO: ["1"] 401 | `) 402 | assert.Error(t, err) 403 | assert.Contains(t, err.Error(), "services.dict-env.environment.FOO must be a string, number or null") 404 | } 405 | 406 | func TestInvalidEnvironmentObject(t *testing.T) { 407 | _, err := loadYAML(` 408 | version: "3" 409 | services: 410 | dict-env: 411 | image: busybox 412 | environment: "FOO=1" 413 | `) 414 | assert.Error(t, err) 415 | assert.Contains(t, err.Error(), "services.dict-env.environment must be a mapping") 416 | } 417 | 418 | func TestEnvironmentInterpolation(t *testing.T) { 419 | config, err := loadYAML(` 420 | version: "3" 421 | services: 422 | test: 423 | image: busybox 424 | labels: 425 | - home1=$HOME 426 | - home2=${HOME} 427 | - nonexistent=$NONEXISTENT 428 | - default=${NONEXISTENT-default} 429 | networks: 430 | test: 431 | driver: $HOME 432 | volumes: 433 | test: 434 | driver: $HOME 435 | `) 436 | 437 | assert.NoError(t, err) 438 | 439 | home := os.Getenv("HOME") 440 | 441 | expectedLabels := map[string]string{ 442 | "home1": home, 443 | "home2": home, 444 | "nonexistent": "", 445 | "default": "default", 446 | } 447 | 448 | assert.Equal(t, expectedLabels, config.Services[0].Labels) 449 | assert.Equal(t, home, config.Networks["test"].Driver) 450 | assert.Equal(t, home, config.Volumes["test"].Driver) 451 | } 452 | 453 | func TestUnsupportedProperties(t *testing.T) { 454 | dict, err := ParseYAML([]byte(` 455 | version: "3" 456 | services: 457 | web: 458 | image: web 459 | build: ./web 460 | links: 461 | - bar 462 | db: 463 | image: db 464 | build: ./db 465 | `)) 466 | assert.NoError(t, err) 467 | 468 | configDetails := buildConfigDetails(dict) 469 | 470 | _, err = Load(configDetails) 471 | assert.NoError(t, err) 472 | 473 | unsupported := GetUnsupportedProperties(configDetails) 474 | assert.Equal(t, []string{"build", "links"}, unsupported) 475 | } 476 | 477 | func TestDeprecatedProperties(t *testing.T) { 478 | dict, err := ParseYAML([]byte(` 479 | version: "3" 480 | services: 481 | web: 482 | image: web 483 | container_name: web 484 | db: 485 | image: db 486 | container_name: db 487 | expose: ["5434"] 488 | `)) 489 | assert.NoError(t, err) 490 | 491 | configDetails := buildConfigDetails(dict) 492 | 493 | _, err = Load(configDetails) 494 | assert.NoError(t, err) 495 | 496 | deprecated := GetDeprecatedProperties(configDetails) 497 | assert.Equal(t, 2, len(deprecated)) 498 | assert.Contains(t, deprecated, "container_name") 499 | assert.Contains(t, deprecated, "expose") 500 | } 501 | 502 | func TestForbiddenProperties(t *testing.T) { 503 | _, err := loadYAML(` 504 | version: "3" 505 | services: 506 | foo: 507 | image: busybox 508 | volumes: 509 | - /data 510 | volume_driver: some-driver 511 | bar: 512 | extends: 513 | service: foo 514 | `) 515 | 516 | assert.Error(t, err) 517 | assert.IsType(t, &ForbiddenPropertiesError{}, err) 518 | fmt.Println(err) 519 | forbidden := err.(*ForbiddenPropertiesError).Properties 520 | 521 | assert.Equal(t, 2, len(forbidden)) 522 | assert.Contains(t, forbidden, "volume_driver") 523 | assert.Contains(t, forbidden, "extends") 524 | } 525 | 526 | func durationPtr(value time.Duration) *time.Duration { 527 | return &value 528 | } 529 | 530 | func int64Ptr(value int64) *int64 { 531 | return &value 532 | } 533 | 534 | func uint64Ptr(value uint64) *uint64 { 535 | return &value 536 | } 537 | 538 | func TestFullExample(t *testing.T) { 539 | bytes, err := ioutil.ReadFile("full-example.yml") 540 | assert.NoError(t, err) 541 | 542 | config, err := loadYAML(string(bytes)) 543 | if !assert.NoError(t, err) { 544 | return 545 | } 546 | 547 | workingDir, err := os.Getwd() 548 | assert.NoError(t, err) 549 | 550 | homeDir := os.Getenv("HOME") 551 | stopGracePeriod := time.Duration(20 * time.Second) 552 | 553 | expectedServiceConfig := types.ServiceConfig{ 554 | Name: "foo", 555 | 556 | CapAdd: []string{"ALL"}, 557 | CapDrop: []string{"NET_ADMIN", "SYS_ADMIN"}, 558 | CgroupParent: "m-executor-abcd", 559 | Command: []string{"bundle", "exec", "thin", "-p", "3000"}, 560 | ContainerName: "my-web-container", 561 | DependsOn: []string{"db", "redis"}, 562 | Deploy: types.DeployConfig{ 563 | Mode: "replicated", 564 | Replicas: uint64Ptr(6), 565 | Labels: map[string]string{"FOO": "BAR"}, 566 | UpdateConfig: &types.UpdateConfig{ 567 | Parallelism: uint64Ptr(3), 568 | Delay: time.Duration(10 * time.Second), 569 | FailureAction: "continue", 570 | Monitor: time.Duration(60 * time.Second), 571 | MaxFailureRatio: 0.3, 572 | }, 573 | Resources: types.Resources{ 574 | Limits: &types.Resource{ 575 | NanoCPUs: "0.001", 576 | MemoryBytes: 50 * 1024 * 1024, 577 | }, 578 | Reservations: &types.Resource{ 579 | NanoCPUs: "0.0001", 580 | MemoryBytes: 20 * 1024 * 1024, 581 | }, 582 | }, 583 | RestartPolicy: &types.RestartPolicy{ 584 | Condition: "on_failure", 585 | Delay: durationPtr(5 * time.Second), 586 | MaxAttempts: uint64Ptr(3), 587 | Window: durationPtr(2 * time.Minute), 588 | }, 589 | Placement: types.Placement{ 590 | Constraints: []string{"node=foo"}, 591 | }, 592 | }, 593 | Devices: []string{"/dev/ttyUSB0:/dev/ttyUSB0"}, 594 | Dns: []string{"8.8.8.8", "9.9.9.9"}, 595 | DnsSearch: []string{"dc1.example.com", "dc2.example.com"}, 596 | DomainName: "foo.com", 597 | Entrypoint: []string{"/code/entrypoint.sh", "-p", "3000"}, 598 | Environment: map[string]string{ 599 | "RACK_ENV": "development", 600 | "SHOW": "true", 601 | "SESSION_SECRET": "", 602 | "FOO": "1", 603 | "BAR": "2", 604 | "BAZ": "3", 605 | }, 606 | Expose: []string{"3000", "8000"}, 607 | ExternalLinks: []string{ 608 | "redis_1", 609 | "project_db_1:mysql", 610 | "project_db_1:postgresql", 611 | }, 612 | ExtraHosts: map[string]string{ 613 | "otherhost": "50.31.209.229", 614 | "somehost": "162.242.195.82", 615 | }, 616 | HealthCheck: &types.HealthCheckConfig{ 617 | Test: []string{ 618 | "CMD-SHELL", 619 | "echo \"hello world\"", 620 | }, 621 | Interval: "10s", 622 | Timeout: "1s", 623 | Retries: uint64Ptr(5), 624 | }, 625 | Hostname: "foo", 626 | Image: "redis", 627 | Ipc: "host", 628 | Labels: map[string]string{ 629 | "com.example.description": "Accounting webapp", 630 | "com.example.number": "42", 631 | "com.example.empty-label": "", 632 | }, 633 | Links: []string{ 634 | "db", 635 | "db:database", 636 | "redis", 637 | }, 638 | Logging: &types.LoggingConfig{ 639 | Driver: "syslog", 640 | Options: map[string]string{ 641 | "syslog-address": "tcp://192.168.0.42:123", 642 | }, 643 | }, 644 | MacAddress: "02:42:ac:11:65:43", 645 | NetworkMode: "container:0cfeab0f748b9a743dc3da582046357c6ef497631c1a016d28d2bf9b4f899f7b", 646 | Networks: map[string]*types.ServiceNetworkConfig{ 647 | "some-network": { 648 | Aliases: []string{"alias1", "alias3"}, 649 | Ipv4Address: "", 650 | Ipv6Address: "", 651 | }, 652 | "other-network": { 653 | Ipv4Address: "172.16.238.10", 654 | Ipv6Address: "2001:3984:3989::10", 655 | }, 656 | "other-other-network": nil, 657 | }, 658 | Pid: "host", 659 | Ports: []string{ 660 | "3000", 661 | "3000-3005", 662 | "8000:8000", 663 | "9090-9091:8080-8081", 664 | "49100:22", 665 | "127.0.0.1:8001:8001", 666 | "127.0.0.1:5000-5010:5000-5010", 667 | }, 668 | Privileged: true, 669 | ReadOnly: true, 670 | Restart: "always", 671 | SecurityOpt: []string{ 672 | "label=level:s0:c100,c200", 673 | "label=type:svirt_apache_t", 674 | }, 675 | StdinOpen: true, 676 | StopSignal: "SIGUSR1", 677 | StopGracePeriod: &stopGracePeriod, 678 | Tmpfs: []string{"/run", "/tmp"}, 679 | Tty: true, 680 | Ulimits: map[string]*types.UlimitsConfig{ 681 | "nproc": { 682 | Single: 65535, 683 | }, 684 | "nofile": { 685 | Soft: 20000, 686 | Hard: 40000, 687 | }, 688 | }, 689 | User: "someone", 690 | Volumes: []string{ 691 | "/var/lib/mysql", 692 | "/opt/data:/var/lib/mysql", 693 | fmt.Sprintf("%s:/code", workingDir), 694 | fmt.Sprintf("%s/static:/var/www/html", workingDir), 695 | fmt.Sprintf("%s/configs:/etc/configs/:ro", homeDir), 696 | "datavolume:/var/lib/mysql", 697 | }, 698 | WorkingDir: "/code", 699 | } 700 | 701 | assert.Equal(t, []types.ServiceConfig{expectedServiceConfig}, config.Services) 702 | 703 | expectedNetworkConfig := map[string]types.NetworkConfig{ 704 | "some-network": {}, 705 | 706 | "other-network": { 707 | Driver: "overlay", 708 | DriverOpts: map[string]string{ 709 | "foo": "bar", 710 | "baz": "1", 711 | }, 712 | Ipam: types.IPAMConfig{ 713 | Driver: "overlay", 714 | Config: []*types.IPAMPool{ 715 | {Subnet: "172.16.238.0/24"}, 716 | {Subnet: "2001:3984:3989::/64"}, 717 | }, 718 | }, 719 | }, 720 | 721 | "external-network": { 722 | External: types.External{ 723 | Name: "external-network", 724 | External: true, 725 | }, 726 | }, 727 | 728 | "other-external-network": { 729 | External: types.External{ 730 | Name: "my-cool-network", 731 | External: true, 732 | }, 733 | }, 734 | } 735 | 736 | assert.Equal(t, expectedNetworkConfig, config.Networks) 737 | 738 | expectedVolumeConfig := map[string]types.VolumeConfig{ 739 | "some-volume": {}, 740 | "other-volume": { 741 | Driver: "flocker", 742 | DriverOpts: map[string]string{ 743 | "foo": "bar", 744 | "baz": "1", 745 | }, 746 | }, 747 | "external-volume": { 748 | External: types.External{ 749 | Name: "external-volume", 750 | External: true, 751 | }, 752 | }, 753 | "other-external-volume": { 754 | External: types.External{ 755 | Name: "my-cool-volume", 756 | External: true, 757 | }, 758 | }, 759 | } 760 | 761 | assert.Equal(t, expectedVolumeConfig, config.Volumes) 762 | } 763 | 764 | func loadYAML(yaml string) (*types.Config, error) { 765 | dict, err := ParseYAML([]byte(yaml)) 766 | if err != nil { 767 | return nil, err 768 | } 769 | 770 | return Load(buildConfigDetails(dict)) 771 | } 772 | 773 | func serviceSort(services []types.ServiceConfig) []types.ServiceConfig { 774 | sort.Sort(servicesByName(services)) 775 | return services 776 | } 777 | 778 | type servicesByName []types.ServiceConfig 779 | 780 | func (sbn servicesByName) Len() int { return len(sbn) } 781 | func (sbn servicesByName) Swap(i, j int) { sbn[i], sbn[j] = sbn[j], sbn[i] } 782 | func (sbn servicesByName) Less(i, j int) bool { return sbn[i].Name < sbn[j].Name } 783 | -------------------------------------------------------------------------------- /schema/bindata.go: -------------------------------------------------------------------------------- 1 | // Code generated by go-bindata. 2 | // sources: 3 | // data/config_schema_v3.0.json 4 | // DO NOT EDIT! 5 | 6 | package schema 7 | 8 | import ( 9 | "bytes" 10 | "compress/gzip" 11 | "fmt" 12 | "io" 13 | "io/ioutil" 14 | "os" 15 | "path/filepath" 16 | "strings" 17 | "time" 18 | ) 19 | 20 | func bindataRead(data []byte, name string) ([]byte, error) { 21 | gz, err := gzip.NewReader(bytes.NewBuffer(data)) 22 | if err != nil { 23 | return nil, fmt.Errorf("Read %q: %v", name, err) 24 | } 25 | 26 | var buf bytes.Buffer 27 | _, err = io.Copy(&buf, gz) 28 | clErr := gz.Close() 29 | 30 | if err != nil { 31 | return nil, fmt.Errorf("Read %q: %v", name, err) 32 | } 33 | if clErr != nil { 34 | return nil, err 35 | } 36 | 37 | return buf.Bytes(), nil 38 | } 39 | 40 | type asset struct { 41 | bytes []byte 42 | info os.FileInfo 43 | } 44 | 45 | type bindataFileInfo struct { 46 | name string 47 | size int64 48 | mode os.FileMode 49 | modTime time.Time 50 | } 51 | 52 | func (fi bindataFileInfo) Name() string { 53 | return fi.name 54 | } 55 | func (fi bindataFileInfo) Size() int64 { 56 | return fi.size 57 | } 58 | func (fi bindataFileInfo) Mode() os.FileMode { 59 | return fi.mode 60 | } 61 | func (fi bindataFileInfo) ModTime() time.Time { 62 | return fi.modTime 63 | } 64 | func (fi bindataFileInfo) IsDir() bool { 65 | return false 66 | } 67 | func (fi bindataFileInfo) Sys() interface{} { 68 | return nil 69 | } 70 | 71 | var _dataConfig_schema_v30Json = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xec\x5a\x4d\x93\xdb\x28\x13\xbe\xfb\x57\x4c\x29\xb9\xc5\x33\x93\xaa\x37\xf5\x56\x6d\x6e\x7b\xdc\xd3\xee\x79\x5d\x8a\x0a\x4b\xd8\x26\x23\x09\x02\xc8\x89\x93\xf2\x7f\x5f\x10\x92\x0c\x88\x2f\xdb\x4a\x66\x0f\x3b\x87\xa9\x19\xe8\x6e\xfa\xe3\xa1\x69\x1a\xfd\x58\x3d\x3c\x64\x6f\x59\x79\x80\x0d\xc8\x3e\x3e\x64\x07\xce\xc9\xc7\xe7\xe7\xcf\x0c\xb7\x8f\x6a\xf4\x09\xd3\xfd\x73\x45\xc1\x8e\x3f\xbe\xff\xf0\xac\xc6\xde\x64\x6b\xc9\x87\x2a\xc9\x52\xe2\x76\x87\xf6\x85\x9a\x29\x8e\xff\x7b\x7a\xff\x24\xd9\x15\x09\x3f\x11\x28\x89\xf0\xf6\x33\x2c\xb9\x1a\xa3\xf0\x4b\x87\x28\x94\xcc\x9b\xec\x08\x29\x43\x82\x3a\x5f\xaf\xe4\x1c\xa1\x98\x40\xca\x11\x64\x62\x56\x2a\x27\xc6\x46\x92\x71\x40\x13\xcb\x38\x45\xed\x3e\xeb\x87\xcf\xbd\x04\x31\xc9\x20\x3d\xa2\x52\x93\x30\xa9\xfa\xe6\xf9\x22\xff\x79\x22\x5b\xdb\x52\x35\x65\xfb\x71\x02\x38\x87\xb4\xfd\x6b\xae\x5b\x3f\xfd\x69\x03\x1e\xbf\xff\xfe\xf8\xf7\xfb\xc7\xdf\x9e\x8a\xc7\xfc\xdd\x5b\x63\x5a\xfa\x97\xc2\x9d\x5a\xbe\x82\x3b\xd4\x22\x2e\xac\x99\xd6\xcf\x26\xca\xf3\xf0\xd7\x79\x5a\x18\x54\x55\x4f\x0c\x6a\x63\xed\x1d\xa8\x19\x34\x6d\x6e\x21\xff\x8a\xe9\x4b\xcc\xe6\x89\xec\x95\x6c\x1e\xd6\x77\xd8\x6c\x9a\x73\xc4\x75\xd7\x44\x23\x38\x52\xbd\x92\x31\x6a\xf9\xfb\xe2\xb7\x1a\x8d\x0e\xd2\x2a\x0a\x6d\xed\x5e\x41\x03\xed\x2e\x57\xb9\xd0\xe6\xf7\xd5\xe4\x2c\x8f\x97\x2a\x48\x6a\x7c\x92\x63\x1e\x7f\x28\x82\x06\xb6\x3c\x9b\x5c\x20\xf8\xb6\x1d\xaa\x2b\xdb\xa3\xb8\x85\x7f\x4a\x11\x1b\x6d\xf0\x41\x48\xb6\x36\xb6\x26\xa7\x9f\x37\xfe\xf3\x07\x7c\x9a\xf7\xd8\x32\xcd\x8b\xdc\xc5\xe1\x37\xde\x1b\x15\x5e\x5a\xb9\x00\x97\x2f\x90\xee\x50\x0d\x53\x39\x00\xdd\xb3\x80\xcb\x6a\xc4\x78\x81\x69\x51\x21\xa1\xfd\xd9\x62\x9f\xc9\x8b\xe3\x69\x62\xd5\xfe\xcb\x57\x0e\x81\x59\x09\x48\x21\xc4\x19\x76\x00\x4a\xc1\x29\x5b\x0b\x00\x71\xd8\x30\xb7\x89\x0f\x59\xd7\xa2\x2f\x1d\xfc\x63\x20\xe1\xb4\x83\xb6\xdc\x4a\x28\xb7\xbc\xe0\x3d\xc5\x1d\x29\x08\xa0\x12\x60\x61\xf7\x8b\xb8\x36\x0d\x68\x97\x42\xdd\x35\x76\x24\x78\x5e\x60\x0e\xa0\x16\xd2\xa2\x05\x4d\x0c\x48\x72\xd7\xc1\xb6\x62\x85\x3a\xff\x82\x30\xda\x15\x8a\x9f\x59\x02\xa6\xc3\x70\xd1\x78\x54\x6d\x08\xd8\x4a\x8c\x84\xb6\xd4\x2d\xb3\x18\x0b\x06\x01\x2d\x0f\x37\xf2\xe3\x46\xb8\x2f\xc5\x77\x02\x28\xf4\x44\x30\x52\x78\xf9\xd7\x01\x01\xb6\xc7\x62\xca\x25\x57\xbb\x41\x70\x23\x8a\xdb\x66\xdc\x0d\x29\x09\x66\x4a\xf2\x92\xff\x1b\xc1\x0c\xda\x8e\xb1\x0c\xd4\xa7\x26\x53\x0d\x9f\x8c\x1c\x9b\xd1\x70\xe1\x94\xb6\x6b\xb6\x90\xca\x92\xce\xa0\xdc\x61\xda\x00\xa9\xec\xb8\xb6\x36\x6d\x78\xda\x81\x3c\xdd\x81\xba\x0d\xf2\x58\x07\xb5\xf0\x4e\xfb\xb2\x3c\xc4\x85\x78\x0a\x8a\x03\x66\x3c\x3d\x87\x6b\xec\x07\x08\x6a\x7e\x10\x65\x71\xf9\x12\x60\xd7\xa9\x0c\x6e\xb1\x6c\x0a\xc8\x51\x03\xf6\x71\x22\x52\xc6\x48\x6a\xb0\x85\xf5\x4d\x76\x2e\xea\x7c\x4d\x2c\xde\xef\x25\xa9\x0f\x71\xb3\xca\x65\x98\x8e\x9d\xf9\x15\x45\xe2\x46\x91\x7a\x80\x63\x72\x29\xb8\xec\xc9\x78\x01\xa2\x14\x0a\x56\x9f\x06\xe9\xa7\x27\x55\x7c\x06\x76\x55\xff\x57\x5d\x67\xb9\x5d\x2e\xc8\x9f\xf9\x98\x39\x62\x59\x98\x56\x50\x18\x51\x69\x40\x29\xeb\x06\x0a\x99\x27\xae\x17\xd2\xa1\xd8\x2f\x1a\x5c\xf9\x00\x3a\x23\xb6\x7d\xe3\xcd\xd4\x57\x1f\x84\x3d\xdb\xd5\xf5\x63\x52\xe8\xa2\x17\x88\x88\x35\x3e\xf5\x52\xd5\xbc\xa8\x1b\x87\x58\x4f\x07\x6a\x04\x18\x8c\x6f\x76\xaf\x23\x0d\x69\x88\x1c\x3f\x24\x62\xc2\xc5\xfb\xff\x20\xaf\x87\xd5\x2b\x33\xbd\x46\x8e\x88\xba\xa8\xd2\x6f\x37\x97\x22\x79\x64\xb7\xfd\xe4\x12\x9e\xa0\xca\x9f\x2b\xfa\x0c\xa1\x6f\x30\x82\x29\x9f\xed\xae\x5f\x73\xdc\xab\xa5\xef\x3e\xed\x89\x48\xdc\xa2\x5c\xda\x43\xf3\xd6\xb2\xc5\xb8\x86\xa0\x35\x52\x0f\x85\xa0\x12\x25\x73\x7d\x4a\xa0\x64\x1c\xd0\xe8\x85\x82\xc1\xb2\xa3\x88\x9f\x0a\x71\x1e\x2c\x5e\x67\xb0\x43\x53\x30\xf4\x1d\x9a\xd1\xbc\xe4\xfb\x41\x50\x6e\xf0\xf0\x0a\xb5\x42\x1b\xd8\x46\x4d\x64\x1c\x13\x21\x7f\x2f\x30\x17\x35\x53\x92\xee\x29\x28\x61\x21\xb0\x89\x70\xe5\x62\x58\xeb\xb1\xad\x3a\x0a\x24\x9e\x0d\x31\xbc\x21\xbb\x1b\x6f\x07\x9c\xc7\x63\xd6\xd5\xa8\x41\x7e\x30\x3b\xb2\x64\x42\x22\x57\x49\xdc\x9d\xbb\x03\x79\xfb\xa2\xa9\xb8\x66\x08\x6c\x52\x57\xba\x0b\x94\x0e\xe1\xca\x21\xa1\x64\x38\x00\x6a\x46\x29\xa0\x47\xcf\xc0\xf0\x8e\xbb\x19\x5c\x05\x85\x53\x2f\xa3\x83\xdb\xcb\x5b\x0f\x8a\xe4\x4e\xfa\xab\x72\xb2\xad\x46\xee\x4d\x8b\x67\x67\x5a\xec\x58\xb4\xba\xd3\xfb\x8b\x8b\xee\x64\x59\xc2\x48\x64\x57\xc8\xad\xc2\xca\x52\xf7\x8a\x0e\xaf\x75\x9b\x18\x05\xb8\x7a\x7d\x3a\xa9\xdd\xef\xdb\x4c\x80\x1b\x4f\x89\x4b\x97\xd4\xd3\xf8\x93\xf8\xa0\x47\x23\x79\xb8\x7c\xca\x51\x03\x71\xc7\x23\x54\x14\x8a\x31\xcb\xf3\x43\xa6\x33\x84\x89\xb4\x9c\x5a\x0a\xfe\xd2\x4b\x7b\x85\x18\xd8\x5a\xfd\xbf\x29\x47\xdd\x14\x5e\x25\xf6\xd2\x3b\x8d\x04\x57\xa3\x5c\x20\xb6\x81\xda\x5c\x0b\x19\xa9\x51\x09\x58\x2c\xcb\xdc\x71\x85\xec\x48\x05\x38\x2c\xd4\x53\xd2\x55\x79\x3d\x90\xd0\x09\xa0\xa0\xae\xa1\x58\xb4\x49\x49\x90\x22\x06\x35\x38\xdd\x74\xe0\xf5\xec\x3b\x80\xea\x8e\xc2\x02\x94\x7c\x78\xad\x8a\x20\x53\x38\x5f\x38\x06\x3b\x33\x45\xda\x92\x0d\xf8\x56\x8c\xcb\xf6\x24\xce\x6d\xe5\x2d\xbc\x52\x6f\x7f\x1a\x12\x18\xee\x68\x39\x73\xf6\xcd\x21\xba\x1c\xe4\x1e\xc4\x8c\x2b\xce\x4c\x17\x13\x32\x29\x4d\x97\xf3\x28\x7f\xf4\xdc\x18\x2a\xc1\x82\x60\x81\xf6\xd3\x52\x16\x0a\x48\x2b\x27\xa7\x00\xe2\x4e\x04\x4a\x38\xc8\x3a\xa7\x21\x3c\xba\x59\x7b\x86\xaf\xa8\xad\xf0\xd7\x2b\x16\x5c\x0e\x4a\xa4\x16\x45\xa6\x95\xef\xee\x75\xb4\xd0\x1d\x08\x53\xaf\x3e\xd6\xef\x35\xeb\x8e\x53\x7d\xc2\x67\x24\xeb\x4f\x74\xf1\xb7\x4e\x4f\xa6\x2f\x49\x17\xed\xd8\x34\xb0\xc1\xd4\x09\xc0\x80\x8d\x89\x4f\xd3\x31\x0b\x47\xb2\x05\x0e\xb5\xa4\x0e\xdf\x40\x25\x2f\x74\x8b\xdf\x24\xe2\x5d\xbc\x3c\x9e\x8f\x10\x01\xcd\x52\x9b\x23\xb9\xe7\x99\x39\x8f\x60\x63\xed\x79\xaf\x40\xa9\xeb\xec\x17\xc4\xb4\x8e\xeb\x3e\x50\xb0\x6e\x2b\x10\x12\x82\xe6\xe5\xc7\xf9\x10\x9b\x7e\x05\x39\xfb\x2f\x1c\xf7\xe5\xbc\xf1\xb9\xc2\x13\xd5\xcd\x54\x48\xae\x27\x5f\xe5\xc9\x21\xf6\xbe\x15\x2c\xa7\xff\x95\xf5\xdd\x1d\x69\x71\xf8\xb4\x22\x92\x32\x06\xaa\xff\x32\xc6\x20\xe5\xf5\xf1\x15\x38\x13\x6f\xbc\x1c\x5c\x01\x1a\xab\xab\xa4\x81\x67\x7e\x73\x0c\xc5\x39\xb9\x27\x3e\x70\xe4\xa6\x1a\x36\xd9\xc7\xf9\x67\x6b\x66\x0a\x0d\x35\x1c\x46\x12\x4f\x8f\xd4\x5a\x74\x70\x5e\xd8\xf2\x05\x61\xfb\xf4\x2e\x70\x50\x84\xde\xae\x7e\x52\x86\x5d\xa0\x99\xe3\x8e\xa9\x55\x5c\x8e\xde\x9d\x7f\x7b\xe5\xc9\x54\x1a\xff\xec\x4b\x2c\x69\x67\x7b\x9a\x75\x36\x7e\x98\x5d\x36\xf5\x15\x55\x6e\xf8\xc7\x22\x51\x2f\xc1\x5a\x9e\xc8\xf5\x7a\xdb\x17\x46\xe7\xf7\x59\x76\x8f\x6f\xfc\x4e\x2a\x0f\x6f\xf6\xd5\xf8\xfb\xbc\x3a\xaf\xfe\x09\x00\x00\xff\xff\x37\x89\x5b\xf1\x5b\x2a\x00\x00") 72 | 73 | func dataConfig_schema_v30JsonBytes() ([]byte, error) { 74 | return bindataRead( 75 | _dataConfig_schema_v30Json, 76 | "data/config_schema_v3.0.json", 77 | ) 78 | } 79 | 80 | func dataConfig_schema_v30Json() (*asset, error) { 81 | bytes, err := dataConfig_schema_v30JsonBytes() 82 | if err != nil { 83 | return nil, err 84 | } 85 | 86 | info := bindataFileInfo{name: "data/config_schema_v3.0.json", size: 10843, mode: os.FileMode(420), modTime: time.Unix(1479392593, 0)} 87 | a := &asset{bytes: bytes, info: info} 88 | return a, nil 89 | } 90 | 91 | // Asset loads and returns the asset for the given name. 92 | // It returns an error if the asset could not be found or 93 | // could not be loaded. 94 | func Asset(name string) ([]byte, error) { 95 | cannonicalName := strings.Replace(name, "\\", "/", -1) 96 | if f, ok := _bindata[cannonicalName]; ok { 97 | a, err := f() 98 | if err != nil { 99 | return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err) 100 | } 101 | return a.bytes, nil 102 | } 103 | return nil, fmt.Errorf("Asset %s not found", name) 104 | } 105 | 106 | // MustAsset is like Asset but panics when Asset would return an error. 107 | // It simplifies safe initialization of global variables. 108 | func MustAsset(name string) []byte { 109 | a, err := Asset(name) 110 | if err != nil { 111 | panic("asset: Asset(" + name + "): " + err.Error()) 112 | } 113 | 114 | return a 115 | } 116 | 117 | // AssetInfo loads and returns the asset info for the given name. 118 | // It returns an error if the asset could not be found or 119 | // could not be loaded. 120 | func AssetInfo(name string) (os.FileInfo, error) { 121 | cannonicalName := strings.Replace(name, "\\", "/", -1) 122 | if f, ok := _bindata[cannonicalName]; ok { 123 | a, err := f() 124 | if err != nil { 125 | return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err) 126 | } 127 | return a.info, nil 128 | } 129 | return nil, fmt.Errorf("AssetInfo %s not found", name) 130 | } 131 | 132 | // AssetNames returns the names of the assets. 133 | func AssetNames() []string { 134 | names := make([]string, 0, len(_bindata)) 135 | for name := range _bindata { 136 | names = append(names, name) 137 | } 138 | return names 139 | } 140 | 141 | // _bindata is a table, holding each asset generator, mapped to its name. 142 | var _bindata = map[string]func() (*asset, error){ 143 | "data/config_schema_v3.0.json": dataConfig_schema_v30Json, 144 | } 145 | 146 | // AssetDir returns the file names below a certain 147 | // directory embedded in the file by go-bindata. 148 | // For example if you run go-bindata on data/... and data contains the 149 | // following hierarchy: 150 | // data/ 151 | // foo.txt 152 | // img/ 153 | // a.png 154 | // b.png 155 | // then AssetDir("data") would return []string{"foo.txt", "img"} 156 | // AssetDir("data/img") would return []string{"a.png", "b.png"} 157 | // AssetDir("foo.txt") and AssetDir("notexist") would return an error 158 | // AssetDir("") will return []string{"data"}. 159 | func AssetDir(name string) ([]string, error) { 160 | node := _bintree 161 | if len(name) != 0 { 162 | cannonicalName := strings.Replace(name, "\\", "/", -1) 163 | pathList := strings.Split(cannonicalName, "/") 164 | for _, p := range pathList { 165 | node = node.Children[p] 166 | if node == nil { 167 | return nil, fmt.Errorf("Asset %s not found", name) 168 | } 169 | } 170 | } 171 | if node.Func != nil { 172 | return nil, fmt.Errorf("Asset %s not found", name) 173 | } 174 | rv := make([]string, 0, len(node.Children)) 175 | for childName := range node.Children { 176 | rv = append(rv, childName) 177 | } 178 | return rv, nil 179 | } 180 | 181 | type bintree struct { 182 | Func func() (*asset, error) 183 | Children map[string]*bintree 184 | } 185 | var _bintree = &bintree{nil, map[string]*bintree{ 186 | "data": &bintree{nil, map[string]*bintree{ 187 | "config_schema_v3.0.json": &bintree{dataConfig_schema_v30Json, map[string]*bintree{}}, 188 | }}, 189 | }} 190 | 191 | // RestoreAsset restores an asset under the given directory 192 | func RestoreAsset(dir, name string) error { 193 | data, err := Asset(name) 194 | if err != nil { 195 | return err 196 | } 197 | info, err := AssetInfo(name) 198 | if err != nil { 199 | return err 200 | } 201 | err = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755)) 202 | if err != nil { 203 | return err 204 | } 205 | err = ioutil.WriteFile(_filePath(dir, name), data, info.Mode()) 206 | if err != nil { 207 | return err 208 | } 209 | err = os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime()) 210 | if err != nil { 211 | return err 212 | } 213 | return nil 214 | } 215 | 216 | // RestoreAssets restores an asset under the given directory recursively 217 | func RestoreAssets(dir, name string) error { 218 | children, err := AssetDir(name) 219 | // File 220 | if err != nil { 221 | return RestoreAsset(dir, name) 222 | } 223 | // Dir 224 | for _, child := range children { 225 | err = RestoreAssets(dir, filepath.Join(name, child)) 226 | if err != nil { 227 | return err 228 | } 229 | } 230 | return nil 231 | } 232 | 233 | func _filePath(dir, name string) string { 234 | cannonicalName := strings.Replace(name, "\\", "/", -1) 235 | return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...) 236 | } 237 | 238 | -------------------------------------------------------------------------------- /schema/data/config_schema_v3.0.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "id": "config_schema_v3.0.json", 4 | "type": "object", 5 | "required": ["version"], 6 | 7 | "properties": { 8 | "version": { 9 | "type": "string" 10 | }, 11 | 12 | "services": { 13 | "id": "#/properties/services", 14 | "type": "object", 15 | "patternProperties": { 16 | "^[a-zA-Z0-9._-]+$": { 17 | "$ref": "#/definitions/service" 18 | } 19 | }, 20 | "additionalProperties": false 21 | }, 22 | 23 | "networks": { 24 | "id": "#/properties/networks", 25 | "type": "object", 26 | "patternProperties": { 27 | "^[a-zA-Z0-9._-]+$": { 28 | "$ref": "#/definitions/network" 29 | } 30 | } 31 | }, 32 | 33 | "volumes": { 34 | "id": "#/properties/volumes", 35 | "type": "object", 36 | "patternProperties": { 37 | "^[a-zA-Z0-9._-]+$": { 38 | "$ref": "#/definitions/volume" 39 | } 40 | }, 41 | "additionalProperties": false 42 | } 43 | }, 44 | 45 | "additionalProperties": false, 46 | 47 | "definitions": { 48 | 49 | "service": { 50 | "id": "#/definitions/service", 51 | "type": "object", 52 | 53 | "properties": { 54 | "deploy": {"$ref": "#/definitions/deployment"}, 55 | "build": { 56 | "oneOf": [ 57 | {"type": "string"}, 58 | { 59 | "type": "object", 60 | "properties": { 61 | "context": {"type": "string"}, 62 | "dockerfile": {"type": "string"}, 63 | "args": {"$ref": "#/definitions/list_or_dict"} 64 | }, 65 | "additionalProperties": false 66 | } 67 | ] 68 | }, 69 | "cap_add": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, 70 | "cap_drop": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, 71 | "cgroup_parent": {"type": "string"}, 72 | "command": { 73 | "oneOf": [ 74 | {"type": "string"}, 75 | {"type": "array", "items": {"type": "string"}} 76 | ] 77 | }, 78 | "container_name": {"type": "string"}, 79 | "depends_on": {"$ref": "#/definitions/list_of_strings"}, 80 | "devices": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, 81 | "dns": {"$ref": "#/definitions/string_or_list"}, 82 | "dns_search": {"$ref": "#/definitions/string_or_list"}, 83 | "domainname": {"type": "string"}, 84 | "entrypoint": { 85 | "oneOf": [ 86 | {"type": "string"}, 87 | {"type": "array", "items": {"type": "string"}} 88 | ] 89 | }, 90 | "env_file": {"$ref": "#/definitions/string_or_list"}, 91 | "environment": {"$ref": "#/definitions/list_or_dict"}, 92 | 93 | "expose": { 94 | "type": "array", 95 | "items": { 96 | "type": ["string", "number"], 97 | "format": "expose" 98 | }, 99 | "uniqueItems": true 100 | }, 101 | 102 | "external_links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, 103 | "extra_hosts": {"$ref": "#/definitions/list_or_dict"}, 104 | "healthcheck": {"$ref": "#/definitions/healthcheck"}, 105 | "hostname": {"type": "string"}, 106 | "image": {"type": "string"}, 107 | "ipc": {"type": "string"}, 108 | "labels": {"$ref": "#/definitions/list_or_dict"}, 109 | "links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, 110 | 111 | "logging": { 112 | "type": "object", 113 | 114 | "properties": { 115 | "driver": {"type": "string"}, 116 | "options": { 117 | "type": "object", 118 | "patternProperties": { 119 | "^.+$": {"type": ["string", "number", "null"]} 120 | } 121 | } 122 | }, 123 | "additionalProperties": false 124 | }, 125 | 126 | "mac_address": {"type": "string"}, 127 | "network_mode": {"type": "string"}, 128 | 129 | "networks": { 130 | "oneOf": [ 131 | {"$ref": "#/definitions/list_of_strings"}, 132 | { 133 | "type": "object", 134 | "patternProperties": { 135 | "^[a-zA-Z0-9._-]+$": { 136 | "oneOf": [ 137 | { 138 | "type": "object", 139 | "properties": { 140 | "aliases": {"$ref": "#/definitions/list_of_strings"}, 141 | "ipv4_address": {"type": "string"}, 142 | "ipv6_address": {"type": "string"} 143 | }, 144 | "additionalProperties": false 145 | }, 146 | {"type": "null"} 147 | ] 148 | } 149 | }, 150 | "additionalProperties": false 151 | } 152 | ] 153 | }, 154 | "pid": {"type": ["string", "null"]}, 155 | 156 | "ports": { 157 | "type": "array", 158 | "items": { 159 | "type": ["string", "number"], 160 | "format": "ports" 161 | }, 162 | "uniqueItems": true 163 | }, 164 | 165 | "privileged": {"type": "boolean"}, 166 | "read_only": {"type": "boolean"}, 167 | "restart": {"type": "string"}, 168 | "security_opt": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, 169 | "shm_size": {"type": ["number", "string"]}, 170 | "sysctls": {"$ref": "#/definitions/list_or_dict"}, 171 | "stdin_open": {"type": "boolean"}, 172 | "stop_signal": {"type": "string"}, 173 | "stop_grace_period": {"type": "string", "format": "duration"}, 174 | "tmpfs": {"$ref": "#/definitions/string_or_list"}, 175 | "tty": {"type": "boolean"}, 176 | "ulimits": { 177 | "type": "object", 178 | "patternProperties": { 179 | "^[a-z]+$": { 180 | "oneOf": [ 181 | {"type": "integer"}, 182 | { 183 | "type":"object", 184 | "properties": { 185 | "hard": {"type": "integer"}, 186 | "soft": {"type": "integer"} 187 | }, 188 | "required": ["soft", "hard"], 189 | "additionalProperties": false 190 | } 191 | ] 192 | } 193 | } 194 | }, 195 | "user": {"type": "string"}, 196 | "userns_mode": {"type": "string"}, 197 | "volumes": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, 198 | "working_dir": {"type": "string"} 199 | }, 200 | "additionalProperties": false 201 | }, 202 | 203 | "healthcheck": { 204 | "id": "#/definitions/healthcheck", 205 | "type": ["object", "null"], 206 | "properties": { 207 | "interval": {"type":"string"}, 208 | "timeout": {"type":"string"}, 209 | "retries": {"type": "number"}, 210 | "test": { 211 | "oneOf": [ 212 | {"type": "string"}, 213 | {"type": "array", "items": {"type": "string"}} 214 | ] 215 | }, 216 | "disable": {"type": "boolean"} 217 | }, 218 | "additionalProperties": false 219 | }, 220 | "deployment": { 221 | "id": "#/definitions/deployment", 222 | "type": ["object", "null"], 223 | "properties": { 224 | "mode": {"type": "string"}, 225 | "replicas": {"type": "integer"}, 226 | "labels": {"$ref": "#/definitions/list_or_dict"}, 227 | "update_config": { 228 | "type": "object", 229 | "properties": { 230 | "parallelism": {"type": "integer"}, 231 | "delay": {"type": "string", "format": "duration"}, 232 | "failure_action": {"type": "string"}, 233 | "monitor": {"type": "string", "format": "duration"}, 234 | "max_failure_ratio": {"type": "number"} 235 | }, 236 | "additionalProperties": false 237 | }, 238 | "resources": { 239 | "type": "object", 240 | "properties": { 241 | "limits": {"$ref": "#/definitions/resource"}, 242 | "reservations": {"$ref": "#/definitions/resource"} 243 | } 244 | }, 245 | "restart_policy": { 246 | "type": "object", 247 | "properties": { 248 | "condition": {"type": "string"}, 249 | "delay": {"type": "string", "format": "duration"}, 250 | "max_attempts": {"type": "integer"}, 251 | "window": {"type": "string", "format": "duration"} 252 | }, 253 | "additionalProperties": false 254 | }, 255 | "placement": { 256 | "type": "object", 257 | "properties": { 258 | "constraints": {"type": "array", "items": {"type": "string"}} 259 | }, 260 | "additionalProperties": false 261 | } 262 | }, 263 | "additionalProperties": false 264 | }, 265 | 266 | "resource": { 267 | "id": "#/definitions/resource", 268 | "type": "object", 269 | "properties": { 270 | "cpus": {"type": "string"}, 271 | "memory": {"type": "string"} 272 | }, 273 | "additionalProperties": false 274 | }, 275 | 276 | "network": { 277 | "id": "#/definitions/network", 278 | "type": ["object", "null"], 279 | "properties": { 280 | "driver": {"type": "string"}, 281 | "driver_opts": { 282 | "type": "object", 283 | "patternProperties": { 284 | "^.+$": {"type": ["string", "number"]} 285 | } 286 | }, 287 | "ipam": { 288 | "type": "object", 289 | "properties": { 290 | "driver": {"type": "string"}, 291 | "config": { 292 | "type": "array", 293 | "items": { 294 | "type": "object", 295 | "properties": { 296 | "subnet": {"type": "string"} 297 | }, 298 | "additionalProperties": false 299 | } 300 | } 301 | }, 302 | "additionalProperties": false 303 | }, 304 | "external": { 305 | "type": ["boolean", "object"], 306 | "properties": { 307 | "name": {"type": "string"} 308 | }, 309 | "additionalProperties": false 310 | }, 311 | "labels": {"$ref": "#/definitions/list_or_dict"} 312 | }, 313 | "additionalProperties": false 314 | }, 315 | 316 | "volume": { 317 | "id": "#/definitions/volume", 318 | "type": ["object", "null"], 319 | "properties": { 320 | "driver": {"type": "string"}, 321 | "driver_opts": { 322 | "type": "object", 323 | "patternProperties": { 324 | "^.+$": {"type": ["string", "number"]} 325 | } 326 | }, 327 | "external": { 328 | "type": ["boolean", "object"], 329 | "properties": { 330 | "name": {"type": "string"} 331 | } 332 | } 333 | }, 334 | "labels": {"$ref": "#/definitions/list_or_dict"}, 335 | "additionalProperties": false 336 | }, 337 | 338 | "string_or_list": { 339 | "oneOf": [ 340 | {"type": "string"}, 341 | {"$ref": "#/definitions/list_of_strings"} 342 | ] 343 | }, 344 | 345 | "list_of_strings": { 346 | "type": "array", 347 | "items": {"type": "string"}, 348 | "uniqueItems": true 349 | }, 350 | 351 | "list_or_dict": { 352 | "oneOf": [ 353 | { 354 | "type": "object", 355 | "patternProperties": { 356 | ".+": { 357 | "type": ["string", "number", "null"] 358 | } 359 | }, 360 | "additionalProperties": false 361 | }, 362 | {"type": "array", "items": {"type": "string"}, "uniqueItems": true} 363 | ] 364 | }, 365 | 366 | "constraints": { 367 | "service": { 368 | "id": "#/definitions/constraints/service", 369 | "anyOf": [ 370 | {"required": ["build"]}, 371 | {"required": ["image"]} 372 | ], 373 | "properties": { 374 | "build": { 375 | "required": ["context"] 376 | } 377 | } 378 | } 379 | } 380 | } 381 | } 382 | -------------------------------------------------------------------------------- /schema/schema.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | //go:generate go-bindata -pkg schema data 4 | 5 | import ( 6 | "fmt" 7 | "strings" 8 | "time" 9 | 10 | "github.com/xeipuuv/gojsonschema" 11 | ) 12 | 13 | type portsFormatChecker struct{} 14 | 15 | func (checker portsFormatChecker) IsFormat(input string) bool { 16 | // TODO: implement this 17 | return true 18 | } 19 | 20 | type durationFormatChecker struct{} 21 | 22 | func (checker durationFormatChecker) IsFormat(input string) bool { 23 | _, err := time.ParseDuration(input) 24 | return err == nil 25 | } 26 | 27 | func init() { 28 | gojsonschema.FormatCheckers.Add("expose", portsFormatChecker{}) 29 | gojsonschema.FormatCheckers.Add("ports", portsFormatChecker{}) 30 | gojsonschema.FormatCheckers.Add("duration", durationFormatChecker{}) 31 | } 32 | 33 | // Validate uses the jsonschema to validate the configuration 34 | func Validate(config map[string]interface{}) error { 35 | schemaData, err := Asset("data/config_schema_v3.0.json") 36 | if err != nil { 37 | return err 38 | } 39 | 40 | schemaLoader := gojsonschema.NewStringLoader(string(schemaData)) 41 | dataLoader := gojsonschema.NewGoLoader(config) 42 | 43 | result, err := gojsonschema.Validate(schemaLoader, dataLoader) 44 | if err != nil { 45 | return err 46 | } 47 | 48 | if !result.Valid() { 49 | return toError(result) 50 | } 51 | 52 | return nil 53 | } 54 | 55 | func toError(result *gojsonschema.Result) error { 56 | err := getMostSpecificError(result.Errors()) 57 | description := getDescription(err) 58 | return fmt.Errorf("%s %s", err.Field(), description) 59 | } 60 | 61 | func getDescription(err gojsonschema.ResultError) string { 62 | if err.Type() == "invalid_type" { 63 | if expectedType, ok := err.Details()["expected"].(string); ok { 64 | return fmt.Sprintf("must be a %s", humanReadableType(expectedType)) 65 | } 66 | } 67 | 68 | return err.Description() 69 | } 70 | 71 | func humanReadableType(definition string) string { 72 | if definition[0:1] == "[" { 73 | allTypes := strings.Split(definition[1:len(definition)-1], ",") 74 | for i, t := range allTypes { 75 | allTypes[i] = humanReadableType(t) 76 | } 77 | return fmt.Sprintf( 78 | "%s or %s", 79 | strings.Join(allTypes[0:len(allTypes)-1], ", "), 80 | allTypes[len(allTypes)-1], 81 | ) 82 | } 83 | if definition == "object" { 84 | return "mapping" 85 | } 86 | if definition == "array" { 87 | return "list" 88 | } 89 | return definition 90 | } 91 | 92 | func getMostSpecificError(errors []gojsonschema.ResultError) gojsonschema.ResultError { 93 | var mostSpecificError gojsonschema.ResultError 94 | 95 | for _, err := range errors { 96 | if mostSpecificError == nil { 97 | mostSpecificError = err 98 | } else if specificity(err) > specificity(mostSpecificError) { 99 | mostSpecificError = err 100 | } else if specificity(err) == specificity(mostSpecificError) { 101 | // Invalid type errors win in a tie-breaker for most specific field name 102 | if err.Type() == "invalid_type" && mostSpecificError.Type() != "invalid_type" { 103 | mostSpecificError = err 104 | } 105 | } 106 | } 107 | 108 | return mostSpecificError 109 | } 110 | 111 | func specificity(err gojsonschema.ResultError) int { 112 | return len(strings.Split(err.Field(), ".")) 113 | } 114 | -------------------------------------------------------------------------------- /schema/schema_test.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | type dict map[string]interface{} 10 | 11 | func TestValid(t *testing.T) { 12 | config := dict{ 13 | "version": "2.1", 14 | "services": dict{ 15 | "foo": dict{ 16 | "image": "busybox", 17 | }, 18 | }, 19 | } 20 | 21 | assert.NoError(t, Validate(config)) 22 | } 23 | 24 | func TestUndefinedTopLevelOption(t *testing.T) { 25 | config := dict{ 26 | "version": "2.1", 27 | "helicopters": dict{ 28 | "foo": dict{ 29 | "image": "busybox", 30 | }, 31 | }, 32 | } 33 | 34 | assert.Error(t, Validate(config)) 35 | } 36 | -------------------------------------------------------------------------------- /script/watch: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | exec filewatcher \ 4 | -x '.git' \ 5 | -x 'vendor' \ 6 | -x '**/*.swp' \ 7 | -x 'dist' \ 8 | -x 'script' \ 9 | -x 'docs' \ 10 | -- \ 11 | bash -c 'go test -v ./${dir} || ( echo; echo; echo; echo; exit 1 )' 12 | -------------------------------------------------------------------------------- /template/template.go: -------------------------------------------------------------------------------- 1 | package template 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strings" 7 | ) 8 | 9 | var delimiter = "\\$" 10 | var substitution = "[_a-z][_a-z0-9]*(?::?-[^}]+)?" 11 | 12 | var patternString = fmt.Sprintf( 13 | "%s(?i:(?P%s)|(?P%s)|{(?P%s)}|(?P))", 14 | delimiter, delimiter, substitution, substitution, 15 | ) 16 | 17 | var pattern = regexp.MustCompile(patternString) 18 | 19 | type InvalidTemplateError struct { 20 | Template string 21 | } 22 | 23 | func (e InvalidTemplateError) Error() string { 24 | return fmt.Sprintf("Invalid template: %#v", e.Template) 25 | } 26 | 27 | // A user-supplied function which maps from variable names to values. 28 | // Returns the value as a string and a bool indicating whether 29 | // the value is present, to distinguish between an empty string 30 | // and the absence of a value. 31 | type Mapping func(string) (string, bool) 32 | 33 | func Substitute(template string, mapping Mapping) (result string, err *InvalidTemplateError) { 34 | defer func() { 35 | if r := recover(); r != nil { 36 | if e, ok := r.(*InvalidTemplateError); ok { 37 | err = e 38 | } else { 39 | panic(r) 40 | } 41 | } 42 | }() 43 | 44 | result = pattern.ReplaceAllStringFunc(template, func(substring string) string { 45 | matches := pattern.FindStringSubmatch(substring) 46 | groups := make(map[string]string) 47 | for i, name := range pattern.SubexpNames() { 48 | if i != 0 { 49 | groups[name] = matches[i] 50 | } 51 | } 52 | 53 | substitution := groups["named"] 54 | if substitution == "" { 55 | substitution = groups["braced"] 56 | } 57 | if substitution != "" { 58 | // Soft default (fall back if unset or empty) 59 | if strings.Contains(substitution, ":-") { 60 | name, defaultValue := partition(substitution, ":-") 61 | value, ok := mapping(name) 62 | if !ok || value == "" { 63 | return defaultValue 64 | } 65 | return value 66 | } 67 | 68 | // Hard default (fall back if-and-only-if empty) 69 | if strings.Contains(substitution, "-") { 70 | name, defaultValue := partition(substitution, "-") 71 | value, ok := mapping(name) 72 | if !ok { 73 | return defaultValue 74 | } 75 | return value 76 | } 77 | 78 | // No default (fall back to empty string) 79 | value, ok := mapping(substitution) 80 | if !ok { 81 | return "" 82 | } 83 | return value 84 | } 85 | 86 | if escaped := groups["escaped"]; escaped != "" { 87 | return escaped 88 | } 89 | 90 | panic(&InvalidTemplateError{Template: template}) 91 | return "" 92 | }) 93 | 94 | return 95 | } 96 | 97 | // Split the string at the first occurrence of sep, and return the part before the separator, 98 | // and the part after the separator. 99 | // 100 | // If the separator is not found, return the string itself, followed by an empty string. 101 | func partition(s, sep string) (string, string) { 102 | if strings.Contains(s, sep) { 103 | parts := strings.SplitN(s, sep, 2) 104 | return parts[0], parts[1] 105 | } else { 106 | return s, "" 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /template/template_test.go: -------------------------------------------------------------------------------- 1 | package template 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | var defaults = map[string]string{ 10 | "FOO": "first", 11 | "BAR": "", 12 | } 13 | 14 | func defaultMapping(name string) (string, bool) { 15 | val, ok := defaults[name] 16 | return val, ok 17 | } 18 | 19 | func TestEscaped(t *testing.T) { 20 | result, err := Substitute("$${foo}", defaultMapping) 21 | assert.NoError(t, err) 22 | assert.Equal(t, "${foo}", result) 23 | } 24 | 25 | func TestInvalid(t *testing.T) { 26 | invalidTemplates := []string{ 27 | "${", 28 | "$}", 29 | "${}", 30 | "${ }", 31 | "${ foo}", 32 | "${foo }", 33 | "${foo!}", 34 | } 35 | 36 | for _, template := range invalidTemplates { 37 | _, err := Substitute(template, defaultMapping) 38 | assert.Error(t, err) 39 | assert.IsType(t, &InvalidTemplateError{}, err) 40 | } 41 | } 42 | 43 | func TestNoValueNoDefault(t *testing.T) { 44 | for _, template := range []string{"This ${missing} var", "This ${BAR} var"} { 45 | result, err := Substitute(template, defaultMapping) 46 | assert.NoError(t, err) 47 | assert.Equal(t, "This var", result) 48 | } 49 | } 50 | 51 | func TestValueNoDefault(t *testing.T) { 52 | for _, template := range []string{"This $FOO var", "This ${FOO} var"} { 53 | result, err := Substitute(template, defaultMapping) 54 | assert.NoError(t, err) 55 | assert.Equal(t, "This first var", result) 56 | } 57 | } 58 | 59 | func TestNoValueWithDefault(t *testing.T) { 60 | for _, template := range []string{"ok ${missing:-def}", "ok ${missing-def}"} { 61 | result, err := Substitute(template, defaultMapping) 62 | assert.NoError(t, err) 63 | assert.Equal(t, "ok def", result) 64 | } 65 | } 66 | 67 | func TestEmptyValueWithSoftDefault(t *testing.T) { 68 | result, err := Substitute("ok ${BAR:-def}", defaultMapping) 69 | assert.NoError(t, err) 70 | assert.Equal(t, "ok def", result) 71 | } 72 | 73 | func TestEmptyValueWithHardDefault(t *testing.T) { 74 | result, err := Substitute("ok ${BAR-def}", defaultMapping) 75 | assert.NoError(t, err) 76 | assert.Equal(t, "ok ", result) 77 | } 78 | 79 | func TestNonAlphanumericDefault(t *testing.T) { 80 | result, err := Substitute("ok ${BAR:-/non:-alphanumeric}", defaultMapping) 81 | assert.NoError(t, err) 82 | assert.Equal(t, "ok /non:-alphanumeric", result) 83 | } 84 | -------------------------------------------------------------------------------- /types/types.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | var UnsupportedProperties = []string{ 8 | "build", 9 | "cap_add", 10 | "cap_drop", 11 | "cgroup_parent", 12 | "devices", 13 | "dns", 14 | "dns_search", 15 | "domainname", 16 | "external_links", 17 | "ipc", 18 | "links", 19 | "mac_address", 20 | "network_mode", 21 | "privileged", 22 | "read_only", 23 | "restart", 24 | "security_opt", 25 | "shm_size", 26 | "stop_signal", 27 | "sysctls", 28 | "tmpfs", 29 | "userns_mode", 30 | } 31 | 32 | var DeprecatedProperties = map[string]string{ 33 | "container_name": "Setting the container name is not supported.", 34 | "expose": "Exposing ports is unnecessary - services on the same network can access each other's containers on any port.", 35 | } 36 | 37 | var ForbiddenProperties = map[string]string{ 38 | "extends": "`extends` is not supported.", 39 | "volume_driver": "Instead of setting the volume driver on the service, define a volume using the top-level `volumes` option and specify the driver there.", 40 | "volumes_from": "To share a volume between services, define it using the top-level `volumes` option and reference it from each service that shares it using the service-level `volumes` option.", 41 | "cpu_quota": "Set resource limits using deploy.resources", 42 | "cpu_shares": "Set resource limits using deploy.resources", 43 | "cpuset": "Set resource limits using deploy.resources", 44 | "mem_limit": "Set resource limits using deploy.resources", 45 | "memswap_limit": "Set resource limits using deploy.resources", 46 | } 47 | 48 | type Dict map[string]interface{} 49 | 50 | type ConfigFile struct { 51 | Filename string 52 | Config Dict 53 | } 54 | 55 | type ConfigDetails struct { 56 | WorkingDir string 57 | ConfigFiles []ConfigFile 58 | Environment map[string]string 59 | } 60 | 61 | type Config struct { 62 | Services []ServiceConfig 63 | Networks map[string]NetworkConfig 64 | Volumes map[string]VolumeConfig 65 | } 66 | 67 | type ServiceConfig struct { 68 | Name string 69 | 70 | CapAdd []string `mapstructure:"cap_add"` 71 | CapDrop []string `mapstructure:"cap_drop"` 72 | CgroupParent string `mapstructure:"cgroup_parent"` 73 | Command []string `compose:"shell_command"` 74 | ContainerName string `mapstructure:"container_name"` 75 | DependsOn []string `mapstructure:"depends_on"` 76 | Deploy DeployConfig 77 | Devices []string 78 | Dns []string `compose:"string_or_list"` 79 | DnsSearch []string `mapstructure:"dns_search" compose:"string_or_list"` 80 | DomainName string `mapstructure:"domainname"` 81 | Entrypoint []string `compose:"shell_command"` 82 | Environment map[string]string `compose:"list_or_dict_equals"` 83 | Expose []string `compose:"list_of_strings_or_numbers"` 84 | ExternalLinks []string `mapstructure:"external_links"` 85 | ExtraHosts map[string]string `mapstructure:"extra_hosts" compose:"list_or_dict_colon"` 86 | Hostname string 87 | HealthCheck *HealthCheckConfig 88 | Image string 89 | Ipc string 90 | Labels map[string]string `compose:"list_or_dict_equals"` 91 | Links []string 92 | Logging *LoggingConfig 93 | MacAddress string `mapstructure:"mac_address"` 94 | NetworkMode string `mapstructure:"network_mode"` 95 | Networks map[string]*ServiceNetworkConfig `compose:"list_or_struct_map"` 96 | Pid string 97 | Ports []string `compose:"list_of_strings_or_numbers"` 98 | Privileged bool 99 | ReadOnly bool `mapstructure:"read_only"` 100 | Restart string 101 | SecurityOpt []string `mapstructure:"security_opt"` 102 | StdinOpen bool `mapstructure:"stdin_open"` 103 | StopGracePeriod *time.Duration `mapstructure:"stop_grace_period"` 104 | StopSignal string `mapstructure:"stop_signal"` 105 | Tmpfs []string `compose:"string_or_list"` 106 | Tty bool `mapstructure:"tty"` 107 | Ulimits map[string]*UlimitsConfig 108 | User string 109 | Volumes []string 110 | WorkingDir string `mapstructure:"working_dir"` 111 | } 112 | 113 | type LoggingConfig struct { 114 | Driver string 115 | Options map[string]string 116 | } 117 | 118 | type DeployConfig struct { 119 | Mode string 120 | Replicas *uint64 121 | Labels map[string]string `compose:"list_or_dict_equals"` 122 | UpdateConfig *UpdateConfig `mapstructure:"update_config"` 123 | Resources Resources 124 | RestartPolicy *RestartPolicy `mapstructure:"restart_policy"` 125 | Placement Placement 126 | } 127 | 128 | type HealthCheckConfig struct { 129 | Test []string `compose:"healthcheck"` 130 | Timeout string 131 | Interval string 132 | Retries *uint64 133 | Disable bool 134 | } 135 | 136 | type UpdateConfig struct { 137 | Parallelism *uint64 138 | Delay time.Duration 139 | FailureAction string `mapstructure:"failure_action"` 140 | Monitor time.Duration 141 | MaxFailureRatio float32 `mapstructure:"max_failure_ratio"` 142 | } 143 | 144 | type Resources struct { 145 | Limits *Resource 146 | Reservations *Resource 147 | } 148 | 149 | type Resource struct { 150 | // TODO: types to convert from units and ratios 151 | NanoCPUs string `mapstructure:"cpus"` 152 | MemoryBytes UnitBytes `mapstructure:"memory"` 153 | } 154 | 155 | type UnitBytes int64 156 | 157 | type RestartPolicy struct { 158 | Condition string 159 | Delay *time.Duration 160 | MaxAttempts *uint64 `mapstructure:"max_attempts"` 161 | Window *time.Duration 162 | } 163 | 164 | type Placement struct { 165 | Constraints []string 166 | } 167 | 168 | type ServiceNetworkConfig struct { 169 | Aliases []string 170 | Ipv4Address string `mapstructure:"ipv4_address"` 171 | Ipv6Address string `mapstructure:"ipv6_address"` 172 | } 173 | 174 | type UlimitsConfig struct { 175 | Single int 176 | Soft int 177 | Hard int 178 | } 179 | 180 | type NetworkConfig struct { 181 | Driver string 182 | DriverOpts map[string]string `mapstructure:"driver_opts"` 183 | Ipam IPAMConfig 184 | External External 185 | Labels map[string]string `compose:"list_or_dict_equals"` 186 | } 187 | 188 | type IPAMConfig struct { 189 | Driver string 190 | Config []*IPAMPool 191 | } 192 | 193 | type IPAMPool struct { 194 | Subnet string 195 | } 196 | 197 | type VolumeConfig struct { 198 | Driver string 199 | DriverOpts map[string]string `mapstructure:"driver_opts"` 200 | External External 201 | Labels map[string]string `compose:"list_or_dict_equals"` 202 | } 203 | 204 | // External identifies a Volume or Network as a reference to a resource that is 205 | // not managed, and should already exist. 206 | type External struct { 207 | Name string 208 | External bool 209 | } 210 | --------------------------------------------------------------------------------