├── .gitattributes ├── .github ├── CODEOWNERS ├── dependabot.yml └── workflows │ ├── ci.yml │ ├── release.yml │ └── stale.yml ├── .gitignore ├── .golangci.yml ├── .pre-commit-config.yaml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── MAINTENANCE.md ├── Makefile ├── NOTICE ├── README.md ├── ci └── Dockerfile ├── cli ├── options.go ├── options_test.go ├── options_windows_test.go └── testdata │ ├── UNNORMALIZED PATH │ └── compose.yaml │ ├── env-file │ ├── .env │ ├── compose-with-env-file.yaml │ ├── compose-with-env-files.yaml │ ├── override.env │ ├── second-env │ └── simple-env │ └── simple │ ├── .env │ ├── compose-name.yaml │ ├── compose-with-overrides.yaml │ ├── compose-with-paths.yaml │ ├── compose-with-variables.yaml │ └── compose.yaml ├── cmd └── main.go ├── consts └── consts.go ├── dotenv ├── LICENSE ├── env.go ├── fixtures │ ├── custom.format │ ├── equals.env │ ├── exported.env │ ├── inherited-multi-var.env │ ├── inherited-not-found.env │ ├── inherited-single-var.env │ ├── invalid1.env │ ├── plain.env │ ├── quoted.env │ ├── special.env │ ├── substitutions-default.env │ ├── substitutions.env │ ├── unquoted.env │ └── utf8-bom.env ├── format.go ├── godotenv.go ├── godotenv_test.go ├── godotenv_var_expansion_test.go ├── parser.go └── parser_test.go ├── errdefs └── errors.go ├── format ├── volume.go └── volume_test.go ├── go.mod ├── go.sum ├── graph ├── cycle.go ├── graph.go ├── graph_test.go ├── services.go └── traversal.go ├── interpolation ├── interpolation.go └── interpolation_test.go ├── loader ├── environment.go ├── example1.env ├── example1.label ├── example2.env ├── example2.label ├── extends.go ├── extends_test.go ├── fix.go ├── full-example.yml ├── full-struct_test.go ├── include.go ├── include_test.go ├── interpolate.go ├── loader.go ├── loader_test.go ├── loader_windows_test.go ├── loader_yaml_test.go ├── mapstructure.go ├── mapstructure_test.go ├── merge_reset_test.go ├── normalize.go ├── normalize_test.go ├── omitEmpty.go ├── override_test.go ├── paths.go ├── paths_test.go ├── reset.go ├── reset_test.go ├── testdata │ ├── Dockerfile │ ├── combined │ │ ├── compose.yaml │ │ └── dir │ │ │ ├── extended.yaml │ │ │ └── included.yaml │ ├── compose-depends-on-cycle.yaml │ ├── compose-depends-on-profile-no-cycle.yaml │ ├── compose-depends-on-self.yaml │ ├── compose-include-cycle.yaml │ ├── compose-include.yaml │ ├── compose-test-extends-with-context-url-imported.yaml │ ├── compose-test-extends-with-context-url.yaml │ ├── compose-test-extends.yaml │ ├── compose-test-with-version.yaml │ ├── empty.yaml │ ├── extends │ │ ├── base.yaml │ │ ├── depends_on.yaml │ │ ├── interpolated.yaml │ │ ├── nested.yaml │ │ ├── ports.yaml │ │ ├── reset.yaml │ │ ├── sibling.yaml │ │ └── withdir │ │ │ ├── compose.yaml │ │ │ └── dir │ │ │ ├── base.yaml │ │ │ └── compose.yaml │ ├── include │ │ ├── compose.yaml │ │ ├── dir │ │ │ └── compose.yaml │ │ ├── included.yaml │ │ └── project-directory.yaml │ ├── remote │ │ ├── compose.yaml │ │ ├── cycle │ │ │ ├── compose-cycle.yaml │ │ │ └── compose.yaml │ │ ├── env │ │ └── nested │ │ │ ├── compose-nested.yaml │ │ │ └── compose.yaml │ └── subdir │ │ ├── compose-test-extends-imported.yaml │ │ └── extra.env ├── types_test.go ├── validate.go ├── validate_test.go └── with-version-struct_test.go ├── override ├── extends.go ├── merge.go ├── merge_annotations_test.go ├── merge_build_test.go ├── merge_cap_test.go ├── merge_devices_test.go ├── merge_dns_test.go ├── merge_env_file_test.go ├── merge_environment_test.go ├── merge_extra_hosts_test.go ├── merge_labels_test.go ├── merge_links_test.go ├── merge_logging_test.go ├── merge_mem_test.go ├── merge_mounts_test.go ├── merge_networks_test.go ├── merge_profiles_test.go ├── merge_sysctls_test.go ├── merge_test.go ├── merge_tmpfs_test.go ├── merge_ulimits_test.go ├── merge_volumes_test.go ├── uncity.go └── uncity_test.go ├── package.go ├── parsing.md ├── paths ├── context.go ├── extends.go ├── home.go ├── resolve.go ├── unix.go ├── windows_path.go └── windows_path_test.go ├── schema ├── compose-spec.json ├── schema.go ├── schema_test.go └── using-variables.yaml ├── scripts └── validate │ └── template │ ├── bash.txt │ ├── dockerfile.txt │ ├── go.txt │ └── makefile.txt ├── template ├── template.go ├── template_test.go ├── variables.go └── variables_test.go ├── transform ├── build.go ├── build_test.go ├── canonical.go ├── defaults.go ├── dependson.go ├── device.go ├── devices.go ├── envfile.go ├── envfile_test.go ├── extends.go ├── external.go ├── external_test.go ├── gpus.go ├── include.go ├── mapping.go ├── ports.go ├── ports_test.go ├── secrets.go ├── services.go ├── ssh.go ├── ssh_test.go ├── ulimits.go └── volume.go ├── tree ├── path.go └── path_test.go ├── types ├── bytes.go ├── command.go ├── config.go ├── config_test.go ├── cpus.go ├── derived.gen.go ├── develop.go ├── device.go ├── duration.go ├── envfile.go ├── fixtures │ ├── base.env │ └── override.env ├── healthcheck.go ├── hooks.go ├── hostList.go ├── hostList_test.go ├── labels.go ├── labels_test.go ├── mapping.go ├── options.go ├── project.go ├── project_test.go ├── services.go ├── ssh.go ├── stringOrList.go ├── types.go └── types_test.go ├── utils ├── collectionutils.go ├── pathutils.go ├── set.go ├── set_test.go └── stringutils.go └── validation ├── external.go ├── validation.go ├── validation_test.go └── volume.go /.gitattributes: -------------------------------------------------------------------------------- 1 | # required for golangci-lint on Windows 2 | *.go text eol=lf 3 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @ndeloof 2 | 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: "/" 5 | schedule: 6 | interval: weekly 7 | - package-ecosystem: gomod 8 | directory: "/" 9 | schedule: 10 | interval: weekly 11 | # compose-go is a library, so to maximize compatibility for downstream 12 | # users with go's minimal version selection for dependencies we should 13 | # ignore version bumps and only update when there are security updates 14 | open-pull-requests-limit: 0 15 | ignore: 16 | - dependency-name: github.com/sirupsen/logrus 17 | versions: 18 | - 1.8.0 19 | - 1.8.1 20 | - dependency-name: github.com/google/go-cmp 21 | versions: 22 | - 0.5.5 23 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | name: Continuous integration 3 | 4 | permissions: 5 | contents: read 6 | 7 | jobs: 8 | validate: 9 | name: validate 10 | runs-on: ubuntu-latest 11 | timeout-minutes: 5 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | - name: Check license 16 | run: DOCKER_BUILDKIT=1 make check-license 17 | - name: Check deepcopy 18 | run: | 19 | make deepcopy 20 | if [ -n "$(git status --porcelain)" ]; then 21 | echo >&2 'ERROR: DeepCopy is not up to date with code. Please run "make deepcopy"' 22 | fi 23 | 24 | test: 25 | strategy: 26 | matrix: 27 | go-version: ['1.23', '1.24'] 28 | platform: [ubuntu-latest, macos-latest, windows-latest] 29 | runs-on: ${{ matrix.platform }} 30 | timeout-minutes: 10 31 | steps: 32 | - name: Checkout code 33 | uses: actions/checkout@v4 34 | - name: Install Go 35 | uses: actions/setup-go@v5 36 | with: 37 | go-version: ${{ matrix.go-version }} 38 | check-latest: true 39 | cache: true 40 | - uses: golangci/golangci-lint-action@v8 41 | with: 42 | version: v2.1.6 43 | args: --verbose 44 | skip-cache: true 45 | - name: Test 46 | run: go test ./... 47 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | tags: 5 | - 'v*' 6 | workflow_dispatch: 7 | 8 | jobs: 9 | binary: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | os: [linux, darwin, windows] 14 | arch: [amd64, arm64] 15 | steps: 16 | - 17 | name: Checkout 18 | uses: actions/checkout@v4 19 | - 20 | name: Setup Go 21 | uses: actions/setup-go@v5 22 | with: 23 | go-version-file: go.mod 24 | - run: go version 25 | - 26 | name: Build 27 | run: | 28 | GOOS=${{ matrix.os }} GOARCH=${{ matrix.arch }} make build 29 | ls -al bin/ 30 | - 31 | name: Upload artifacts 32 | uses: actions/upload-artifact@v4 33 | with: 34 | name: compose-spec-${{ matrix.os }}-${{ matrix.arch }} 35 | path: ./bin/* 36 | if-no-files-found: error 37 | 38 | release: 39 | permissions: 40 | contents: write # to create a release (ncipollo/release-action) 41 | runs-on: ubuntu-latest 42 | needs: 43 | - binary 44 | steps: 45 | - 46 | name: Checkout 47 | uses: actions/checkout@v4 48 | - 49 | name: Download artifacts 50 | uses: actions/download-artifact@v4 51 | with: 52 | path: bin/ 53 | merge-multiple: true 54 | - 55 | name: GitHub Release 56 | uses: ncipollo/release-action@v1 57 | with: 58 | artifacts: bin/* 59 | generateReleaseNotes: true 60 | draft: true 61 | token: ${{ secrets.GITHUB_TOKEN }} 62 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: 'Close stale issues' 2 | 3 | # Default to 'contents: read', which grants actions to read commits. 4 | # 5 | # If any permission is set, any permission not included in the list is 6 | # implicitly set to "none". 7 | # 8 | # see https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions 9 | permissions: 10 | contents: read 11 | 12 | on: 13 | schedule: 14 | - cron: '0 0 * * 0,3' # at midnight UTC every Sunday and Wednesday 15 | jobs: 16 | stale: 17 | runs-on: ubuntu-latest 18 | permissions: 19 | issues: write 20 | pull-requests: write 21 | steps: 22 | - uses: actions/stale@v9 23 | with: 24 | repo-token: ${{ secrets.GITHUB_TOKEN }} 25 | stale-issue-message: > 26 | This issue has been automatically marked as stale because it has not had 27 | recent activity. It will be closed if no further activity occurs. Thank you 28 | for your contributions. 29 | days-before-issue-stale: 90 # marks stale after 3 months 30 | days-before-issue-close: 30 # closes 1 month after being marked with no action 31 | stale-issue-label: "stale" 32 | exempt-issue-labels: "Proposal 💡" 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### IDEs ### 2 | .idea/* 3 | .vscode/* 4 | bin/ 5 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | linters: 3 | default: none 4 | enable: 5 | - copyloopvar 6 | - depguard 7 | - errcheck 8 | - errorlint 9 | - gocritic 10 | - gocyclo 11 | - gomodguard 12 | - govet 13 | - ineffassign 14 | - lll 15 | - misspell 16 | - nakedret 17 | - nolintlint 18 | - revive 19 | - staticcheck 20 | - testifylint 21 | - unconvert 22 | - unparam 23 | - unused 24 | settings: 25 | depguard: 26 | rules: 27 | all: 28 | deny: 29 | - pkg: gopkg.in/yaml.v2 30 | desc: compose-go uses yaml.v3 31 | gocritic: 32 | disabled-checks: 33 | - paramTypeCombine 34 | - unnamedResult 35 | - whyNoLint 36 | enabled-tags: 37 | - diagnostic 38 | - opinionated 39 | - style 40 | gomodguard: 41 | blocked: 42 | modules: 43 | - github.com/pkg/errors: 44 | recommendations: 45 | - errors 46 | - fmt 47 | lll: 48 | line-length: 200 49 | testifylint: 50 | enable: 51 | - bool-compare 52 | - compares 53 | - empty 54 | - error-is-as 55 | - error-nil 56 | - expected-actual 57 | - len 58 | - require-error 59 | - suite-dont-use-pkg 60 | - suite-extra-assert-call 61 | disable: 62 | - float-compare 63 | - go-require 64 | exclusions: 65 | generated: lax 66 | presets: 67 | - comments 68 | - common-false-positives 69 | - legacy 70 | - std-error-handling 71 | paths: 72 | - third_party$ 73 | - builtin$ 74 | - examples$ 75 | - paths/windows_path.go 76 | 77 | issues: 78 | max-issues-per-linter: 0 79 | max-same-issues: 0 80 | formatters: 81 | enable: 82 | - gofumpt 83 | - goimports 84 | exclusions: 85 | generated: lax 86 | paths: 87 | - third_party$ 88 | - builtin$ 89 | - examples$ 90 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v2.4.0 4 | hooks: 5 | - id: check-yaml 6 | exclude: '^vendor/' 7 | - id: end-of-file-fixer 8 | exclude: '^vendor/' 9 | - id: trailing-whitespace 10 | exclude: '^vendor/' 11 | - repo: https://github.com/dnephin/pre-commit-golang 12 | rev: v0.3.5 13 | hooks: 14 | - id: go-fmt 15 | - id: golangci-lint 16 | - id: go-imports 17 | -------------------------------------------------------------------------------- /MAINTENANCE.md: -------------------------------------------------------------------------------- 1 | # Maintenance 2 | 3 | The compose-go library has to be kept up-to-date with approved changes in the [Compose specification](https://github.com/compose-spec/compose-spec). 4 | As we define new attributes to be added to the spec, this typically requires: 5 | 6 | 1. Updating `schema` to latest version from compose-spec 7 | 1. Creating the matching struct/field in `types` 8 | 1. Creating the matching `CheckXX` method in `compatibility` 9 | 1. If the new attribute replaces a legacy one we want to deprecate, creating the adequate logic in `normalize.go` -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Copyright 2020 The Compose Specification Authors. 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | IMAGE_PREFIX=composespec/conformance-tests- 16 | 17 | ifeq ($(OS),Windows_NT) 18 | BINARY_EXT=.exe 19 | endif 20 | 21 | .PHONY: build 22 | build: ## Build command line 23 | go build -o bin/compose-spec-$(GOOS)-$(GOARCH)$(BINARY_EXT) cmd/main.go 24 | 25 | .PHONY: test 26 | test: ## Run tests 27 | gotestsum ./... 28 | 29 | .PHONY: fmt 30 | fmt: ## Format go files 31 | go fmt ./... 32 | 33 | .PHONY: deepcopy 34 | deepcopy: build-validate-image 35 | docker run --rm -v .:/go/src $(IMAGE_PREFIX)validate goderive ./types/... 36 | 37 | .PHONY: build-validate-image 38 | build-validate-image: 39 | docker build . -f ci/Dockerfile -t $(IMAGE_PREFIX)validate 40 | 41 | .PHONY: lint 42 | lint: build-validate-image 43 | docker run --rm -v .:/go/src $(IMAGE_PREFIX)validate golangci-lint run --config ./.golangci.yml ./... 44 | 45 | .PHONY: check-license 46 | check-license: build-validate-image 47 | docker run --rm -v .:/go/src $(IMAGE_PREFIX)validate ltag -t scripts/validate/template --excludes "validate dotenv" --check -v 48 | 49 | .PHONY: setup 50 | setup: ## Setup the precommit hook 51 | @which pre-commit > /dev/null 2>&1 || (echo "pre-commit not installed see README." && false) 52 | @pre-commit install 53 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | The Compose Specification 2 | Copyright 2020 The Compose Specification Authors 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # compose-go 2 | [![Continuous integration](https://github.com/compose-spec/compose-go/actions/workflows/ci.yml/badge.svg)](https://github.com/compose-spec/compose-go/actions/workflows/ci.yml) 3 | [![Go Reference](https://pkg.go.dev/badge/github.com/compose-spec/compose-go.svg)](https://pkg.go.dev/github.com/compose-spec/compose-go) 4 | 5 | Go reference library for parsing and loading Compose files as specified by the 6 | [Compose specification](https://github.com/compose-spec/compose-spec). 7 | 8 | ## Usage 9 | 10 | ```go 11 | package main 12 | 13 | import ( 14 | "context" 15 | "fmt" 16 | "log" 17 | 18 | "github.com/compose-spec/compose-go/v2/cli" 19 | ) 20 | 21 | func main() { 22 | composeFilePath := "docker-compose.yml" 23 | projectName := "my_project" 24 | ctx := context.Background() 25 | 26 | options, err := cli.NewProjectOptions( 27 | []string{composeFilePath}, 28 | cli.WithOsEnv, 29 | cli.WithDotEnv, 30 | cli.WithName(projectName), 31 | ) 32 | if err != nil { 33 | log.Fatal(err) 34 | } 35 | 36 | project, err := options.LoadProject(ctx) 37 | if err != nil { 38 | log.Fatal(err) 39 | } 40 | 41 | // Use the MarshalYAML method to get YAML representation 42 | projectYAML, err := project.MarshalYAML() 43 | if err != nil { 44 | log.Fatal(err) 45 | } 46 | 47 | fmt.Println(string(projectYAML)) 48 | } 49 | ``` 50 | 51 | ## Build the library 52 | 53 | To build the library, you could either use the makefile 54 | ```bash 55 | make build 56 | ``` 57 | or use the go build command 58 | ```bash 59 | go build ./... 60 | ``` 61 | 62 | ## Run the tests 63 | You can run the tests with the makefile 64 | ```bash 65 | make test 66 | ``` 67 | or with the go test command 68 | ```bash 69 | gotestsum ./... 70 | ``` 71 | 72 | ## Other helpful make commands 73 | Run the linter 74 | ```bash 75 | make lint 76 | ``` 77 | 78 | Check the license headers 79 | ```bash 80 | make check_license 81 | ``` 82 | 83 | Check the `compose-spec.json` file is sync with the `compose-spec` repository 84 | ```bash 85 | make check_compose_spec 86 | ``` 87 | 88 | ## Used by 89 | 90 | * [compose](https://github.com/docker/compose) 91 | * [containerd/nerdctl](https://github.com/containerd/nerdctl) 92 | * [compose-cli](https://github.com/docker/compose-cli) 93 | * [tilt.dev](https://github.com/tilt-dev/tilt) 94 | * [kompose](https://github.com/kubernetes/kompose) 95 | * [kurtosis](https://github.com/kurtosis-tech/kurtosis/) 96 | * [testcontainers-go's Compose module](https://github.com/testcontainers/testcontainers-go/tree/main/modules/compose) 97 | * [compose2nix](https://github.com/aksiksi/compose2nix) 98 | * [Defang](https://github.com/DefangLabs/defang) 99 | * [score-compose](https://github.com/score-spec/score-compose) 100 | * [CasaOS](https://github.com/IceWhaleTech/CasaOS) 101 | -------------------------------------------------------------------------------- /ci/Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright 2020 The Compose Specification Authors. 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | FROM golang:1.24 16 | 17 | WORKDIR /go/src 18 | 19 | ARG GOLANGCILINT_VERSION=v2.1.6 20 | RUN curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin ${GOLANGCILINT_VERSION} 21 | RUN go install github.com/awalterschulze/goderive@v0.5.1 22 | RUN go install github.com/containerd/ltag@v0.3.0 23 | -------------------------------------------------------------------------------- /cli/options_windows_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cli 18 | 19 | import ( 20 | "context" 21 | "os" 22 | "testing" 23 | 24 | "gotest.tools/v3/assert" 25 | ) 26 | 27 | func TestConvertWithEnvVar(t *testing.T) { 28 | os.Setenv("COMPOSE_CONVERT_WINDOWS_PATHS", "1") 29 | defer os.Unsetenv("COMPOSE_CONVERT_WINDOWS_PATHS") 30 | opts, _ := NewProjectOptions([]string{"testdata/simple/compose-with-paths.yaml"}, 31 | WithOsEnv, 32 | WithWorkingDirectory("C:\\project-dir\\"), 33 | WithResolvedPaths(true)) 34 | 35 | p, err := ProjectFromOptions(context.TODO(), opts) 36 | 37 | assert.NilError(t, err) 38 | volumes := p.Services["test"].Volumes 39 | assert.Equal(t, len(volumes), 3) 40 | assert.Equal(t, volumes[0].Source, "/c/docker/project") 41 | assert.Equal(t, volumes[1].Source, "/c/project-dir/relative") 42 | assert.Equal(t, volumes[2].Source, "/c/project-dir/relative2") 43 | } 44 | -------------------------------------------------------------------------------- /cli/testdata/UNNORMALIZED PATH/compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | nginx: 3 | image: nginx 4 | -------------------------------------------------------------------------------- /cli/testdata/env-file/.env: -------------------------------------------------------------------------------- 1 | COMPOSE_FILE=compose-with-env-file.yaml 2 | COMPOSE_PROJECT_NAME=my_project_from_dot_env 3 | PORT=8000 4 | -------------------------------------------------------------------------------- /cli/testdata/env-file/compose-with-env-file.yaml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | simple: 4 | image: nginx 5 | env_file: 6 | - ./simple-env 7 | ports: 8 | - 8000:80 9 | -------------------------------------------------------------------------------- /cli/testdata/env-file/compose-with-env-files.yaml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | simple: 4 | image: nginx 5 | env_file: 6 | - ./simple-env 7 | - ./second-env 8 | ports: 9 | - ${PORT}:80 10 | -------------------------------------------------------------------------------- /cli/testdata/env-file/override.env: -------------------------------------------------------------------------------- 1 | PORT=9000 2 | -------------------------------------------------------------------------------- /cli/testdata/env-file/second-env: -------------------------------------------------------------------------------- 1 | DEFAULT_PORT=9090 2 | -------------------------------------------------------------------------------- /cli/testdata/env-file/simple-env: -------------------------------------------------------------------------------- 1 | DEFAULT_PORT=8080 2 | -------------------------------------------------------------------------------- /cli/testdata/simple/.env: -------------------------------------------------------------------------------- 1 | PUBLIC_PORT=8000 2 | -------------------------------------------------------------------------------- /cli/testdata/simple/compose-name.yaml: -------------------------------------------------------------------------------- 1 | name: test-${TEST}-test 2 | services: 3 | simple: 4 | image: nginx 5 | -------------------------------------------------------------------------------- /cli/testdata/simple/compose-with-overrides.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | simple: 3 | image: haproxy 4 | -------------------------------------------------------------------------------- /cli/testdata/simple/compose-with-paths.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | test: 3 | image: hello-world 4 | volumes: 5 | - type: bind 6 | source: C:\docker\project 7 | target: /test 8 | - type: bind 9 | source: ./relative 10 | target: /test-relative 11 | - ./relative2:/test-relative2 12 | -------------------------------------------------------------------------------- /cli/testdata/simple/compose-with-variables.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | simple: 3 | image: nginx 4 | ports: 5 | - ${PUBLIC_PORT}:80 6 | -------------------------------------------------------------------------------- /cli/testdata/simple/compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | simple: 3 | image: nginx 4 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "context" 21 | "encoding/json" 22 | "flag" 23 | "fmt" 24 | "os" 25 | 26 | "github.com/compose-spec/compose-go/v2/cli" 27 | "gopkg.in/yaml.v3" 28 | ) 29 | 30 | func main() { 31 | if len(os.Args) == 1 { 32 | fmt.Println(` 33 | Validates a compose file conforms to the Compose Specification 34 | 35 | Usage: compose-spec [OPTIONS] COMPOSE_FILE [COMPOSE_OVERRIDE_FILE]`) 36 | } 37 | 38 | var skipInterpolation, skipResolvePaths, skipNormalization, skipConsistencyCheck bool 39 | var format string 40 | 41 | flag.BoolVar(&skipInterpolation, "no-interpolation", false, "Don't interpolate environment variables.") 42 | flag.BoolVar(&skipResolvePaths, "no-path-resolution", false, "Don't resolve file paths.") 43 | flag.BoolVar(&skipNormalization, "no-normalization", false, "Don't normalize compose model.") 44 | flag.BoolVar(&skipConsistencyCheck, "no-consistency", false, "Don't check model consistency.") 45 | flag.StringVar(&format, "format", "yaml", "Output format (yaml|json).") 46 | flag.Parse() 47 | 48 | wd, err := os.Getwd() 49 | if err != nil { 50 | exitError("can't determine current directory", err) 51 | } 52 | 53 | options, err := cli.NewProjectOptions(flag.Args(), 54 | cli.WithWorkingDirectory(wd), 55 | cli.WithOsEnv, 56 | cli.WithDotEnv, 57 | cli.WithConfigFileEnv, 58 | cli.WithDefaultConfigPath, 59 | cli.WithInterpolation(!skipInterpolation), 60 | cli.WithResolvedPaths(!skipResolvePaths), 61 | cli.WithNormalization(!skipNormalization), 62 | cli.WithConsistency(!skipConsistencyCheck), 63 | ) 64 | if err != nil { 65 | exitError("failed to configure project options", err) 66 | } 67 | 68 | model, err := options.LoadModel(context.Background()) 69 | if err != nil { 70 | exitError("failed to load project", err) 71 | } 72 | 73 | var raw []byte 74 | switch format { 75 | case "yaml": 76 | raw, err = yaml.Marshal(model) 77 | if err != nil { 78 | exitError("failed to marshall project", err) 79 | } 80 | case "json": 81 | raw, err = json.MarshalIndent(model, "", " ") 82 | if err != nil { 83 | exitError("failed to marshall project", err) 84 | } 85 | default: 86 | _ = fmt.Errorf("unsupported output format %s", format) 87 | os.Exit(1) 88 | } 89 | 90 | fmt.Println(string(raw)) 91 | } 92 | 93 | func exitError(message string, err error) { 94 | fmt.Fprintf(os.Stderr, "%s: %v", message, err) 95 | os.Exit(1) 96 | } 97 | -------------------------------------------------------------------------------- /consts/consts.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package consts 18 | 19 | const ( 20 | ComposeProjectName = "COMPOSE_PROJECT_NAME" 21 | ComposePathSeparator = "COMPOSE_PATH_SEPARATOR" 22 | ComposeFilePath = "COMPOSE_FILE" 23 | ComposeDisableDefaultEnvFile = "COMPOSE_DISABLE_ENV_FILE" 24 | ComposeProfiles = "COMPOSE_PROFILES" 25 | ) 26 | 27 | const Extensions = "#extensions" // Using # prefix, we prevent risk to conflict with an actual yaml key 28 | 29 | type ComposeFileKey struct{} 30 | -------------------------------------------------------------------------------- /dotenv/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 John Barton 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /dotenv/env.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package dotenv 18 | 19 | import ( 20 | "bytes" 21 | "fmt" 22 | "os" 23 | "path/filepath" 24 | ) 25 | 26 | func GetEnvFromFile(currentEnv map[string]string, filenames []string) (map[string]string, error) { 27 | envMap := make(map[string]string) 28 | 29 | for _, dotEnvFile := range filenames { 30 | abs, err := filepath.Abs(dotEnvFile) 31 | if err != nil { 32 | return envMap, err 33 | } 34 | dotEnvFile = abs 35 | 36 | s, err := os.Stat(dotEnvFile) 37 | if os.IsNotExist(err) { 38 | return envMap, fmt.Errorf("couldn't find env file: %s", dotEnvFile) 39 | } 40 | if err != nil { 41 | return envMap, err 42 | } 43 | 44 | if s.IsDir() { 45 | if len(filenames) == 0 { 46 | return envMap, nil 47 | } 48 | return envMap, fmt.Errorf("%s is a directory", dotEnvFile) 49 | } 50 | 51 | b, err := os.ReadFile(dotEnvFile) 52 | if os.IsNotExist(err) { 53 | return nil, fmt.Errorf("couldn't read env file: %s", dotEnvFile) 54 | } 55 | if err != nil { 56 | return envMap, err 57 | } 58 | 59 | env, err := ParseWithLookup(bytes.NewReader(b), func(k string) (string, bool) { 60 | v, ok := currentEnv[k] 61 | if ok { 62 | return v, true 63 | } 64 | v, ok = envMap[k] 65 | return v, ok 66 | }) 67 | if err != nil { 68 | return envMap, fmt.Errorf("failed to read %s: %w", dotEnvFile, err) 69 | } 70 | for k, v := range env { 71 | envMap[k] = v 72 | } 73 | } 74 | 75 | return envMap, nil 76 | } 77 | -------------------------------------------------------------------------------- /dotenv/fixtures/custom.format: -------------------------------------------------------------------------------- 1 | FOO:BAR 2 | ZOT:QIX -------------------------------------------------------------------------------- /dotenv/fixtures/equals.env: -------------------------------------------------------------------------------- 1 | export OPTION_A='postgres://localhost:5432/database?sslmode=disable' 2 | 3 | -------------------------------------------------------------------------------- /dotenv/fixtures/exported.env: -------------------------------------------------------------------------------- 1 | export OPTION_A=2 2 | export OPTION_B='\n' 3 | -------------------------------------------------------------------------------- /dotenv/fixtures/inherited-multi-var.env: -------------------------------------------------------------------------------- 1 | foo=bar 2 | VAR_TO_BE_LOADED_FROM_OS_ENV 3 | bar=baz 4 | -------------------------------------------------------------------------------- /dotenv/fixtures/inherited-not-found.env: -------------------------------------------------------------------------------- 1 | VARIABLE_NOT_FOUND 2 | -------------------------------------------------------------------------------- /dotenv/fixtures/inherited-single-var.env: -------------------------------------------------------------------------------- 1 | VAR_TO_BE_LOADED_FROM_OS_ENV 2 | -------------------------------------------------------------------------------- /dotenv/fixtures/invalid1.env: -------------------------------------------------------------------------------- 1 | # some comments 2 | foo=" 3 | a 4 | multine 5 | value 6 | " 7 | INVALID LINE 8 | zot=qix -------------------------------------------------------------------------------- /dotenv/fixtures/plain.env: -------------------------------------------------------------------------------- 1 | OPTION_A=1 2 | OPTION_B=2 3 | OPTION_C= 3 4 | OPTION_D =4 5 | OPTION_E = 5 6 | 456 = ABC 7 | OPTION_F = 8 | OPTION_G= 9 | OPTION_H = my string # Inline comment 10 | -------------------------------------------------------------------------------- /dotenv/fixtures/quoted.env: -------------------------------------------------------------------------------- 1 | OPTION_A='1' 2 | OPTION_B='2' 3 | OPTION_C='' 4 | OPTION_D='\n' 5 | OPTION_E="1" 6 | OPTION_F="2" 7 | OPTION_G="" 8 | OPTION_H="\n" 9 | OPTION_I = "echo 'asd'" # Inline comment 10 | OPTION_J = 'first line 11 | second line 12 | third line 13 | and so on' 14 | OPTION_K='Let\'s go!' 15 | OPTION_Z = "last value" 16 | -------------------------------------------------------------------------------- /dotenv/fixtures/special.env: -------------------------------------------------------------------------------- 1 | VAR.WITH.DOTS=dots 2 | VAR_WITH_UNDERSCORES=underscores 3 | VAR-WITH-DASHES=dashes 4 | -------------------------------------------------------------------------------- /dotenv/fixtures/substitutions-default.env: -------------------------------------------------------------------------------- 1 | OPTION_A=${OPTION_A:-1} 2 | OPTION_B=${OPTION_A} 3 | OPTION_C=$OPTION_B 4 | OPTION_D=${OPTION_A}_${OPTION_B} 5 | OPTION_E=${OPTION_NOT_DEFINED} 6 | -------------------------------------------------------------------------------- /dotenv/fixtures/substitutions.env: -------------------------------------------------------------------------------- 1 | OPTION_A=1 2 | OPTION_B=${OPTION_A} 3 | OPTION_C=$OPTION_B 4 | OPTION_D=${OPTION_A}_${OPTION_B} 5 | OPTION_E=${OPTION_NOT_DEFINED} 6 | -------------------------------------------------------------------------------- /dotenv/fixtures/unquoted.env: -------------------------------------------------------------------------------- 1 | OPTION_A = "some quoted phrase" 2 | OPTION_B=first one with an unquoted phrase 3 | OPTION_C = then another one with an unquoted phrase 4 | OPTION_D = then another one with an unquoted phrase special è char 5 | OPTION_E = "then another one quoted phrase" 6 | -------------------------------------------------------------------------------- /dotenv/fixtures/utf8-bom.env: -------------------------------------------------------------------------------- 1 | OPTION_A=1 2 | OPTION_B=2 3 | OPTION_C= 3 4 | OPTION_D =4 5 | OPTION_E = 5 6 | 456 = ABC 7 | OPTION_F = 8 | OPTION_G= 9 | OPTION_H = my string # Inline comment 10 | -------------------------------------------------------------------------------- /dotenv/format.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package dotenv 18 | 19 | import ( 20 | "fmt" 21 | "io" 22 | ) 23 | 24 | const DotEnv = ".env" 25 | 26 | var formats = map[string]Parser{ 27 | DotEnv: func(r io.Reader, filename string, vars map[string]string, lookup func(key string) (string, bool)) error { 28 | err := parseWithLookup(r, vars, lookup) 29 | if err != nil { 30 | return fmt.Errorf("failed to read %s: %w", filename, err) 31 | } 32 | return nil 33 | }, 34 | } 35 | 36 | type Parser func(r io.Reader, filename string, vars map[string]string, lookup func(key string) (string, bool)) error 37 | 38 | func RegisterFormat(format string, p Parser) { 39 | formats[format] = p 40 | } 41 | 42 | func ParseWithFormat(r io.Reader, filename string, vars map[string]string, resolve LookupFn, format string) error { 43 | if format == "" { 44 | format = DotEnv 45 | } 46 | fn, ok := formats[format] 47 | if !ok { 48 | return fmt.Errorf("unsupported env_file format %q", format) 49 | } 50 | return fn(r, filename, vars, resolve) 51 | } 52 | -------------------------------------------------------------------------------- /dotenv/parser_test.go: -------------------------------------------------------------------------------- 1 | package dotenv 2 | 3 | import ( 4 | "fmt" 5 | "runtime" 6 | "strings" 7 | "testing" 8 | 9 | "gotest.tools/v3/assert" 10 | ) 11 | 12 | var testInput = ` 13 | a=b 14 | a[1]=c 15 | a.propertyKey=d 16 | árvíztűrő-TÜKÖRFÚRÓGÉP=ÁRVÍZTŰRŐ-tükörfúrógép 17 | ` 18 | 19 | func TestParseBytes(t *testing.T) { 20 | p := newParser() 21 | 22 | expectedOutput := map[string]string{ 23 | "a": "b", 24 | "a[1]": "c", 25 | "a.propertyKey": "d", 26 | "árvíztűrő-TÜKÖRFÚRÓGÉP": "ÁRVÍZTŰRŐ-tükörfúrógép", 27 | } 28 | 29 | out := map[string]string{} 30 | err := p.parse(testInput, out, nil) 31 | 32 | assert.NilError(t, err) 33 | assert.Equal(t, len(expectedOutput), len(out)) 34 | for key, value := range expectedOutput { 35 | assert.Equal(t, value, out[key]) 36 | } 37 | } 38 | 39 | func TestParseVariable(t *testing.T) { 40 | err := newParser().parse("%!(EXTRA string)=foo", map[string]string{}, nil) 41 | assert.Error(t, err, "line 1: unexpected character \"%\" in variable name \"%!(EXTRA string)=foo\"") 42 | } 43 | 44 | func TestMemoryExplosion(t *testing.T) { 45 | p := newParser() 46 | var startMemStats runtime.MemStats 47 | var endMemStats runtime.MemStats 48 | runtime.ReadMemStats(&startMemStats) 49 | 50 | size := 1000 51 | input := []string{} 52 | for i := 0; i < size; i++ { 53 | input = append(input, fmt.Sprintf("KEY%d=VALUE%d", i, i)) 54 | } 55 | out := map[string]string{} 56 | err := p.parse(strings.Join(input, "\n"), out, nil) 57 | assert.NilError(t, err) 58 | assert.Equal(t, size, len(out)) 59 | runtime.ReadMemStats(&endMemStats) 60 | assert.Assert(t, endMemStats.Alloc-startMemStats.Alloc < uint64(size)*1000, /* assume 1K per line */ 61 | "memory usage should be linear with input size. Memory grew by: %d", 62 | endMemStats.Alloc-startMemStats.Alloc) 63 | } 64 | -------------------------------------------------------------------------------- /errdefs/errors.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package errdefs 18 | 19 | import "errors" 20 | 21 | var ( 22 | // ErrNotFound is returned when an object is not found 23 | ErrNotFound = errors.New("not found") 24 | 25 | // ErrInvalid is returned when a compose project is invalid 26 | ErrInvalid = errors.New("invalid compose project") 27 | 28 | // ErrUnsupported is returned when a compose project uses an unsupported attribute 29 | ErrUnsupported = errors.New("unsupported attribute") 30 | 31 | // ErrIncompatible is returned when a compose project uses an incompatible attribute 32 | ErrIncompatible = errors.New("incompatible attribute") 33 | 34 | // ErrDisabled is returned when a resource was found in model but is disabled 35 | ErrDisabled = errors.New("disabled") 36 | ) 37 | 38 | // IsNotFoundError returns true if the unwrapped error is ErrNotFound 39 | func IsNotFoundError(err error) bool { 40 | return errors.Is(err, ErrNotFound) 41 | } 42 | 43 | // IsInvalidError returns true if the unwrapped error is ErrInvalid 44 | func IsInvalidError(err error) bool { 45 | return errors.Is(err, ErrInvalid) 46 | } 47 | 48 | // IsUnsupportedError returns true if the unwrapped error is ErrUnsupported 49 | func IsUnsupportedError(err error) bool { 50 | return errors.Is(err, ErrUnsupported) 51 | } 52 | 53 | // IsUnsupportedError returns true if the unwrapped error is ErrIncompatible 54 | func IsIncompatibleError(err error) bool { 55 | return errors.Is(err, ErrIncompatible) 56 | } 57 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/compose-spec/compose-go/v2 2 | 3 | go 1.23 4 | 5 | require ( 6 | github.com/distribution/reference v0.5.0 7 | github.com/docker/go-connections v0.4.0 8 | github.com/docker/go-units v0.5.0 9 | github.com/go-viper/mapstructure/v2 v2.0.0 10 | github.com/google/go-cmp v0.5.9 11 | github.com/mattn/go-shellwords v1.0.12 12 | github.com/opencontainers/go-digest v1.0.0 13 | github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 14 | github.com/sirupsen/logrus v1.9.0 15 | github.com/stretchr/testify v1.8.4 16 | github.com/xhit/go-str2duration/v2 v2.1.0 17 | golang.org/x/sync v0.3.0 18 | gopkg.in/yaml.v3 v3.0.1 19 | gotest.tools/v3 v3.4.0 20 | ) 21 | 22 | require ( 23 | github.com/davecgh/go-spew v1.1.1 // indirect 24 | github.com/pmezard/go-difflib v1.0.0 // indirect 25 | golang.org/x/sys v0.5.0 // indirect 26 | golang.org/x/text v0.14.0 // indirect 27 | ) 28 | -------------------------------------------------------------------------------- /graph/cycle.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package graph 18 | 19 | import ( 20 | "fmt" 21 | "slices" 22 | "strings" 23 | 24 | "github.com/compose-spec/compose-go/v2/types" 25 | "github.com/compose-spec/compose-go/v2/utils" 26 | ) 27 | 28 | // CheckCycle analyze project's depends_on relation and report an error on cycle detection 29 | func CheckCycle(project *types.Project) error { 30 | g, err := newGraph(project) 31 | if err != nil { 32 | return err 33 | } 34 | return g.checkCycle() 35 | } 36 | 37 | func (g *graph[T]) checkCycle() error { 38 | // iterate on vertices in a name-order to render a predicable error message 39 | // this is required by tests and enforce command reproducibility by user, which otherwise could be confusing 40 | names := utils.MapKeys(g.vertices) 41 | for _, name := range names { 42 | err := searchCycle([]string{name}, g.vertices[name]) 43 | if err != nil { 44 | return err 45 | } 46 | } 47 | return nil 48 | } 49 | 50 | func searchCycle[T any](path []string, v *vertex[T]) error { 51 | names := utils.MapKeys(v.children) 52 | for _, name := range names { 53 | if i := slices.Index(path, name); i >= 0 { 54 | return fmt.Errorf("dependency cycle detected: %s -> %s", strings.Join(path[i:], " -> "), name) 55 | } 56 | ch := v.children[name] 57 | err := searchCycle(append(path, name), ch) 58 | if err != nil { 59 | return err 60 | } 61 | } 62 | return nil 63 | } 64 | -------------------------------------------------------------------------------- /graph/graph.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package graph 18 | 19 | // graph represents project as service dependencies 20 | type graph[T any] struct { 21 | vertices map[string]*vertex[T] 22 | } 23 | 24 | // vertex represents a service in the dependencies structure 25 | type vertex[T any] struct { 26 | key string 27 | service *T 28 | children map[string]*vertex[T] 29 | parents map[string]*vertex[T] 30 | } 31 | 32 | func (g *graph[T]) addVertex(name string, service T) { 33 | g.vertices[name] = &vertex[T]{ 34 | key: name, 35 | service: &service, 36 | parents: map[string]*vertex[T]{}, 37 | children: map[string]*vertex[T]{}, 38 | } 39 | } 40 | 41 | func (g *graph[T]) addEdge(src, dest string) { 42 | g.vertices[src].children[dest] = g.vertices[dest] 43 | g.vertices[dest].parents[src] = g.vertices[src] 44 | } 45 | 46 | func (g *graph[T]) roots() []*vertex[T] { 47 | var res []*vertex[T] 48 | for _, v := range g.vertices { 49 | if len(v.parents) == 0 { 50 | res = append(res, v) 51 | } 52 | } 53 | return res 54 | } 55 | 56 | func (g *graph[T]) leaves() []*vertex[T] { 57 | var res []*vertex[T] 58 | for _, v := range g.vertices { 59 | if len(v.children) == 0 { 60 | res = append(res, v) 61 | } 62 | } 63 | 64 | return res 65 | } 66 | 67 | // descendents return all descendents for a vertex, might contain duplicates 68 | func (v *vertex[T]) descendents() []string { 69 | var vx []string 70 | for _, n := range v.children { 71 | vx = append(vx, n.key) 72 | vx = append(vx, n.descendents()...) 73 | } 74 | return vx 75 | } 76 | -------------------------------------------------------------------------------- /graph/services.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package graph 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | 23 | "github.com/compose-spec/compose-go/v2/types" 24 | ) 25 | 26 | // InDependencyOrder walk the service graph an invoke VisitorFn in respect to dependency order 27 | func InDependencyOrder(ctx context.Context, project *types.Project, fn VisitorFn[types.ServiceConfig], options ...func(*Options)) error { 28 | _, err := CollectInDependencyOrder[any](ctx, project, func(ctx context.Context, s string, config types.ServiceConfig) (any, error) { 29 | return nil, fn(ctx, s, config) 30 | }, options...) 31 | return err 32 | } 33 | 34 | // CollectInDependencyOrder walk the service graph an invoke CollectorFn in respect to dependency order, then return result for each call 35 | func CollectInDependencyOrder[T any](ctx context.Context, project *types.Project, fn CollectorFn[types.ServiceConfig, T], options ...func(*Options)) (map[string]T, error) { 36 | graph, err := newGraph(project) 37 | if err != nil { 38 | return nil, err 39 | } 40 | t := newTraversal(fn) 41 | for _, option := range options { 42 | option(t.Options) 43 | } 44 | err = walk(ctx, graph, t) 45 | return t.results, err 46 | } 47 | 48 | // newGraph creates a service graph from project 49 | func newGraph(project *types.Project) (*graph[types.ServiceConfig], error) { 50 | g := &graph[types.ServiceConfig]{ 51 | vertices: map[string]*vertex[types.ServiceConfig]{}, 52 | } 53 | 54 | for name, s := range project.Services { 55 | g.addVertex(name, s) 56 | } 57 | 58 | for name, s := range project.Services { 59 | src := g.vertices[name] 60 | for dep, condition := range s.DependsOn { 61 | dest, ok := g.vertices[dep] 62 | if !ok { 63 | if condition.Required { 64 | if ds, exists := project.DisabledServices[dep]; exists { 65 | return nil, fmt.Errorf("service %q is required by %q but is disabled. Can be enabled by profiles %s", dep, name, ds.Profiles) 66 | } 67 | return nil, fmt.Errorf("service %q depends on unknown service %q", name, dep) 68 | } 69 | delete(s.DependsOn, name) 70 | project.Services[name] = s 71 | continue 72 | } 73 | src.children[dep] = dest 74 | dest.parents[name] = src 75 | } 76 | } 77 | 78 | err := g.checkCycle() 79 | return g, err 80 | } 81 | -------------------------------------------------------------------------------- /loader/environment.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package loader 18 | 19 | import ( 20 | "fmt" 21 | 22 | "github.com/compose-spec/compose-go/v2/types" 23 | ) 24 | 25 | // ResolveEnvironment update the environment variables for the format {- VAR} (without interpolation) 26 | func ResolveEnvironment(dict map[string]any, environment types.Mapping) { 27 | resolveServicesEnvironment(dict, environment) 28 | resolveSecretsEnvironment(dict, environment) 29 | resolveConfigsEnvironment(dict, environment) 30 | } 31 | 32 | func resolveServicesEnvironment(dict map[string]any, environment types.Mapping) { 33 | services, ok := dict["services"].(map[string]any) 34 | if !ok { 35 | return 36 | } 37 | 38 | for service, cfg := range services { 39 | serviceConfig, ok := cfg.(map[string]any) 40 | if !ok { 41 | continue 42 | } 43 | serviceEnv, ok := serviceConfig["environment"].([]any) 44 | if !ok { 45 | continue 46 | } 47 | envs := []any{} 48 | for _, env := range serviceEnv { 49 | varEnv, ok := env.(string) 50 | if !ok { 51 | continue 52 | } 53 | if found, ok := environment[varEnv]; ok { 54 | envs = append(envs, fmt.Sprintf("%s=%s", varEnv, found)) 55 | } else { 56 | // either does not exist or it was already resolved in interpolation 57 | envs = append(envs, varEnv) 58 | } 59 | } 60 | serviceConfig["environment"] = envs 61 | services[service] = serviceConfig 62 | } 63 | dict["services"] = services 64 | } 65 | 66 | func resolveSecretsEnvironment(dict map[string]any, environment types.Mapping) { 67 | secrets, ok := dict["secrets"].(map[string]any) 68 | if !ok { 69 | return 70 | } 71 | 72 | for name, cfg := range secrets { 73 | secret, ok := cfg.(map[string]any) 74 | if !ok { 75 | continue 76 | } 77 | env, ok := secret["environment"].(string) 78 | if !ok { 79 | continue 80 | } 81 | if found, ok := environment[env]; ok { 82 | secret[types.SecretConfigXValue] = found 83 | } 84 | secrets[name] = secret 85 | } 86 | dict["secrets"] = secrets 87 | } 88 | 89 | func resolveConfigsEnvironment(dict map[string]any, environment types.Mapping) { 90 | configs, ok := dict["configs"].(map[string]any) 91 | if !ok { 92 | return 93 | } 94 | 95 | for name, cfg := range configs { 96 | config, ok := cfg.(map[string]any) 97 | if !ok { 98 | continue 99 | } 100 | env, ok := config["environment"].(string) 101 | if !ok { 102 | continue 103 | } 104 | if found, ok := environment[env]; ok { 105 | config["content"] = found 106 | } 107 | configs[name] = config 108 | } 109 | dict["configs"] = configs 110 | } 111 | -------------------------------------------------------------------------------- /loader/example1.env: -------------------------------------------------------------------------------- 1 | # passed through 2 | FOO=foo_from_env_file 3 | ENV.WITH.DOT=ok 4 | ENV_WITH_UNDERSCORE=ok 5 | 6 | # overridden in example2.env 7 | BAR=bar_from_env_file 8 | 9 | # overridden in full-example.yml 10 | BAZ=baz_from_env_file 11 | -------------------------------------------------------------------------------- /loader/example1.label: -------------------------------------------------------------------------------- 1 | # passed through 2 | FOO=foo_from_label_file 3 | LABEL.WITH.DOT=ok 4 | LABEL_WITH_UNDERSCORE=ok 5 | 6 | # overridden in example2.label 7 | BAR=bar_from_label_file 8 | 9 | # overridden in full-example.yml 10 | BAZ=baz_from_label_file 11 | -------------------------------------------------------------------------------- /loader/example2.env: -------------------------------------------------------------------------------- 1 | BAR=bar_from_env_file_2 2 | 3 | # overridden in configDetails.Environment 4 | QUX=quz_from_env_file_2 5 | -------------------------------------------------------------------------------- /loader/example2.label: -------------------------------------------------------------------------------- 1 | BAR=bar_from_label_file_2 2 | 3 | # overridden in configDetails.Labels 4 | QUX=quz_from_label_file_2 5 | -------------------------------------------------------------------------------- /loader/fix.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package loader 18 | 19 | // fixEmptyNotNull is a workaround for https://github.com/xeipuuv/gojsonschema/issues/141 20 | // as go-yaml `[]` will load as a `[]any(nil)`, which is not the same as an empty array 21 | func fixEmptyNotNull(value any) interface{} { 22 | switch v := value.(type) { 23 | case []any: 24 | if v == nil { 25 | return []any{} 26 | } 27 | for i, e := range v { 28 | v[i] = fixEmptyNotNull(e) 29 | } 30 | case map[string]any: 31 | for k, e := range v { 32 | v[k] = fixEmptyNotNull(e) 33 | } 34 | } 35 | return value 36 | } 37 | -------------------------------------------------------------------------------- /loader/loader_windows_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package loader 18 | 19 | import ( 20 | "testing" 21 | 22 | "github.com/compose-spec/compose-go/v2/types" 23 | "gotest.tools/v3/assert" 24 | ) 25 | 26 | func TestConvertWindowsVolumePath(t *testing.T) { 27 | testcases := []struct { 28 | windowsPath string 29 | expectedConvertedPath string 30 | }{ 31 | { 32 | windowsPath: "c:\\hello\\docker", 33 | expectedConvertedPath: "/c/hello/docker", 34 | }, 35 | { 36 | windowsPath: "d:\\compose", 37 | expectedConvertedPath: "/d/compose", 38 | }, 39 | { 40 | windowsPath: "e:\\path with spaces\\compose", 41 | expectedConvertedPath: "/e/path with spaces/compose", 42 | }, 43 | } 44 | for _, testcase := range testcases { 45 | volume := types.ServiceVolumeConfig{ 46 | Type: "bind", 47 | Source: testcase.windowsPath, 48 | Target: "/test", 49 | } 50 | 51 | assert.Equal(t, testcase.expectedConvertedPath, convertVolumePath(volume).Source) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /loader/mapstructure.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package loader 18 | 19 | import ( 20 | "reflect" 21 | "strconv" 22 | ) 23 | 24 | // comparable to yaml.Unmarshaler, decoder allow a type to define it's own custom logic to convert value 25 | // see https://github.com/mitchellh/mapstructure/pull/294 26 | type decoder interface { 27 | DecodeMapstructure(interface{}) error 28 | } 29 | 30 | // see https://github.com/mitchellh/mapstructure/issues/115#issuecomment-735287466 31 | // adapted to support types derived from built-in types, as DecodeMapstructure would not be able to mutate internal 32 | // value, so need to invoke DecodeMapstructure defined by pointer to type 33 | func decoderHook(from reflect.Value, to reflect.Value) (interface{}, error) { 34 | // If the destination implements the decoder interface 35 | u, ok := to.Interface().(decoder) 36 | if !ok { 37 | // for non-struct types we need to invoke func (*type) DecodeMapstructure() 38 | if to.CanAddr() { 39 | pto := to.Addr() 40 | u, ok = pto.Interface().(decoder) 41 | } 42 | if !ok { 43 | return from.Interface(), nil 44 | } 45 | } 46 | // If it is nil and a pointer, create and assign the target value first 47 | if to.Type().Kind() == reflect.Ptr && to.IsNil() { 48 | to.Set(reflect.New(to.Type().Elem())) 49 | u = to.Interface().(decoder) 50 | } 51 | // Call the custom DecodeMapstructure method 52 | if err := u.DecodeMapstructure(from.Interface()); err != nil { 53 | return to.Interface(), err 54 | } 55 | return to.Interface(), nil 56 | } 57 | 58 | func cast(from reflect.Value, to reflect.Value) (interface{}, error) { 59 | switch from.Type().Kind() { 60 | case reflect.String: 61 | switch to.Kind() { 62 | case reflect.Bool: 63 | return toBoolean(from.String()) 64 | case reflect.Int: 65 | return toInt(from.String()) 66 | case reflect.Int64: 67 | return toInt64(from.String()) 68 | case reflect.Float32: 69 | return toFloat32(from.String()) 70 | case reflect.Float64: 71 | return toFloat(from.String()) 72 | } 73 | case reflect.Int: 74 | if to.Kind() == reflect.String { 75 | return strconv.FormatInt(from.Int(), 10), nil 76 | } 77 | } 78 | return from.Interface(), nil 79 | } 80 | -------------------------------------------------------------------------------- /loader/mapstructure_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package loader 18 | 19 | import ( 20 | "testing" 21 | 22 | "github.com/compose-spec/compose-go/v2/types" 23 | "github.com/go-viper/mapstructure/v2" 24 | "gotest.tools/v3/assert" 25 | ) 26 | 27 | func TestDecodeMapStructure(t *testing.T) { 28 | var target types.ServiceConfig 29 | data := mapstructure.Metadata{} 30 | config := &mapstructure.DecoderConfig{ 31 | Result: &target, 32 | TagName: "yaml", 33 | Metadata: &data, 34 | DecodeHook: mapstructure.ComposeDecodeHookFunc(decoderHook), 35 | } 36 | decoder, err := mapstructure.NewDecoder(config) 37 | assert.NilError(t, err) 38 | err = decoder.Decode(map[string]interface{}{ 39 | "mem_limit": "640k", 40 | "command": "echo hello", 41 | "stop_grace_period": "60s", 42 | "labels": []interface{}{ 43 | "FOO=BAR", 44 | }, 45 | "deploy": map[string]interface{}{ 46 | "labels": map[string]interface{}{ 47 | "FOO": "BAR", 48 | "BAZ": nil, 49 | "QIX": 2, 50 | "ZOT": true, 51 | }, 52 | }, 53 | }) 54 | assert.NilError(t, err) 55 | assert.Equal(t, target.MemLimit, types.UnitBytes(640*1024)) 56 | assert.DeepEqual(t, target.Command, types.ShellCommand{"echo", "hello"}) 57 | assert.Equal(t, *target.StopGracePeriod, types.Duration(60_000_000_000)) 58 | assert.DeepEqual(t, target.Labels, types.Labels{"FOO": "BAR"}) 59 | assert.DeepEqual(t, target.Deploy.Labels, types.Labels{ 60 | "FOO": "BAR", 61 | "BAZ": "", 62 | "QIX": "2", 63 | "ZOT": "true", 64 | }) 65 | } 66 | -------------------------------------------------------------------------------- /loader/merge_reset_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package loader 18 | 19 | import ( 20 | "context" 21 | "testing" 22 | 23 | "github.com/compose-spec/compose-go/v2/types" 24 | "gotest.tools/v3/assert" 25 | ) 26 | 27 | func Test_LoadWithReset(t *testing.T) { 28 | p, err := LoadWithContext(context.TODO(), types.ConfigDetails{ 29 | ConfigFiles: []types.ConfigFile{ 30 | { 31 | Filename: "base.yml", 32 | Content: []byte(` 33 | name: test 34 | services: 35 | foo: 36 | build: 37 | context: . 38 | dockerfile: foo.Dockerfile 39 | environment: 40 | FOO: BAR`), 41 | }, 42 | { 43 | Filename: "override.yml", 44 | Content: []byte(` 45 | services: 46 | foo: 47 | image: foo 48 | build: !reset 49 | environment: 50 | FOO: !reset 51 | `), 52 | }, 53 | }, 54 | }, func(options *Options) { 55 | options.SkipNormalization = true 56 | }) 57 | assert.NilError(t, err) 58 | assert.DeepEqual(t, p.Services["foo"], types.ServiceConfig{ 59 | Name: "foo", 60 | Image: "foo", 61 | Environment: types.MappingWithEquals{}, 62 | }) 63 | } 64 | 65 | func Test_DuplicateReset(t *testing.T) { 66 | _, err := LoadWithContext(context.TODO(), types.ConfigDetails{ 67 | ConfigFiles: []types.ConfigFile{ 68 | { 69 | Filename: "duplicate.yml", 70 | Content: []byte(` 71 | name: test 72 | services: 73 | foo: 74 | command: hello 75 | command: !reset hello world 76 | `), 77 | }, 78 | }, 79 | }, func(options *Options) { 80 | options.SkipNormalization = true 81 | }) 82 | assert.Error(t, err, "line 6: mapping key \"command\" already defined at line 5") 83 | } 84 | -------------------------------------------------------------------------------- /loader/omitEmpty.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package loader 18 | 19 | import "github.com/compose-spec/compose-go/v2/tree" 20 | 21 | var omitempty = []tree.Path{ 22 | "services.*.dns", 23 | } 24 | 25 | // OmitEmpty removes empty attributes which are irrelevant when unset 26 | func OmitEmpty(yaml map[string]any) map[string]any { 27 | cleaned := omitEmpty(yaml, tree.NewPath()) 28 | return cleaned.(map[string]any) 29 | } 30 | 31 | func omitEmpty(data any, p tree.Path) any { 32 | switch v := data.(type) { 33 | case map[string]any: 34 | for k, e := range v { 35 | if isEmpty(e) && mustOmit(p) { 36 | delete(v, k) 37 | continue 38 | } 39 | 40 | v[k] = omitEmpty(e, p.Next(k)) 41 | } 42 | return v 43 | case []any: 44 | var c []any 45 | for _, e := range v { 46 | if isEmpty(e) && mustOmit(p) { 47 | continue 48 | } 49 | 50 | c = append(c, omitEmpty(e, p.Next("[]"))) 51 | } 52 | return c 53 | default: 54 | return data 55 | } 56 | } 57 | 58 | func mustOmit(p tree.Path) bool { 59 | for _, pattern := range omitempty { 60 | if p.Matches(pattern) { 61 | return true 62 | } 63 | } 64 | return false 65 | } 66 | 67 | func isEmpty(e any) bool { 68 | if e == nil { 69 | return true 70 | } 71 | if v, ok := e.(string); ok && v == "" { 72 | return true 73 | } 74 | return false 75 | } 76 | -------------------------------------------------------------------------------- /loader/paths.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package loader 18 | 19 | import ( 20 | "path/filepath" 21 | 22 | "github.com/compose-spec/compose-go/v2/types" 23 | ) 24 | 25 | // ResolveRelativePaths resolves relative paths based on project WorkingDirectory 26 | func ResolveRelativePaths(project *types.Project) error { 27 | absWorkingDir, err := filepath.Abs(project.WorkingDir) 28 | if err != nil { 29 | return err 30 | } 31 | project.WorkingDir = absWorkingDir 32 | 33 | absComposeFiles, err := absComposeFiles(project.ComposeFiles) 34 | if err != nil { 35 | return err 36 | } 37 | project.ComposeFiles = absComposeFiles 38 | return nil 39 | } 40 | 41 | func absComposeFiles(composeFiles []string) ([]string, error) { 42 | for i, composeFile := range composeFiles { 43 | absComposefile, err := filepath.Abs(composeFile) 44 | if err != nil { 45 | return nil, err 46 | } 47 | composeFiles[i] = absComposefile 48 | } 49 | return composeFiles, nil 50 | } 51 | -------------------------------------------------------------------------------- /loader/paths_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package loader 18 | 19 | import ( 20 | "fmt" 21 | "os" 22 | "path/filepath" 23 | "testing" 24 | 25 | "github.com/compose-spec/compose-go/v2/types" 26 | "gotest.tools/v3/assert" 27 | ) 28 | 29 | func TestResolveComposeFilePaths(t *testing.T) { 30 | absWorkingDir, _ := filepath.Abs("testdata") 31 | absComposeFile, _ := filepath.Abs(filepath.Join("testdata", "simple", "compose.yaml")) 32 | absOverrideFile, _ := filepath.Abs(filepath.Join("testdata", "simple", "compose-with-overrides.yaml")) 33 | 34 | project := types.Project{ 35 | Name: "myProject", 36 | WorkingDir: absWorkingDir, 37 | ComposeFiles: []string{filepath.Join("testdata", "simple", "compose.yaml"), filepath.Join("testdata", "simple", "compose-with-overrides.yaml")}, 38 | } 39 | 40 | expected := types.Project{ 41 | Name: "myProject", 42 | WorkingDir: absWorkingDir, 43 | ComposeFiles: []string{absComposeFile, absOverrideFile}, 44 | } 45 | err := ResolveRelativePaths(&project) 46 | assert.NilError(t, err) 47 | assert.DeepEqual(t, expected, project) 48 | } 49 | 50 | func TestResolveBuildContextPaths(t *testing.T) { 51 | yaml := ` 52 | name: test-resolve-build-context-paths 53 | services: 54 | foo: 55 | build: 56 | context: ./testdata 57 | dockerfile: Dockerfile-sample 58 | ` 59 | project, err := loadYAML(yaml) 60 | assert.NilError(t, err) 61 | 62 | wd, err := os.Getwd() 63 | assert.NilError(t, err) 64 | 65 | expected := types.BuildConfig{ 66 | Context: filepath.Join(wd, "testdata"), 67 | Dockerfile: "Dockerfile-sample", 68 | } 69 | assert.DeepEqual(t, expected, *project.Services["foo"].Build) 70 | } 71 | 72 | func TestResolveAdditionalContexts(t *testing.T) { 73 | abs, err := filepath.Abs("/dir") 74 | assert.NilError(t, err) 75 | yaml := fmt.Sprintf(` 76 | name: test-resolve-additional-contexts 77 | services: 78 | test: 79 | build: 80 | context: . 81 | dockerfile: Dockerfile 82 | additional_contexts: 83 | image: docker-image://foo 84 | oci: oci-layout://foo 85 | abs_path: %s 86 | github: github.com/compose-spec/compose-go 87 | rel_path: ./testdata 88 | `, abs) 89 | project, err := loadYAML(yaml) 90 | assert.NilError(t, err) 91 | 92 | wd, err := os.Getwd() 93 | assert.NilError(t, err) 94 | 95 | expected := types.BuildConfig{ 96 | Context: wd, 97 | Dockerfile: "Dockerfile", 98 | AdditionalContexts: map[string]string{ 99 | "image": "docker-image://foo", 100 | "oci": "oci-layout://foo", 101 | "abs_path": abs, 102 | "github": "github.com/compose-spec/compose-go", 103 | "rel_path": filepath.Join(wd, "testdata"), 104 | }, 105 | } 106 | assert.DeepEqual(t, expected, *project.Services["test"].Build) 107 | } 108 | -------------------------------------------------------------------------------- /loader/testdata/Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright 2020 The Compose Specification Authors. 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | FROM busybox:1.31.0-uclibc 16 | RUN echo something 17 | CMD top 18 | -------------------------------------------------------------------------------- /loader/testdata/combined/compose.yaml: -------------------------------------------------------------------------------- 1 | include: 2 | - path: 3 | - dir/included.yaml 4 | 5 | -------------------------------------------------------------------------------- /loader/testdata/combined/dir/extended.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | service: 3 | build: . 4 | 5 | -------------------------------------------------------------------------------- /loader/testdata/combined/dir/included.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | service: 3 | extends: 4 | file: extended.yaml 5 | service: service 6 | 7 | -------------------------------------------------------------------------------- /loader/testdata/compose-depends-on-cycle.yaml: -------------------------------------------------------------------------------- 1 | name : depends-on-cycle 2 | services: 3 | service1: 4 | image: service1 5 | depends_on: 6 | - service2 7 | service2: 8 | image: service2 9 | depends_on: 10 | - service3 11 | service3: 12 | image: service3 13 | depends_on: 14 | - service1 15 | -------------------------------------------------------------------------------- /loader/testdata/compose-depends-on-profile-no-cycle.yaml: -------------------------------------------------------------------------------- 1 | name : depends-on-cycle 2 | services: 3 | service1: 4 | image: service1 5 | depends_on: 6 | - service2 7 | service2: 8 | image: service2 9 | depends_on: 10 | - service3 11 | service3: 12 | image: service3 13 | depends_on: 14 | - service1 15 | -------------------------------------------------------------------------------- /loader/testdata/compose-depends-on-self.yaml: -------------------------------------------------------------------------------- 1 | name : depends-on-cycle 2 | services: 3 | service1: 4 | image: service1 5 | depends_on: 6 | - service1 7 | -------------------------------------------------------------------------------- /loader/testdata/compose-include-cycle.yaml: -------------------------------------------------------------------------------- 1 | include: 2 | - compose-include-cycle.yaml 3 | 4 | name: project 5 | services: 6 | foo: 7 | image: foo 8 | -------------------------------------------------------------------------------- /loader/testdata/compose-include.yaml: -------------------------------------------------------------------------------- 1 | include: 2 | - path: ./subdir/compose-test-extends-imported.yaml 3 | 4 | services: 5 | bar: 6 | image: bar -------------------------------------------------------------------------------- /loader/testdata/compose-test-extends-with-context-url-imported.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | imported-with-https-url: 3 | build: 4 | context: https://github.com/docker/compose.git 5 | -------------------------------------------------------------------------------- /loader/testdata/compose-test-extends-with-context-url.yaml: -------------------------------------------------------------------------------- 1 | name: compose-test-extends-with-context-url 2 | services: 3 | importer-with-https-url: 4 | extends: 5 | file: compose-test-extends-with-context-url-imported.yaml 6 | service: imported-with-https-url 7 | -------------------------------------------------------------------------------- /loader/testdata/compose-test-extends.yaml: -------------------------------------------------------------------------------- 1 | name: compose-test-extends 2 | services: 3 | importer: 4 | extends: 5 | file: subdir/compose-test-extends-imported.yaml 6 | service: imported 7 | -------------------------------------------------------------------------------- /loader/testdata/compose-test-with-version.yaml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | name: compose-test-with-version 3 | 4 | volumes: 5 | data: 6 | driver: local 7 | 8 | networks: 9 | front: {} 10 | 11 | services: 12 | web: 13 | build: ./Dockerfile 14 | networks: 15 | - front 16 | - default 17 | volumes_from: 18 | - other 19 | 20 | other: 21 | image: busybox:1.31.0-uclibc 22 | command: top 23 | volumes: 24 | - /data 25 | -------------------------------------------------------------------------------- /loader/testdata/empty.yaml: -------------------------------------------------------------------------------- 1 | # Empty file 2 | -------------------------------------------------------------------------------- /loader/testdata/extends/base.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | base: 3 | image: base 4 | 5 | another: 6 | extends: base 7 | 8 | with-port: 9 | ports: 10 | - 8000:8000 11 | 12 | withUlimits: 13 | image: test 14 | ulimits: 15 | nproc: 65535 16 | nofile: 17 | soft: 20000 18 | hard: 40000 19 | 20 | with-build: 21 | extends: 22 | file: sibling.yaml 23 | service: test 24 | 25 | nil: #left intentionally empty 26 | -------------------------------------------------------------------------------- /loader/testdata/extends/depends_on.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | with_depends_on: 3 | image: foo 4 | depends_on: 5 | - zot 6 | 7 | with_volumes_from: 8 | image: foo 9 | volumes_from: 10 | - zot 11 | 12 | with_ipc: 13 | image: foo 14 | ipc: "service:zot" 15 | 16 | with_network_mode: 17 | image: foo 18 | network_mode: "service:zot" 19 | 20 | zot: 21 | image: hidden 22 | -------------------------------------------------------------------------------- /loader/testdata/extends/interpolated.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | foo: 3 | image: bash 4 | volumes: 5 | - ${SOURCE:-/dev/null}:/tmp/foo:ro -------------------------------------------------------------------------------- /loader/testdata/extends/nested.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | with-build: 3 | extends: 4 | file: sibling.yaml 5 | service: test 6 | -------------------------------------------------------------------------------- /loader/testdata/extends/ports.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | test: 3 | image: test 4 | ports: 5 | - 8080:8080 -------------------------------------------------------------------------------- /loader/testdata/extends/reset.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | init: 3 | image: alpine:latest 4 | command: "sleep infinity" 5 | base: 6 | extends: { service: init } 7 | command: !reset -------------------------------------------------------------------------------- /loader/testdata/extends/sibling.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | test: 3 | build: . 4 | -------------------------------------------------------------------------------- /loader/testdata/extends/withdir/compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | test: 3 | extends: 4 | file: dir/compose.yaml 5 | service: test 6 | -------------------------------------------------------------------------------- /loader/testdata/extends/withdir/dir/base.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | test: 3 | image: hello-world 4 | 5 | -------------------------------------------------------------------------------- /loader/testdata/extends/withdir/dir/compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | test: 3 | extends: 4 | file: base.yaml 5 | service: test 6 | -------------------------------------------------------------------------------- /loader/testdata/include/compose.yaml: -------------------------------------------------------------------------------- 1 | include: 2 | - path: included.yaml 3 | -------------------------------------------------------------------------------- /loader/testdata/include/dir/compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | service: 3 | build: 4 | dockerfile_inline: | 5 | FROM busybox 6 | COPY compose-test-extends-imported.yaml compose.yaml 7 | volumes: 8 | - type: bind 9 | source: compose-test-extends-imported.yaml 10 | target: /mnt/bind/compose.yaml 11 | env_file: 12 | - extra.env -------------------------------------------------------------------------------- /loader/testdata/include/included.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | included: 3 | build: . 4 | volumes: 5 | - ./:/mnt 6 | -------------------------------------------------------------------------------- /loader/testdata/include/project-directory.yaml: -------------------------------------------------------------------------------- 1 | include: 2 | - project_directory: ../subdir 3 | path: ./dir/compose.yaml -------------------------------------------------------------------------------- /loader/testdata/remote/compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | foo: 3 | image: foo 4 | env_file: 5 | - ./env 6 | volumes: 7 | - .:/foo 8 | -------------------------------------------------------------------------------- /loader/testdata/remote/cycle/compose-cycle.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | bar: 3 | extends: 4 | file: remote:cycle/compose.yaml 5 | service: foo 6 | -------------------------------------------------------------------------------- /loader/testdata/remote/cycle/compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | foo: 3 | extends: 4 | file: remote:cycle/compose-cycle.yaml 5 | service: bar 6 | -------------------------------------------------------------------------------- /loader/testdata/remote/env: -------------------------------------------------------------------------------- 1 | FOO=BAR 2 | -------------------------------------------------------------------------------- /loader/testdata/remote/nested/compose-nested.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | bar: 3 | image: bar 4 | -------------------------------------------------------------------------------- /loader/testdata/remote/nested/compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | foo: 3 | extends: 4 | file: remote:nested/compose-nested.yaml 5 | service: bar 6 | -------------------------------------------------------------------------------- /loader/testdata/subdir/compose-test-extends-imported.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | imported: 3 | image: nginx 4 | container_name: ${SOURCE:-imported} 5 | env_file: 6 | - ./extra.env # expected to be loaded relative to this file, not the extending one 7 | volumes: 8 | - /opt/data:/var/lib/mysql 9 | -------------------------------------------------------------------------------- /loader/testdata/subdir/extra.env: -------------------------------------------------------------------------------- 1 | SOURCE=extends 2 | -------------------------------------------------------------------------------- /loader/types_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package loader 18 | 19 | import ( 20 | "context" 21 | "os" 22 | "testing" 23 | 24 | "gotest.tools/v3/assert" 25 | is "gotest.tools/v3/assert/cmp" 26 | ) 27 | 28 | func TestMarshalProject(t *testing.T) { 29 | workingDir, err := os.Getwd() 30 | assert.NilError(t, err) 31 | homeDir, err := os.UserHomeDir() 32 | assert.NilError(t, err) 33 | project := fullExampleProject(workingDir, homeDir) 34 | expected := fullExampleYAML(workingDir, homeDir) 35 | 36 | actual, err := project.MarshalYAML() 37 | assert.NilError(t, err) 38 | assert.Check(t, is.Equal(expected, string(actual))) 39 | 40 | // Make sure the expected still 41 | _, err = LoadWithContext(context.TODO(), buildConfigDetails(expected, map[string]string{}), func(options *Options) { 42 | options.SkipNormalization = true 43 | options.SkipConsistencyCheck = true 44 | }) 45 | assert.NilError(t, err) 46 | } 47 | 48 | func TestJSONMarshalProject(t *testing.T) { 49 | workingDir, err := os.Getwd() 50 | assert.NilError(t, err) 51 | homeDir, err := os.UserHomeDir() 52 | assert.NilError(t, err) 53 | 54 | project := fullExampleProject(workingDir, homeDir) 55 | expected := fullExampleJSON(workingDir, homeDir) 56 | 57 | actual, err := project.MarshalJSON() 58 | assert.NilError(t, err) 59 | assert.Check(t, is.Equal(expected, string(actual))) 60 | 61 | _, err = LoadWithContext(context.TODO(), buildConfigDetails(expected, map[string]string{}), func(options *Options) { 62 | options.SkipNormalization = true 63 | options.SkipConsistencyCheck = true 64 | }) 65 | assert.NilError(t, err) 66 | } 67 | -------------------------------------------------------------------------------- /loader/with-version-struct_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package loader 18 | 19 | import ( 20 | "path/filepath" 21 | 22 | "github.com/compose-spec/compose-go/v2/types" 23 | ) 24 | 25 | func withVersionExampleConfig() *types.Config { 26 | return &types.Config{ 27 | Services: withVersionServices(), 28 | Networks: withVersionNetworks(), 29 | Volumes: withVersionVolumes(), 30 | } 31 | } 32 | 33 | func withVersionServices() types.Services { 34 | buildCtx, _ := filepath.Abs("./Dockerfile") 35 | return types.Services{ 36 | "web": { 37 | Name: "web", 38 | 39 | Build: &types.BuildConfig{ 40 | Context: buildCtx, 41 | }, 42 | Environment: types.MappingWithEquals{}, 43 | Networks: map[string]*types.ServiceNetworkConfig{ 44 | "front": nil, 45 | "default": nil, 46 | }, 47 | VolumesFrom: []string{"other"}, 48 | }, 49 | "other": { 50 | Name: "other", 51 | 52 | Image: "busybox:1.31.0-uclibc", 53 | Command: []string{"top"}, 54 | Environment: types.MappingWithEquals{}, 55 | Volumes: []types.ServiceVolumeConfig{ 56 | {Target: "/data", Type: "volume", Volume: &types.ServiceVolumeVolume{}}, 57 | }, 58 | }, 59 | } 60 | } 61 | 62 | func withVersionNetworks() map[string]types.NetworkConfig { 63 | return map[string]types.NetworkConfig{ 64 | "front": {}, 65 | } 66 | } 67 | 68 | func withVersionVolumes() map[string]types.VolumeConfig { 69 | return map[string]types.VolumeConfig{ 70 | "data": { 71 | Driver: "local", 72 | }, 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /override/extends.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package override 18 | 19 | import "github.com/compose-spec/compose-go/v2/tree" 20 | 21 | func ExtendService(base, override map[string]any) (map[string]any, error) { 22 | yaml, err := mergeYaml(base, override, tree.NewPath("services.x")) 23 | if err != nil { 24 | return nil, err 25 | } 26 | return yaml.(map[string]any), nil 27 | } 28 | -------------------------------------------------------------------------------- /override/merge_annotations_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package override 18 | 19 | import ( 20 | "testing" 21 | ) 22 | 23 | func TestMergeAnnotationsSequence(t *testing.T) { 24 | assertMergeYaml(t, ` 25 | services: 26 | test: 27 | image: foo 28 | annotations: 29 | - FOO=BAR 30 | `, ` 31 | services: 32 | test: 33 | annotations: 34 | - QIX=ZOT 35 | - EMPTY= 36 | - NIL 37 | `, ` 38 | services: 39 | test: 40 | image: foo 41 | annotations: 42 | - FOO=BAR 43 | - QIX=ZOT 44 | - EMPTY= 45 | - NIL 46 | `) 47 | } 48 | 49 | func TestMergeAnnotationsMapping(t *testing.T) { 50 | assertMergeYaml(t, ` 51 | services: 52 | test: 53 | image: foo 54 | annotations: 55 | FOO: BAR 56 | `, ` 57 | services: 58 | test: 59 | annotations: 60 | EMPTY: "" 61 | NIL: null 62 | QIX: ZOT 63 | `, ` 64 | services: 65 | test: 66 | image: foo 67 | annotations: 68 | - FOO=BAR 69 | - EMPTY= 70 | - NIL 71 | - QIX=ZOT 72 | `) 73 | } 74 | 75 | func TestMergeAnnotationsMixed(t *testing.T) { 76 | assertMergeYaml(t, ` 77 | services: 78 | test: 79 | image: foo 80 | annotations: 81 | FOO: BAR 82 | `, ` 83 | services: 84 | test: 85 | annotations: 86 | - QIX=ZOT 87 | `, ` 88 | services: 89 | test: 90 | image: foo 91 | annotations: 92 | - FOO=BAR 93 | - QIX=ZOT 94 | `) 95 | } 96 | 97 | func TestMergeAnnotationsNumbers(t *testing.T) { 98 | assertMergeYaml(t, ` 99 | services: 100 | test: 101 | annotations: 102 | FOO: 1 103 | `, ` 104 | services: 105 | test: 106 | annotations: 107 | FOO: 3 108 | `, ` 109 | services: 110 | test: 111 | annotations: 112 | - FOO=3 113 | `) 114 | } 115 | -------------------------------------------------------------------------------- /override/merge_cap_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package override 18 | 19 | import ( 20 | "testing" 21 | ) 22 | 23 | func TestMergeCAPSequence(t *testing.T) { 24 | assertMergeYaml(t, ` 25 | services: 26 | test: 27 | image: foo 28 | cap_add: 29 | - CAP_BPF 30 | - CAP_CHOWN 31 | cap_drop: 32 | - NET_ADMIN 33 | - SYS_ADMIN 34 | `, ` 35 | services: 36 | test: 37 | cap_add: 38 | - CAP_KILL 39 | - CAP_CHOWN 40 | cap_drop: 41 | - NET_ADMIN 42 | - CAP_FOWNER 43 | `, ` 44 | services: 45 | test: 46 | image: foo 47 | cap_add: 48 | - CAP_BPF 49 | - CAP_CHOWN 50 | - CAP_KILL 51 | cap_drop: 52 | - NET_ADMIN 53 | - SYS_ADMIN 54 | - CAP_FOWNER 55 | `) 56 | } 57 | -------------------------------------------------------------------------------- /override/merge_devices_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package override 18 | 19 | import ( 20 | "testing" 21 | ) 22 | 23 | func Test_mergeYamlDevices(t *testing.T) { 24 | assertMergeYaml(t, ` 25 | services: 26 | test: 27 | image: foo 28 | devices: 29 | - '/dev/sda:/dev/sda' 30 | - '/dev/sdb:/dev/sdb' 31 | - '/dev/sdc:/dev/sdc' 32 | - '/dev/sdd:/dev/sdd' 33 | `, ` 34 | services: 35 | test: 36 | devices: 37 | - '/dev/sde:/dev/sde' 38 | - '/dev/sdf:/dev/sdf' 39 | - '/dev/sdg:/dev/sdg' 40 | - '/dev/sdh:/dev/sdh' 41 | `, ` 42 | services: 43 | test: 44 | image: foo 45 | devices: 46 | - '/dev/sda:/dev/sda' 47 | - '/dev/sdb:/dev/sdb' 48 | - '/dev/sdc:/dev/sdc' 49 | - '/dev/sdd:/dev/sdd' 50 | - '/dev/sde:/dev/sde' 51 | - '/dev/sdf:/dev/sdf' 52 | - '/dev/sdg:/dev/sdg' 53 | - '/dev/sdh:/dev/sdh' 54 | `) 55 | } 56 | 57 | func Test_mergeYamlDevicesOverride(t *testing.T) { 58 | assertMergeYaml(t, ` 59 | services: 60 | test: 61 | image: foo 62 | devices: 63 | - '/dev/sda:/dev/sda' 64 | - '/dev/sdb:/dev/sdb' 65 | - '/dev/sdc:/dev/sdc' 66 | - '/dev/sdd:/dev/sdd' 67 | `, ` 68 | services: 69 | test: 70 | devices: 71 | - '/dev/nvme0n1p1:/dev/sda' 72 | - '/dev/nvme1n1p1:/dev/sdb' 73 | - '/dev/nvme2n1p1:/dev/sdc' 74 | `, ` 75 | services: 76 | test: 77 | image: foo 78 | devices: 79 | - '/dev/nvme0n1p1:/dev/sda' 80 | - '/dev/nvme1n1p1:/dev/sdb' 81 | - '/dev/nvme2n1p1:/dev/sdc' 82 | - '/dev/sdd:/dev/sdd' 83 | `) 84 | } 85 | -------------------------------------------------------------------------------- /override/merge_dns_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package override 18 | 19 | import ( 20 | "testing" 21 | ) 22 | 23 | func Test_mergeYamlDNSSequence(t *testing.T) { 24 | assertMergeYaml(t, ` 25 | services: 26 | test: 27 | image: foo 28 | dns: 29 | - 8.8.8.8 30 | dns_opt: 31 | - use-vc 32 | dns_search: 33 | - dc1.example.com 34 | `, ` 35 | services: 36 | test: 37 | dns: 38 | - 9.9.9.9 39 | dns_opt: 40 | - no-tld-query 41 | dns_search: 42 | - dc2.example.com 43 | `, ` 44 | services: 45 | test: 46 | image: foo 47 | dns: 48 | - 8.8.8.8 49 | - 9.9.9.9 50 | dns_opt: 51 | - use-vc 52 | - no-tld-query 53 | dns_search: 54 | - dc1.example.com 55 | - dc2.example.com 56 | `) 57 | } 58 | 59 | func Test_mergeYamlDNSMixed(t *testing.T) { 60 | assertMergeYaml(t, ` 61 | services: 62 | test: 63 | image: foo 64 | dns: 65 | - 8.8.8.8 66 | - 10.10.10.10 67 | - 9.9.9.9 68 | dns_opt: 69 | - use-vc 70 | - no-tld-query 71 | dns_search: 72 | - dc1.example.com 73 | - dc2.example.com 74 | `, ` 75 | services: 76 | test: 77 | dns: 9.9.9.9 78 | dns_opt: 79 | - no-tld-query 80 | dns_search: dc2.example.com 81 | `, ` 82 | services: 83 | test: 84 | image: foo 85 | dns: 86 | - 8.8.8.8 87 | - 10.10.10.10 88 | - 9.9.9.9 89 | dns_opt: 90 | - use-vc 91 | - no-tld-query 92 | dns_search: 93 | - dc1.example.com 94 | - dc2.example.com 95 | `) 96 | } 97 | -------------------------------------------------------------------------------- /override/merge_env_file_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package override 18 | 19 | import ( 20 | "testing" 21 | ) 22 | 23 | func TestMergeEnvFilesSequence(t *testing.T) { 24 | assertMergeYaml(t, ` 25 | services: 26 | test: 27 | image: foo 28 | env_file: 29 | - foo.env 30 | `, ` 31 | services: 32 | test: 33 | env_file: 34 | - bar.env 35 | - baz.env 36 | `, ` 37 | services: 38 | test: 39 | image: foo 40 | env_file: 41 | - foo.env 42 | - bar.env 43 | - baz.env 44 | `) 45 | } 46 | 47 | func TestMergeEnvFilesString(t *testing.T) { 48 | assertMergeYaml(t, ` 49 | services: 50 | test: 51 | image: foo 52 | env_file: foo.env 53 | `, ` 54 | services: 55 | test: 56 | env_file: bar.env 57 | `, ` 58 | services: 59 | test: 60 | image: foo 61 | env_file: 62 | - foo.env 63 | - bar.env 64 | `) 65 | } 66 | 67 | func TestMergeEnvFilesMixed(t *testing.T) { 68 | l := ` 69 | services: 70 | test: 71 | env_file: 72 | - bar.env 73 | - foo.env 74 | ` 75 | 76 | r := ` 77 | services: 78 | test: 79 | env_file: foo.env 80 | ` 81 | 82 | t.Run("SequenceThenString", func(t *testing.T) { 83 | assertMergeYaml(t, r, l, ` 84 | services: 85 | test: 86 | env_file: 87 | - foo.env 88 | - bar.env 89 | `) 90 | }) 91 | 92 | t.Run("StringThenSequence", func(t *testing.T) { 93 | assertMergeYaml(t, l, r, ` 94 | services: 95 | test: 96 | env_file: 97 | - bar.env 98 | - foo.env 99 | `) 100 | }) 101 | 102 | r = ` 103 | services: 104 | test: 105 | env_file: 106 | - path: foo.env 107 | required: true 108 | ` 109 | t.Run("Unicity between string and mapping", func(t *testing.T) { 110 | assertMergeYaml(t, l, r, ` 111 | services: 112 | test: 113 | env_file: 114 | - bar.env 115 | - path: foo.env 116 | required: true 117 | 118 | `) 119 | }) 120 | } 121 | -------------------------------------------------------------------------------- /override/merge_environment_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package override 18 | 19 | import ( 20 | "testing" 21 | ) 22 | 23 | func Test_mergeYamlEnvironmentSequence(t *testing.T) { 24 | assertMergeYaml(t, ` 25 | services: 26 | test: 27 | image: foo 28 | environment: 29 | - FOO=BAR 30 | `, ` 31 | services: 32 | test: 33 | environment: 34 | - QIX=ZOT 35 | - EMPTY= 36 | - NIL 37 | `, ` 38 | services: 39 | test: 40 | image: foo 41 | environment: 42 | - FOO=BAR 43 | - QIX=ZOT 44 | - EMPTY= 45 | - NIL 46 | `) 47 | } 48 | 49 | func Test_mergeYamlEnvironmentMapping(t *testing.T) { 50 | assertMergeYaml(t, ` 51 | services: 52 | test: 53 | image: foo 54 | environment: 55 | FOO: BAR 56 | `, ` 57 | services: 58 | test: 59 | environment: 60 | EMPTY: "" 61 | NIL: null 62 | QIX: ZOT 63 | `, ` 64 | services: 65 | test: 66 | image: foo 67 | environment: 68 | - FOO=BAR 69 | - EMPTY= 70 | - NIL 71 | - QIX=ZOT 72 | `) 73 | } 74 | 75 | func Test_mergeYamlEnvironmentMixed(t *testing.T) { 76 | assertMergeYaml(t, ` 77 | services: 78 | test: 79 | image: foo 80 | environment: 81 | FOO: BAR 82 | `, ` 83 | services: 84 | test: 85 | environment: 86 | - QIX=ZOT 87 | `, ` 88 | services: 89 | test: 90 | image: foo 91 | environment: 92 | - FOO=BAR 93 | - QIX=ZOT 94 | `) 95 | } 96 | 97 | func Test_mergeYamlEnvironmentNumber(t *testing.T) { 98 | assertMergeYaml(t, ` 99 | services: 100 | test: 101 | environment: 102 | FOO: 1 103 | `, ` 104 | services: 105 | test: 106 | environment: 107 | FOO: 3 108 | `, ` 109 | services: 110 | test: 111 | environment: 112 | - FOO=3 113 | `) 114 | } 115 | -------------------------------------------------------------------------------- /override/merge_extra_hosts_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package override 18 | 19 | import ( 20 | "testing" 21 | ) 22 | 23 | func TestMergeExtraHostsSequence(t *testing.T) { 24 | assertMergeYaml(t, ` 25 | services: 26 | test: 27 | image: foo 28 | extra_hosts: 29 | - example.com=1.2.3.4 30 | `, ` 31 | services: 32 | test: 33 | extra_hosts: 34 | - localhost=127.0.0.1 35 | - example.com=1.2.3.4 36 | - example.com=4.3.2.1 37 | `, ` 38 | services: 39 | test: 40 | image: foo 41 | extra_hosts: 42 | - example.com=1.2.3.4 43 | - localhost=127.0.0.1 44 | - example.com=4.3.2.1 45 | `) 46 | } 47 | 48 | func TestMergeExtraHostsMapping(t *testing.T) { 49 | assertMergeYaml(t, ` 50 | services: 51 | test: 52 | image: foo 53 | extra_hosts: 54 | "example.com": "1.2.3.4" 55 | `, ` 56 | services: 57 | test: 58 | extra_hosts: 59 | "localhost": "127.0.0.1" 60 | "example.com": ["1.2.3.4", "4.3.2.1"] 61 | `, ` 62 | services: 63 | test: 64 | image: foo 65 | extra_hosts: 66 | - example.com=1.2.3.4 67 | - example.com=4.3.2.1 68 | - localhost=127.0.0.1 69 | `) 70 | } 71 | 72 | func TestMergeExtraHostsMixed(t *testing.T) { 73 | assertMergeYaml(t, ` 74 | services: 75 | test: 76 | image: foo 77 | extra_hosts: 78 | "example.com": "1.2.3.4" 79 | `, ` 80 | services: 81 | test: 82 | extra_hosts: 83 | - localhost=127.0.0.1 84 | - example.com=1.2.3.4 85 | - example.com=4.3.2.1 86 | `, ` 87 | services: 88 | test: 89 | image: foo 90 | extra_hosts: 91 | - example.com=1.2.3.4 92 | - localhost=127.0.0.1 93 | - example.com=4.3.2.1 94 | `) 95 | } 96 | -------------------------------------------------------------------------------- /override/merge_links_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package override 18 | 19 | import ( 20 | "testing" 21 | ) 22 | 23 | func TestMergeExtraLinksSequence(t *testing.T) { 24 | assertMergeYaml(t, ` 25 | services: 26 | test: 27 | image: foo 28 | links: 29 | - service1 30 | - service2 31 | `, ` 32 | services: 33 | test: 34 | links: 35 | - service2=alias1 36 | `, ` 37 | services: 38 | test: 39 | image: foo 40 | links: 41 | - service1 42 | - service2=alias1 43 | `) 44 | } 45 | -------------------------------------------------------------------------------- /override/merge_logging_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package override 18 | 19 | import ( 20 | "testing" 21 | ) 22 | 23 | // override using the same logging driver will override driver options 24 | func Test_mergeYamlLoggingSameDriver(t *testing.T) { 25 | assertMergeYaml(t, ` 26 | services: 27 | test: 28 | image: foo 29 | logging: 30 | driver: syslog 31 | options: 32 | syslog-address: "tcp://192.168.0.42:123" 33 | `, ` 34 | services: 35 | test: 36 | logging: 37 | driver: syslog 38 | options: 39 | syslog-address: "tcp://127.0.0.1:123" 40 | `, ` 41 | services: 42 | test: 43 | image: foo 44 | logging: 45 | driver: syslog 46 | options: 47 | syslog-address: "tcp://127.0.0.1:123" 48 | `) 49 | } 50 | 51 | // check override with a distinct logging driver fully overrides driver options 52 | func Test_mergeYamlLoggingDistinctDriver(t *testing.T) { 53 | assertMergeYaml(t, ` 54 | services: 55 | test: 56 | image: foo 57 | logging: 58 | driver: local 59 | options: 60 | max-size: "10m" 61 | `, ` 62 | services: 63 | test: 64 | logging: 65 | driver: syslog 66 | options: 67 | syslog-address: "tcp://127.0.0.1:123" 68 | `, ` 69 | services: 70 | test: 71 | image: foo 72 | logging: 73 | driver: syslog 74 | options: 75 | syslog-address: "tcp://127.0.0.1:123" 76 | `) 77 | } 78 | 79 | // check override without an explicit driver set (defaults to local driver) 80 | func Test_mergeYamlLoggingImplicitDriver(t *testing.T) { 81 | assertMergeYaml(t, ` 82 | services: 83 | test: 84 | image: foo 85 | logging: 86 | options: 87 | max-size: "10m" 88 | `, ` 89 | services: 90 | test: 91 | logging: 92 | options: 93 | max-file: 3 94 | `, ` 95 | services: 96 | test: 97 | image: foo 98 | logging: 99 | options: 100 | max-size: "10m" 101 | max-file: 3 102 | `) 103 | } 104 | -------------------------------------------------------------------------------- /override/merge_mem_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package override 18 | 19 | import ( 20 | "testing" 21 | ) 22 | 23 | func TestMemLimitMixed(t *testing.T) { 24 | l := ` 25 | services: 26 | test: 27 | mem_limit: '256m' 28 | ` 29 | r := ` 30 | services: 31 | test: 32 | mem_limit: 1 33 | ` 34 | t.Run("StringThenInt", func(t *testing.T) { 35 | assertMergeYaml(t, r, l, ` 36 | services: 37 | test: 38 | mem_limit: 256m 39 | `) 40 | }) 41 | 42 | t.Run("IntThenString", func(t *testing.T) { 43 | assertMergeYaml(t, l, r, ` 44 | services: 45 | test: 46 | mem_limit: 1 47 | `) 48 | }) 49 | } 50 | -------------------------------------------------------------------------------- /override/merge_mounts_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package override 18 | 19 | import ( 20 | "testing" 21 | ) 22 | 23 | // override using the same logging driver will override driver options 24 | func Test_mergeYamlServiceMount(t *testing.T) { 25 | assertMergeYaml(t, ` 26 | services: 27 | test: 28 | image: foo 29 | secrets: 30 | - foo 31 | - bar 32 | - zot 33 | `, ` 34 | services: 35 | test: 36 | image: foo 37 | secrets: 38 | - source: zot 39 | target: /run/secrets/foo 40 | `, ` 41 | services: 42 | test: 43 | image: foo 44 | secrets: 45 | - source: zot 46 | target: /run/secrets/foo 47 | - bar 48 | - zot 49 | `) 50 | } 51 | -------------------------------------------------------------------------------- /override/merge_profiles_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package override 18 | 19 | import ( 20 | "testing" 21 | ) 22 | 23 | func TestMergeProfilesUnicity(t *testing.T) { 24 | assertMergeYaml(t, ` 25 | services: 26 | test: 27 | image: foo 28 | profiles: 29 | - profile1 30 | - profile2 31 | `, ` 32 | services: 33 | test: 34 | profiles: 35 | - profile2 36 | - profile3 37 | `, ` 38 | services: 39 | test: 40 | image: foo 41 | profiles: 42 | - profile1 43 | - profile2 44 | - profile3 45 | `) 46 | } 47 | -------------------------------------------------------------------------------- /override/merge_sysctls_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package override 18 | 19 | import ( 20 | "testing" 21 | ) 22 | 23 | func TestMergeSysctlsSequence(t *testing.T) { 24 | assertMergeYaml(t, ` 25 | services: 26 | test: 27 | image: foo 28 | sysctls: 29 | - net.ipv6.conf.all.disable_ipv6=1 30 | `, ` 31 | services: 32 | test: 33 | sysctls: 34 | - net.ipv4.tcp_keepalive_time=300 35 | `, ` 36 | services: 37 | test: 38 | image: foo 39 | sysctls: 40 | - net.ipv6.conf.all.disable_ipv6=1 41 | - net.ipv4.tcp_keepalive_time=300 42 | `) 43 | } 44 | 45 | func TestMergeSysctlsMapping(t *testing.T) { 46 | assertMergeYaml(t, ` 47 | services: 48 | test: 49 | image: foo 50 | sysctls: 51 | "net.ipv6.conf.all.disable_ipv6": 1 52 | `, ` 53 | services: 54 | test: 55 | sysctls: 56 | "net.ipv4.tcp_keepalive_time": "300" 57 | `, ` 58 | services: 59 | test: 60 | image: foo 61 | sysctls: 62 | - net.ipv6.conf.all.disable_ipv6=1 63 | - net.ipv4.tcp_keepalive_time=300 64 | `) 65 | } 66 | 67 | func TestMergeSysctlsMixed(t *testing.T) { 68 | assertMergeYaml(t, ` 69 | services: 70 | test: 71 | image: foo 72 | sysctls: 73 | "net.ipv6.conf.all.disable_ipv6": 1 74 | `, ` 75 | services: 76 | test: 77 | sysctls: 78 | - net.ipv4.tcp_keepalive_time=300 79 | `, ` 80 | services: 81 | test: 82 | image: foo 83 | sysctls: 84 | - net.ipv6.conf.all.disable_ipv6=1 85 | - net.ipv4.tcp_keepalive_time=300 86 | `) 87 | } 88 | 89 | func TestMergeSysctlsNumbers(t *testing.T) { 90 | assertMergeYaml(t, ` 91 | services: 92 | test: 93 | sysctls: 94 | "net.ipv4.tcp_keepalive_time": 1 95 | `, ` 96 | services: 97 | test: 98 | sysctls: 99 | "net.ipv4.tcp_keepalive_time": 3 100 | `, ` 101 | services: 102 | test: 103 | sysctls: 104 | - net.ipv4.tcp_keepalive_time=3 105 | `) 106 | } 107 | -------------------------------------------------------------------------------- /override/merge_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package override 18 | 19 | import ( 20 | "testing" 21 | 22 | "gopkg.in/yaml.v3" 23 | "gotest.tools/v3/assert" 24 | ) 25 | 26 | // override using the same logging driver will override driver options 27 | func Test_mergeOverrides(t *testing.T) { 28 | right := ` 29 | services: 30 | test: 31 | image: foo 32 | scale: 1 33 | ` 34 | left := ` 35 | services: 36 | test: 37 | image: bar 38 | scale: 2 39 | ` 40 | expected := ` 41 | services: 42 | test: 43 | image: bar 44 | scale: 2 45 | ` 46 | 47 | got, err := Merge(unmarshal(t, right), unmarshal(t, left)) 48 | assert.NilError(t, err) 49 | assert.DeepEqual(t, got, unmarshal(t, expected)) 50 | } 51 | 52 | func assertMergeYaml(t *testing.T, right string, left string, want string) { 53 | t.Helper() 54 | got, err := Merge(unmarshal(t, right), unmarshal(t, left)) 55 | assert.NilError(t, err) 56 | got, err = EnforceUnicity(got) 57 | assert.NilError(t, err) 58 | assert.DeepEqual(t, got, unmarshal(t, want)) 59 | } 60 | 61 | func unmarshal(t *testing.T, s string) map[string]any { 62 | t.Helper() 63 | var val map[string]any 64 | err := yaml.Unmarshal([]byte(s), &val) 65 | assert.NilError(t, err, s) 66 | return val 67 | } 68 | -------------------------------------------------------------------------------- /override/merge_tmpfs_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package override 18 | 19 | import ( 20 | "testing" 21 | ) 22 | 23 | func TestMergeTmpfsSequence(t *testing.T) { 24 | assertMergeYaml(t, ` 25 | services: 26 | test: 27 | image: foo 28 | tmpfs: 29 | - /foo 30 | `, ` 31 | services: 32 | test: 33 | tmpfs: 34 | - /bar 35 | - /baz 36 | - /foo 37 | `, ` 38 | services: 39 | test: 40 | image: foo 41 | tmpfs: 42 | - /foo 43 | - /bar 44 | - /baz 45 | `) 46 | } 47 | 48 | func TestMergeTmpfsString(t *testing.T) { 49 | assertMergeYaml(t, ` 50 | services: 51 | test: 52 | image: foo 53 | tmpfs: /foo 54 | `, ` 55 | services: 56 | test: 57 | tmpfs: /bar 58 | `, ` 59 | services: 60 | test: 61 | image: foo 62 | tmpfs: 63 | - /foo 64 | - /bar 65 | `) 66 | } 67 | 68 | func TestMergeTmpfsMixed(t *testing.T) { 69 | l := ` 70 | services: 71 | test: 72 | tmpfs: 73 | - /bar 74 | - /foo 75 | ` 76 | 77 | r := ` 78 | services: 79 | test: 80 | image: foo 81 | tmpfs: /foo 82 | ` 83 | 84 | t.Run("SequenceThenString", func(t *testing.T) { 85 | assertMergeYaml(t, r, l, ` 86 | services: 87 | test: 88 | image: foo 89 | tmpfs: 90 | - /foo 91 | - /bar 92 | `) 93 | }) 94 | 95 | t.Run("StringThenSequence", func(t *testing.T) { 96 | assertMergeYaml(t, l, r, ` 97 | services: 98 | test: 99 | image: foo 100 | tmpfs: 101 | - /bar 102 | - /foo 103 | `) 104 | }) 105 | } 106 | -------------------------------------------------------------------------------- /override/merge_ulimits_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package override 18 | 19 | import ( 20 | "testing" 21 | ) 22 | 23 | func Test_mergeYamlUlimits(t *testing.T) { 24 | assertMergeYaml(t, ` 25 | services: 26 | test: 27 | image: foo 28 | ulimits: 29 | nofile: 30 | soft: 20000 31 | hard: 40000 32 | nproc: 65535 33 | locks: 34 | soft: 20000 35 | hard: 40000 36 | `, ` 37 | services: 38 | test: 39 | image: foo 40 | ulimits: 41 | nofile: 42 | soft: 10000 43 | hard: 40000 44 | nproc: 45 | soft: 65535 46 | locks: 47 | hard: 65535 48 | `, ` 49 | services: 50 | test: 51 | image: foo 52 | ulimits: 53 | nofile: 54 | soft: 10000 55 | hard: 40000 56 | nproc: 57 | soft: 65535 58 | locks: 59 | hard: 65535 60 | `) 61 | } 62 | -------------------------------------------------------------------------------- /override/merge_volumes_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package override 18 | 19 | import ( 20 | "testing" 21 | ) 22 | 23 | func TestMergeVolumes(t *testing.T) { 24 | assertMergeYaml(t, ` 25 | services: 26 | test: 27 | image: foo 28 | volumes: 29 | - a:/a 30 | - type: bind 31 | source: ./b 32 | target: /b 33 | `, ` 34 | services: 35 | test: 36 | volumes: 37 | - c:/c 38 | `, ` 39 | services: 40 | test: 41 | image: foo 42 | volumes: 43 | - a:/a 44 | - type: bind 45 | source: ./b 46 | target: /b 47 | - c:/c 48 | `) 49 | } 50 | -------------------------------------------------------------------------------- /override/uncity_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package override 18 | 19 | import ( 20 | "testing" 21 | 22 | "gotest.tools/v3/assert" 23 | ) 24 | 25 | func Test_EnvironmentUnicity(t *testing.T) { 26 | assertUnicity(t, ` 27 | services: 28 | test: 29 | image: foo 30 | environment: 31 | - FOO=BAR 32 | - BAR=QIX 33 | - QIX= 34 | - ZOT 35 | - FOO=ZOT 36 | - QIX 37 | - ZOT= 38 | `, ` 39 | services: 40 | test: 41 | image: foo 42 | environment: 43 | - FOO=ZOT 44 | - BAR=QIX 45 | - QIX 46 | - ZOT= 47 | `) 48 | } 49 | 50 | func Test_VolumeUnicity(t *testing.T) { 51 | assertUnicity(t, ` 52 | services: 53 | test: 54 | image: foo 55 | volumes: 56 | - .:/foo 57 | - foo:/bar 58 | - src:/foo 59 | `, ` 60 | services: 61 | test: 62 | image: foo 63 | volumes: 64 | - src:/foo 65 | - foo:/bar 66 | `) 67 | } 68 | 69 | func Test_PortsShortUnicity(t *testing.T) { 70 | assertUnicity(t, ` 71 | services: 72 | test: 73 | image: foo 74 | ports: 75 | - "9080:80" 76 | - "9081:81" 77 | - "9080:80" 78 | - "5000" 79 | - "6060:6060/udp" 80 | - "9080:6060/udp" 81 | `, ` 82 | services: 83 | test: 84 | image: foo 85 | ports: 86 | - "9080:80" 87 | - "9081:81" 88 | - "5000" 89 | - "6060:6060/udp" 90 | - "9080:6060/udp" 91 | `) 92 | } 93 | 94 | func Test_PortsLongtUnicity(t *testing.T) { 95 | assertUnicity(t, ` 96 | services: 97 | test: 98 | image: foo 99 | ports: 100 | - target: 80 101 | host_ip: 127.0.0.1 102 | published: "8080" 103 | protocol: tcp 104 | mode: host 105 | - target: 81 106 | published: "8080" 107 | protocol: tcp 108 | - target: 80 109 | host_ip: 127.0.0.2 110 | published: "8080" 111 | protocol: tcp 112 | - target: 81 113 | published: "8080" 114 | protocol: tcp 115 | `, ` 116 | services: 117 | test: 118 | image: foo 119 | ports: 120 | - target: 80 121 | host_ip: 127.0.0.1 122 | published: "8080" 123 | protocol: tcp 124 | mode: host 125 | - target: 81 126 | published: "8080" 127 | protocol: tcp 128 | - target: 80 129 | host_ip: 127.0.0.2 130 | published: "8080" 131 | protocol: tcp 132 | `) 133 | } 134 | 135 | func assertUnicity(t *testing.T, before string, expected string) { 136 | got, err := EnforceUnicity(unmarshal(t, before)) 137 | assert.NilError(t, err) 138 | assert.DeepEqual(t, got, unmarshal(t, expected)) 139 | } 140 | -------------------------------------------------------------------------------- /package.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | /* 18 | Go reference library for parsing and loading Compose files as specified by the 19 | Compose specification https://github.com/compose-spec/compose-spec. 20 | */ 21 | package compose // import "github.com/compose-spec/compose-go" 22 | -------------------------------------------------------------------------------- /paths/context.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package paths 18 | 19 | import ( 20 | "strings" 21 | 22 | "github.com/compose-spec/compose-go/v2/types" 23 | ) 24 | 25 | func (r *relativePathsResolver) absContextPath(value any) (any, error) { 26 | v := value.(string) 27 | if strings.Contains(v, "://") { // `docker-image://` or any builder specific context type 28 | return v, nil 29 | } 30 | if strings.HasPrefix(v, types.ServicePrefix) { // `docker-image://` or any builder specific context type 31 | return v, nil 32 | } 33 | if isRemoteContext(v) { 34 | return v, nil 35 | } 36 | return r.absPath(v) 37 | } 38 | 39 | // isRemoteContext returns true if the value is a Git reference or HTTP(S) URL. 40 | // 41 | // Any other value is assumed to be a local filesystem path and returns false. 42 | // 43 | // See: https://github.com/moby/buildkit/blob/18fc875d9bfd6e065cd8211abc639434ba65aa56/frontend/dockerui/context.go#L76-L79 44 | func isRemoteContext(maybeURL string) bool { 45 | for _, prefix := range []string{"https://", "http://", "git://", "ssh://", "github.com/", "git@"} { 46 | if strings.HasPrefix(maybeURL, prefix) { 47 | return true 48 | } 49 | } 50 | return false 51 | } 52 | -------------------------------------------------------------------------------- /paths/extends.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package paths 18 | 19 | func (r *relativePathsResolver) absExtendsPath(value any) (any, error) { 20 | v := value.(string) 21 | if r.isRemoteResource(v) { 22 | return v, nil 23 | } 24 | return r.absPath(v) 25 | } 26 | -------------------------------------------------------------------------------- /paths/home.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package paths 18 | 19 | import ( 20 | "os" 21 | "path/filepath" 22 | "strings" 23 | 24 | "github.com/sirupsen/logrus" 25 | ) 26 | 27 | func ExpandUser(p string) string { 28 | if strings.HasPrefix(p, "~") { 29 | home, err := os.UserHomeDir() 30 | if err != nil { 31 | logrus.Warn("cannot expand '~', because the environment lacks HOME") 32 | return p 33 | } 34 | return filepath.Join(home, p[1:]) 35 | } 36 | return p 37 | } 38 | -------------------------------------------------------------------------------- /paths/unix.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package paths 18 | 19 | import ( 20 | "path" 21 | "path/filepath" 22 | 23 | "github.com/compose-spec/compose-go/v2/utils" 24 | ) 25 | 26 | func (r *relativePathsResolver) maybeUnixPath(a any) (any, error) { 27 | p, ok := a.(string) 28 | if !ok { 29 | return a, nil 30 | } 31 | p = ExpandUser(p) 32 | // Check if source is an absolute path (either Unix or Windows), to 33 | // handle a Windows client with a Unix daemon or vice-versa. 34 | // 35 | // Note that this is not required for Docker for Windows when specifying 36 | // a local Windows path, because Docker for Windows translates the Windows 37 | // path into a valid path within the VM. 38 | if !path.IsAbs(p) && !IsWindowsAbs(p) { 39 | if filepath.IsAbs(p) { 40 | return p, nil 41 | } 42 | return filepath.Join(r.workingDir, p), nil 43 | } 44 | return p, nil 45 | } 46 | 47 | func (r *relativePathsResolver) absSymbolicLink(value any) (any, error) { 48 | abs, err := r.absPath(value) 49 | if err != nil { 50 | return nil, err 51 | } 52 | str, ok := abs.(string) 53 | if !ok { 54 | return abs, nil 55 | } 56 | return utils.ResolveSymbolicLink(str) 57 | } 58 | -------------------------------------------------------------------------------- /paths/windows_path_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package paths 18 | 19 | // Copyright 2010 The Go Authors. All rights reserved. 20 | // Use of this source code is governed by a BSD-style 21 | // license that can be found in the LICENSE file. 22 | // https://github.com/golang/go/blob/master/LICENSE 23 | 24 | // The code in this file was copied from the Golang filepath package with some 25 | // small modifications to run it on non-Windows platforms. 26 | // https://github.com/golang/go/blob/1d0e94b1e13d5e8a323a63cd1cc1ef95290c9c36/src/path/filepath/path_test.go#L711-L763 27 | 28 | import "testing" 29 | 30 | type IsAbsTest struct { 31 | path string 32 | isAbs bool 33 | } 34 | 35 | var isabstests = []IsAbsTest{ 36 | {"", false}, 37 | {"/", true}, 38 | {"/usr/bin/gcc", true}, 39 | {"..", false}, 40 | {"/a/../bb", true}, 41 | {".", false}, 42 | {"./", false}, 43 | {"lala", false}, 44 | } 45 | 46 | var winisabstests = []IsAbsTest{ 47 | {`C:\`, true}, 48 | {`c\`, false}, 49 | {`c::`, false}, 50 | {`c:`, false}, 51 | {`/`, false}, 52 | {`\`, false}, 53 | {`\Windows`, false}, 54 | {`c:a\b`, false}, 55 | {`c:\a\b`, true}, 56 | {`c:/a/b`, true}, 57 | {`\\host\share\foo`, true}, 58 | {`//host/share/foo/bar`, true}, 59 | } 60 | 61 | func TestIsAbs(t *testing.T) { 62 | tests := winisabstests 63 | 64 | // All non-windows tests should fail, because they have no volume letter. 65 | for _, test := range isabstests { 66 | tests = append(tests, IsAbsTest{test.path, false}) 67 | } 68 | // All non-windows test should work as intended if prefixed with volume letter. 69 | for _, test := range isabstests { 70 | tests = append(tests, IsAbsTest{"c:" + test.path, test.isAbs}) 71 | } 72 | 73 | for _, test := range tests { 74 | if r := IsWindowsAbs(test.path); r != test.isAbs { 75 | t.Errorf("IsAbs(%q) = %v, want %v", test.path, r, test.isAbs) 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /schema/using-variables.yaml: -------------------------------------------------------------------------------- 1 | name: ${VARIABLE} 2 | services: 3 | foo: 4 | deploy: 5 | mode: ${VARIABLE} 6 | replicas: ${VARIABLE} 7 | rollback_config: 8 | parallelism: ${VARIABLE} 9 | delay: ${VARIABLE} 10 | failure_action: ${VARIABLE} 11 | monitor: ${VARIABLE} 12 | max_failure_ratio: ${VARIABLE} 13 | update_config: 14 | parallelism: ${VARIABLE} 15 | delay: ${VARIABLE} 16 | failure_action: ${VARIABLE} 17 | monitor: ${VARIABLE} 18 | max_failure_ratio: ${VARIABLE} 19 | resources: 20 | limits: 21 | memory: ${VARIABLE} 22 | reservations: 23 | memory: ${VARIABLE} 24 | generic_resources: 25 | - discrete_resource_spec: 26 | kind: ${VARIABLE} 27 | value: ${VARIABLE} 28 | - discrete_resource_spec: 29 | kind: ${VARIABLE} 30 | value: ${VARIABLE} 31 | restart_policy: 32 | condition: ${VARIABLE} 33 | delay: ${VARIABLE} 34 | max_attempts: ${VARIABLE} 35 | window: ${VARIABLE} 36 | placement: 37 | max_replicas_per_node: ${VARIABLE} 38 | preferences: 39 | - spread: ${VARIABLE} 40 | endpoint_mode: ${VARIABLE} 41 | expose: 42 | - ${VARIABLE} 43 | external_links: 44 | - ${VARIABLE} 45 | extra_hosts: 46 | - ${VARIABLE} 47 | hostname: ${VARIABLE} 48 | 49 | healthcheck: 50 | test: ${VARIABLE} 51 | interval: ${VARIABLE} 52 | timeout: ${VARIABLE} 53 | retries: ${VARIABLE} 54 | start_period: ${VARIABLE} 55 | start_interval: ${VARIABLE} 56 | image: ${VARIABLE} 57 | mac_address: ${VARIABLE} 58 | networks: 59 | some-network: 60 | aliases: 61 | - ${VARIABLE} 62 | other-network: 63 | ipv4_address: ${VARIABLE} 64 | ipv6_address: ${VARIABLE} 65 | mac_address: ${VARIABLE} 66 | ports: 67 | - ${VARIABLE} 68 | privileged: ${VARIABLE} 69 | read_only: ${VARIABLE} 70 | restart: ${VARIABLE} 71 | secrets: 72 | - source: ${VARIABLE} 73 | target: ${VARIABLE} 74 | uid: ${VARIABLE} 75 | gid: ${VARIABLE} 76 | mode: ${VARIABLE} 77 | stdin_open: ${VARIABLE} 78 | stop_grace_period: ${VARIABLE} 79 | stop_signal: ${VARIABLE} 80 | storage_opt: 81 | size: ${VARIABLE} 82 | sysctls: 83 | net.core.somaxconn: ${VARIABLE} 84 | tmpfs: 85 | - ${VARIABLE} 86 | tty: ${VARIABLE} 87 | ulimits: 88 | nproc: ${VARIABLE} 89 | nofile: 90 | soft: ${VARIABLE} 91 | hard: ${VARIABLE} 92 | user: ${VARIABLE} 93 | volumes: 94 | - ${VARIABLE}:${VARIABLE} 95 | - type: tmpfs 96 | target: ${VARIABLE} 97 | tmpfs: 98 | size: ${VARIABLE} 99 | 100 | networks: 101 | network: 102 | ipam: 103 | driver: ${VARIABLE} 104 | config: 105 | - subnet: ${VARIABLE} 106 | ip_range: ${VARIABLE} 107 | gateway: ${VARIABLE} 108 | aux_addresses: 109 | host1: ${VARIABLE} 110 | external-network: 111 | external: ${VARIABLE} 112 | 113 | volumes: 114 | external-volume: 115 | external: ${VARIABLE} 116 | 117 | configs: 118 | config1: 119 | external: ${VARIABLE} 120 | 121 | secrets: 122 | secret1: 123 | external: ${VARIABLE} 124 | -------------------------------------------------------------------------------- /scripts/validate/template/bash.txt: -------------------------------------------------------------------------------- 1 | # Copyright 2020 The Compose Specification Authors. 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | -------------------------------------------------------------------------------- /scripts/validate/template/dockerfile.txt: -------------------------------------------------------------------------------- 1 | # Copyright 2020 The Compose Specification Authors. 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | -------------------------------------------------------------------------------- /scripts/validate/template/go.txt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | -------------------------------------------------------------------------------- /scripts/validate/template/makefile.txt: -------------------------------------------------------------------------------- 1 | # Copyright 2020 The Compose Specification Authors. 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | -------------------------------------------------------------------------------- /transform/build.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package transform 18 | 19 | import ( 20 | "fmt" 21 | 22 | "github.com/compose-spec/compose-go/v2/tree" 23 | ) 24 | 25 | func transformBuild(data any, p tree.Path, ignoreParseError bool) (any, error) { 26 | switch v := data.(type) { 27 | case map[string]any: 28 | return transformMapping(v, p, ignoreParseError) 29 | case string: 30 | return map[string]any{ 31 | "context": v, 32 | }, nil 33 | default: 34 | return data, fmt.Errorf("%s: invalid type %T for build", p, v) 35 | } 36 | } 37 | 38 | func defaultBuildContext(data any, _ tree.Path, _ bool) (any, error) { 39 | switch v := data.(type) { 40 | case map[string]any: 41 | if _, ok := v["context"]; !ok { 42 | v["context"] = "." 43 | } 44 | return v, nil 45 | default: 46 | return data, nil 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /transform/build_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package transform 18 | 19 | import ( 20 | "reflect" 21 | "testing" 22 | 23 | "github.com/compose-spec/compose-go/v2/tree" 24 | "gotest.tools/v3/assert" 25 | ) 26 | 27 | func Test_transformBuild(t *testing.T) { 28 | tests := []struct { 29 | name string 30 | yaml any 31 | want any 32 | }{ 33 | { 34 | name: "single context string", 35 | yaml: "context_path", 36 | want: map[string]any{ 37 | "context": "context_path", 38 | }, 39 | }, 40 | { 41 | name: "mapping without context", 42 | yaml: map[string]any{ 43 | "dockerfile": "foo.Dockerfile", 44 | }, 45 | want: map[string]any{ 46 | "dockerfile": "foo.Dockerfile", 47 | }, 48 | }, 49 | } 50 | for _, tt := range tests { 51 | t.Run(tt.name, func(t *testing.T) { 52 | got, err := transformBuild(tt.yaml, tree.NewPath("services.foo.build"), false) 53 | assert.NilError(t, err) 54 | if !reflect.DeepEqual(got, tt.want) { 55 | t.Errorf("transformBuild() got = %v, want %v", got, tt.want) 56 | } 57 | }) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /transform/defaults.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package transform 18 | 19 | import ( 20 | "github.com/compose-spec/compose-go/v2/tree" 21 | ) 22 | 23 | var defaultValues = map[tree.Path]transformFunc{} 24 | 25 | func init() { 26 | defaultValues["services.*.build"] = defaultBuildContext 27 | defaultValues["services.*.secrets.*"] = defaultSecretMount 28 | defaultValues["services.*.ports.*"] = portDefaults 29 | defaultValues["services.*.deploy.resources.reservations.devices.*"] = deviceRequestDefaults 30 | defaultValues["services.*.gpus.*"] = deviceRequestDefaults 31 | } 32 | 33 | // SetDefaultValues transforms a compose model to set default values to missing attributes 34 | func SetDefaultValues(yaml map[string]any) (map[string]any, error) { 35 | result, err := setDefaults(yaml, tree.NewPath()) 36 | if err != nil { 37 | return nil, err 38 | } 39 | return result.(map[string]any), nil 40 | } 41 | 42 | func setDefaults(data any, p tree.Path) (any, error) { 43 | for pattern, transformer := range defaultValues { 44 | if p.Matches(pattern) { 45 | t, err := transformer(data, p, false) 46 | if err != nil { 47 | return nil, err 48 | } 49 | return t, nil 50 | } 51 | } 52 | switch v := data.(type) { 53 | case map[string]any: 54 | a, err := setDefaultsMapping(v, p) 55 | if err != nil { 56 | return a, err 57 | } 58 | return v, nil 59 | case []any: 60 | a, err := setDefaultsSequence(v, p) 61 | if err != nil { 62 | return a, err 63 | } 64 | return v, nil 65 | default: 66 | return data, nil 67 | } 68 | } 69 | 70 | func setDefaultsSequence(v []any, p tree.Path) ([]any, error) { 71 | for i, e := range v { 72 | t, err := setDefaults(e, p.Next("[]")) 73 | if err != nil { 74 | return nil, err 75 | } 76 | v[i] = t 77 | } 78 | return v, nil 79 | } 80 | 81 | func setDefaultsMapping(v map[string]any, p tree.Path) (map[string]any, error) { 82 | for k, e := range v { 83 | t, err := setDefaults(e, p.Next(k)) 84 | if err != nil { 85 | return nil, err 86 | } 87 | v[k] = t 88 | } 89 | return v, nil 90 | } 91 | -------------------------------------------------------------------------------- /transform/dependson.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package transform 18 | 19 | import ( 20 | "fmt" 21 | 22 | "github.com/compose-spec/compose-go/v2/tree" 23 | ) 24 | 25 | func transformDependsOn(data any, p tree.Path, _ bool) (any, error) { 26 | switch v := data.(type) { 27 | case map[string]any: 28 | for i, e := range v { 29 | d, ok := e.(map[string]any) 30 | if !ok { 31 | return nil, fmt.Errorf("%s.%s: unsupported value %s", p, i, v) 32 | } 33 | if _, ok := d["condition"]; !ok { 34 | d["condition"] = "service_started" 35 | } 36 | if _, ok := d["required"]; !ok { 37 | d["required"] = true 38 | } 39 | } 40 | return v, nil 41 | case []any: 42 | d := map[string]any{} 43 | for _, k := range v { 44 | d[k.(string)] = map[string]any{ 45 | "condition": "service_started", 46 | "required": true, 47 | } 48 | } 49 | return d, nil 50 | default: 51 | return data, fmt.Errorf("%s: invalid type %T for depend_on", p, v) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /transform/device.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package transform 18 | 19 | import ( 20 | "fmt" 21 | "strings" 22 | 23 | "github.com/compose-spec/compose-go/v2/tree" 24 | ) 25 | 26 | func transformDeviceMapping(data any, p tree.Path, ignoreParseError bool) (any, error) { 27 | switch v := data.(type) { 28 | case map[string]any: 29 | return v, nil 30 | case string: 31 | src := "" 32 | dst := "" 33 | permissions := "rwm" 34 | arr := strings.Split(v, ":") 35 | switch len(arr) { 36 | case 3: 37 | permissions = arr[2] 38 | fallthrough 39 | case 2: 40 | dst = arr[1] 41 | fallthrough 42 | case 1: 43 | src = arr[0] 44 | default: 45 | if !ignoreParseError { 46 | return nil, fmt.Errorf("confusing device mapping, please use long syntax: %s", v) 47 | } 48 | } 49 | if dst == "" { 50 | dst = src 51 | } 52 | return map[string]any{ 53 | "source": src, 54 | "target": dst, 55 | "permissions": permissions, 56 | }, nil 57 | default: 58 | return data, fmt.Errorf("%s: invalid type %T for service volume mount", p, v) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /transform/devices.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package transform 18 | 19 | import ( 20 | "fmt" 21 | 22 | "github.com/compose-spec/compose-go/v2/tree" 23 | ) 24 | 25 | func deviceRequestDefaults(data any, p tree.Path, _ bool) (any, error) { 26 | v, ok := data.(map[string]any) 27 | if !ok { 28 | return data, fmt.Errorf("%s: invalid type %T for device request", p, v) 29 | } 30 | _, hasCount := v["count"] 31 | _, hasIDs := v["device_ids"] 32 | if !hasCount && !hasIDs { 33 | v["count"] = "all" 34 | } 35 | return v, nil 36 | } 37 | -------------------------------------------------------------------------------- /transform/envfile.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package transform 18 | 19 | import ( 20 | "fmt" 21 | 22 | "github.com/compose-spec/compose-go/v2/tree" 23 | ) 24 | 25 | func transformEnvFile(data any, p tree.Path, _ bool) (any, error) { 26 | switch v := data.(type) { 27 | case string: 28 | return []any{ 29 | transformEnvFileValue(v), 30 | }, nil 31 | case []any: 32 | for i, e := range v { 33 | v[i] = transformEnvFileValue(e) 34 | } 35 | return v, nil 36 | default: 37 | return nil, fmt.Errorf("%s: invalid type %T for env_file", p, v) 38 | } 39 | } 40 | 41 | func transformEnvFileValue(data any) any { 42 | switch v := data.(type) { 43 | case string: 44 | return map[string]any{ 45 | "path": v, 46 | "required": true, 47 | } 48 | case map[string]any: 49 | if _, ok := v["required"]; !ok { 50 | v["required"] = true 51 | } 52 | return v 53 | } 54 | return nil 55 | } 56 | -------------------------------------------------------------------------------- /transform/envfile_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package transform 18 | 19 | import ( 20 | "testing" 21 | 22 | "github.com/compose-spec/compose-go/v2/tree" 23 | "gopkg.in/yaml.v3" 24 | "gotest.tools/v3/assert" 25 | ) 26 | 27 | func TestSingle(t *testing.T) { 28 | env, err := transformEnvFile(".env", tree.NewPath("service.test.env_file"), false) 29 | assert.NilError(t, err) 30 | assert.DeepEqual(t, env, []any{ 31 | map[string]any{ 32 | "path": ".env", 33 | "required": true, 34 | }, 35 | }) 36 | } 37 | 38 | func TestSequence(t *testing.T) { 39 | var in any 40 | err := yaml.Unmarshal([]byte(` 41 | - .env 42 | - other.env 43 | `), &in) 44 | assert.NilError(t, err) 45 | env, err := transformEnvFile(in, tree.NewPath("service.test.env_file"), false) 46 | assert.NilError(t, err) 47 | assert.DeepEqual(t, env, []any{ 48 | map[string]any{ 49 | "path": ".env", 50 | "required": true, 51 | }, 52 | map[string]any{ 53 | "path": "other.env", 54 | "required": true, 55 | }, 56 | }) 57 | } 58 | 59 | func TestOptional(t *testing.T) { 60 | var in any 61 | err := yaml.Unmarshal([]byte(` 62 | - .env 63 | - path: other.env 64 | required: false 65 | `), &in) 66 | assert.NilError(t, err) 67 | env, err := transformEnvFile(in, tree.NewPath("service.test.env_file"), false) 68 | assert.NilError(t, err) 69 | assert.DeepEqual(t, env, []any{ 70 | map[string]any{ 71 | "path": ".env", 72 | "required": true, 73 | }, 74 | map[string]any{ 75 | "path": "other.env", 76 | "required": false, 77 | }, 78 | }) 79 | } 80 | -------------------------------------------------------------------------------- /transform/extends.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package transform 18 | 19 | import ( 20 | "fmt" 21 | 22 | "github.com/compose-spec/compose-go/v2/tree" 23 | ) 24 | 25 | func transformExtends(data any, p tree.Path, ignoreParseError bool) (any, error) { 26 | switch v := data.(type) { 27 | case map[string]any: 28 | return transformMapping(v, p, ignoreParseError) 29 | case string: 30 | return map[string]any{ 31 | "service": v, 32 | }, nil 33 | default: 34 | return data, fmt.Errorf("%s: invalid type %T for extends", p, v) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /transform/external.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package transform 18 | 19 | import ( 20 | "fmt" 21 | 22 | "github.com/compose-spec/compose-go/v2/tree" 23 | "github.com/sirupsen/logrus" 24 | ) 25 | 26 | func transformMaybeExternal(data any, p tree.Path, ignoreParseError bool) (any, error) { 27 | if data == nil { 28 | return nil, nil 29 | } 30 | resource, err := transformMapping(data.(map[string]any), p, ignoreParseError) 31 | if err != nil { 32 | return nil, err 33 | } 34 | 35 | if ext, ok := resource["external"]; ok { 36 | name, named := resource["name"] 37 | if external, ok := ext.(map[string]any); ok { 38 | resource["external"] = true 39 | if extname, extNamed := external["name"]; extNamed { 40 | logrus.Warnf("%s: external.name is deprecated. Please set name and external: true", p) 41 | if named && extname != name { 42 | return nil, fmt.Errorf("%s: name and external.name conflict; only use name", p) 43 | } 44 | if !named { 45 | // adopt (deprecated) external.name if set 46 | resource["name"] = extname 47 | return resource, nil 48 | } 49 | } 50 | } 51 | } 52 | 53 | return resource, nil 54 | } 55 | -------------------------------------------------------------------------------- /transform/external_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package transform 18 | 19 | import ( 20 | "testing" 21 | 22 | "github.com/compose-spec/compose-go/v2/tree" 23 | "gotest.tools/v3/assert" 24 | ) 25 | 26 | func TestNotExternal(t *testing.T) { 27 | ssh, err := transformMaybeExternal(map[string]any{ 28 | "driver": "foo", 29 | }, tree.NewPath("resources.test"), false) 30 | assert.NilError(t, err) 31 | assert.DeepEqual(t, ssh, map[string]any{ 32 | "driver": "foo", 33 | }) 34 | } 35 | 36 | func TestExternalNamed(t *testing.T) { 37 | ssh, err := transformMaybeExternal(map[string]any{ 38 | "external": true, 39 | "name": "foo", 40 | }, tree.NewPath("resources.test"), false) 41 | assert.NilError(t, err) 42 | assert.DeepEqual(t, ssh, map[string]any{ 43 | "external": true, 44 | "name": "foo", 45 | }) 46 | } 47 | 48 | func TestExternalUnnamed(t *testing.T) { 49 | ssh, err := transformMaybeExternal(map[string]any{ 50 | "external": true, 51 | }, tree.NewPath("resources.test"), false) 52 | assert.NilError(t, err) 53 | assert.DeepEqual(t, ssh, map[string]any{ 54 | "external": true, 55 | }) 56 | } 57 | 58 | func TestExternalLegacy(t *testing.T) { 59 | ssh, err := transformMaybeExternal(map[string]any{ 60 | "external": map[string]any{ 61 | "name": "foo", 62 | }, 63 | }, tree.NewPath("resources.test"), false) 64 | assert.NilError(t, err) 65 | assert.DeepEqual(t, ssh, map[string]any{ 66 | "external": true, 67 | "name": "foo", 68 | }) 69 | } 70 | 71 | func TestExternalLegacyNamed(t *testing.T) { 72 | ssh, err := transformMaybeExternal(map[string]any{ 73 | "external": map[string]any{ 74 | "name": "foo", 75 | }, 76 | "name": "foo", 77 | }, tree.NewPath("resources.test"), false) 78 | assert.NilError(t, err) 79 | assert.DeepEqual(t, ssh, map[string]any{ 80 | "external": true, 81 | "name": "foo", 82 | }) 83 | } 84 | -------------------------------------------------------------------------------- /transform/gpus.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package transform 18 | 19 | import ( 20 | "fmt" 21 | 22 | "github.com/compose-spec/compose-go/v2/tree" 23 | ) 24 | 25 | func transformGpus(data any, p tree.Path, ignoreParseError bool) (any, error) { 26 | switch v := data.(type) { 27 | case []any: 28 | return transformSequence(v, p, ignoreParseError) 29 | case string: 30 | return []any{ 31 | map[string]any{ 32 | "count": "all", 33 | }, 34 | }, nil 35 | default: 36 | return data, fmt.Errorf("%s: invalid type %T for gpus", p, v) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /transform/include.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package transform 18 | 19 | import ( 20 | "fmt" 21 | 22 | "github.com/compose-spec/compose-go/v2/tree" 23 | ) 24 | 25 | func transformInclude(data any, p tree.Path, _ bool) (any, error) { 26 | switch v := data.(type) { 27 | case map[string]any: 28 | return v, nil 29 | case string: 30 | return map[string]any{ 31 | "path": v, 32 | }, nil 33 | default: 34 | return data, fmt.Errorf("%s: invalid type %T for external", p, v) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /transform/mapping.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package transform 18 | 19 | import ( 20 | "fmt" 21 | "strings" 22 | 23 | "github.com/compose-spec/compose-go/v2/tree" 24 | ) 25 | 26 | func transformKeyValue(data any, p tree.Path, ignoreParseError bool) (any, error) { 27 | switch v := data.(type) { 28 | case map[string]any: 29 | return v, nil 30 | case []any: 31 | mapping := map[string]any{} 32 | for _, e := range v { 33 | before, after, found := strings.Cut(e.(string), "=") 34 | if !found { 35 | if ignoreParseError { 36 | return data, nil 37 | } 38 | return nil, fmt.Errorf("%s: invalid value %s, expected key=value", p, e) 39 | } 40 | mapping[before] = after 41 | } 42 | return mapping, nil 43 | default: 44 | return nil, fmt.Errorf("%s: invalid type %T", p, v) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /transform/ports.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package transform 18 | 19 | import ( 20 | "fmt" 21 | 22 | "github.com/compose-spec/compose-go/v2/tree" 23 | "github.com/compose-spec/compose-go/v2/types" 24 | "github.com/go-viper/mapstructure/v2" 25 | ) 26 | 27 | func transformPorts(data any, p tree.Path, ignoreParseError bool) (any, error) { 28 | switch entries := data.(type) { 29 | case []any: 30 | // We process the list instead of individual items here. 31 | // The reason is that one entry might be mapped to multiple ServicePortConfig. 32 | // Therefore we take an input of a list and return an output of a list. 33 | var ports []any 34 | for _, entry := range entries { 35 | switch value := entry.(type) { 36 | case int: 37 | parsed, err := types.ParsePortConfig(fmt.Sprint(value)) 38 | if err != nil { 39 | return data, err 40 | } 41 | for _, v := range parsed { 42 | m, err := encode(v) 43 | if err != nil { 44 | return nil, err 45 | } 46 | ports = append(ports, m) 47 | } 48 | case string: 49 | parsed, err := types.ParsePortConfig(value) 50 | if err != nil { 51 | if ignoreParseError { 52 | return data, nil 53 | } 54 | return nil, err 55 | } 56 | if err != nil { 57 | return nil, err 58 | } 59 | for _, v := range parsed { 60 | m, err := encode(v) 61 | if err != nil { 62 | return nil, err 63 | } 64 | ports = append(ports, m) 65 | } 66 | case map[string]any: 67 | ports = append(ports, value) 68 | default: 69 | return data, fmt.Errorf("%s: invalid type %T for port", p, value) 70 | } 71 | } 72 | return ports, nil 73 | default: 74 | return data, fmt.Errorf("%s: invalid type %T for port", p, entries) 75 | } 76 | } 77 | 78 | func encode(v any) (map[string]any, error) { 79 | m := map[string]any{} 80 | decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ 81 | Result: &m, 82 | TagName: "yaml", 83 | }) 84 | if err != nil { 85 | return nil, err 86 | } 87 | err = decoder.Decode(v) 88 | return m, err 89 | } 90 | 91 | func portDefaults(data any, _ tree.Path, _ bool) (any, error) { 92 | switch v := data.(type) { 93 | case map[string]any: 94 | if _, ok := v["protocol"]; !ok { 95 | v["protocol"] = "tcp" 96 | } 97 | if _, ok := v["mode"]; !ok { 98 | v["mode"] = "ingress" 99 | } 100 | return v, nil 101 | default: 102 | return data, nil 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /transform/secrets.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package transform 18 | 19 | import ( 20 | "fmt" 21 | 22 | "github.com/compose-spec/compose-go/v2/tree" 23 | ) 24 | 25 | func transformFileMount(data any, p tree.Path, _ bool) (any, error) { 26 | switch v := data.(type) { 27 | case map[string]any: 28 | return data, nil 29 | case string: 30 | return map[string]any{ 31 | "source": v, 32 | }, nil 33 | default: 34 | return nil, fmt.Errorf("%s: unsupported type %T", p, data) 35 | } 36 | } 37 | 38 | func defaultSecretMount(data any, p tree.Path, _ bool) (any, error) { 39 | switch v := data.(type) { 40 | case map[string]any: 41 | source := v["source"] 42 | if _, ok := v["target"]; !ok { 43 | v["target"] = fmt.Sprintf("/run/secrets/%s", source) 44 | } 45 | return v, nil 46 | default: 47 | return nil, fmt.Errorf("%s: unsupported type %T", p, data) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /transform/services.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package transform 18 | 19 | import ( 20 | "github.com/compose-spec/compose-go/v2/tree" 21 | ) 22 | 23 | func transformService(data any, p tree.Path, ignoreParseError bool) (any, error) { 24 | switch value := data.(type) { 25 | case map[string]any: 26 | return transformMapping(value, p, ignoreParseError) 27 | default: 28 | return value, nil 29 | } 30 | } 31 | 32 | func transformServiceNetworks(data any, _ tree.Path, _ bool) (any, error) { 33 | if slice, ok := data.([]any); ok { 34 | networks := make(map[string]any, len(slice)) 35 | for _, net := range slice { 36 | networks[net.(string)] = nil 37 | } 38 | return networks, nil 39 | } 40 | return data, nil 41 | } 42 | -------------------------------------------------------------------------------- /transform/ssh.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package transform 18 | 19 | import ( 20 | "fmt" 21 | "strings" 22 | 23 | "github.com/compose-spec/compose-go/v2/tree" 24 | ) 25 | 26 | func transformSSH(data any, p tree.Path, _ bool) (any, error) { 27 | switch v := data.(type) { 28 | case map[string]any: 29 | return v, nil 30 | case []any: 31 | result := make(map[string]any, len(v)) 32 | for _, e := range v { 33 | s, ok := e.(string) 34 | if !ok { 35 | return nil, fmt.Errorf("invalid ssh key type %T", e) 36 | } 37 | id, path, ok := strings.Cut(s, "=") 38 | if !ok { 39 | if id != "default" { 40 | return nil, fmt.Errorf("invalid ssh key %q", s) 41 | } 42 | result[id] = nil 43 | continue 44 | } 45 | result[id] = path 46 | } 47 | return result, nil 48 | default: 49 | return data, fmt.Errorf("%s: invalid type %T for ssh", p, v) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /transform/ssh_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package transform 18 | 19 | import ( 20 | "testing" 21 | 22 | "github.com/compose-spec/compose-go/v2/tree" 23 | "gotest.tools/v3/assert" 24 | ) 25 | 26 | func TestSSHConfig(t *testing.T) { 27 | ssh, err := transformSSH([]any{ 28 | "default", 29 | "foo=bar", 30 | }, tree.NewPath("test"), false) 31 | assert.NilError(t, err) 32 | assert.DeepEqual(t, ssh, map[string]any{ 33 | "default": nil, 34 | "foo": "bar", 35 | }) 36 | } 37 | -------------------------------------------------------------------------------- /transform/ulimits.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package transform 18 | 19 | import ( 20 | "fmt" 21 | 22 | "github.com/compose-spec/compose-go/v2/tree" 23 | ) 24 | 25 | func transformUlimits(data any, p tree.Path, _ bool) (any, error) { 26 | switch v := data.(type) { 27 | case map[string]any: 28 | return v, nil 29 | case int: 30 | return v, nil 31 | default: 32 | return data, fmt.Errorf("%s: invalid type %T for external", p, v) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /transform/volume.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package transform 18 | 19 | import ( 20 | "fmt" 21 | "path" 22 | 23 | "github.com/compose-spec/compose-go/v2/format" 24 | "github.com/compose-spec/compose-go/v2/tree" 25 | ) 26 | 27 | func transformVolumeMount(data any, p tree.Path, ignoreParseError bool) (any, error) { 28 | switch v := data.(type) { 29 | case map[string]any: 30 | return v, nil 31 | case string: 32 | volume, err := format.ParseVolume(v) // TODO(ndeloof) ParseVolume should not rely on types and return map[string] 33 | if err != nil { 34 | if ignoreParseError { 35 | return v, nil 36 | } 37 | return nil, err 38 | } 39 | volume.Target = cleanTarget(volume.Target) 40 | 41 | return encode(volume) 42 | default: 43 | return data, fmt.Errorf("%s: invalid type %T for service volume mount", p, v) 44 | } 45 | } 46 | 47 | func cleanTarget(target string) string { 48 | if target == "" { 49 | return "" 50 | } 51 | return path.Clean(target) 52 | } 53 | -------------------------------------------------------------------------------- /tree/path.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package tree 18 | 19 | import ( 20 | "strings" 21 | ) 22 | 23 | const pathSeparator = "." 24 | 25 | // PathMatchAll is a token used as part of a Path to match any key at that level 26 | // in the nested structure 27 | const PathMatchAll = "*" 28 | 29 | // PathMatchList is a token used as part of a Path to match items in a list 30 | const PathMatchList = "[]" 31 | 32 | // Path is a dotted path of keys to a value in a nested mapping structure. A * 33 | // section in a path will match any key in the mapping structure. 34 | type Path string 35 | 36 | // NewPath returns a new Path 37 | func NewPath(items ...string) Path { 38 | return Path(strings.Join(items, pathSeparator)) 39 | } 40 | 41 | // Next returns a new path by append part to the current path 42 | func (p Path) Next(part string) Path { 43 | if p == "" { 44 | return Path(part) 45 | } 46 | part = strings.ReplaceAll(part, pathSeparator, "👻") 47 | return Path(string(p) + pathSeparator + part) 48 | } 49 | 50 | func (p Path) Parts() []string { 51 | return strings.Split(string(p), pathSeparator) 52 | } 53 | 54 | func (p Path) Matches(pattern Path) bool { 55 | patternParts := pattern.Parts() 56 | parts := p.Parts() 57 | 58 | if len(patternParts) != len(parts) { 59 | return false 60 | } 61 | for index, part := range parts { 62 | switch patternParts[index] { 63 | case PathMatchAll, part: 64 | continue 65 | default: 66 | return false 67 | } 68 | } 69 | return true 70 | } 71 | 72 | func (p Path) Last() string { 73 | parts := p.Parts() 74 | return parts[len(parts)-1] 75 | } 76 | 77 | func (p Path) Parent() Path { 78 | index := strings.LastIndex(string(p), pathSeparator) 79 | if index > 0 { 80 | return p[0:index] 81 | } 82 | return "" 83 | } 84 | 85 | func (p Path) String() string { 86 | return strings.ReplaceAll(string(p), "👻", pathSeparator) 87 | } 88 | -------------------------------------------------------------------------------- /tree/path_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package tree 18 | 19 | import ( 20 | "testing" 21 | 22 | "gotest.tools/v3/assert" 23 | is "gotest.tools/v3/assert/cmp" 24 | ) 25 | 26 | func TestPathMatches(t *testing.T) { 27 | testcases := []struct { 28 | doc string 29 | path Path 30 | pattern Path 31 | expected bool 32 | }{ 33 | { 34 | doc: "pattern too short", 35 | path: NewPath("one", "two", "three"), 36 | pattern: NewPath("one", "two"), 37 | }, 38 | { 39 | doc: "pattern too long", 40 | path: NewPath("one", "two"), 41 | pattern: NewPath("one", "two", "three"), 42 | }, 43 | { 44 | doc: "pattern mismatch", 45 | path: NewPath("one", "three", "two"), 46 | pattern: NewPath("one", "two", "three"), 47 | }, 48 | { 49 | doc: "pattern mismatch with match-all part", 50 | path: NewPath("one", "three", "two"), 51 | pattern: NewPath(PathMatchAll, "two", "three"), 52 | }, 53 | { 54 | doc: "pattern match with match-all part", 55 | path: NewPath("one", "two", "three"), 56 | pattern: NewPath("one", "*", "three"), 57 | expected: true, 58 | }, 59 | { 60 | doc: "pattern match", 61 | path: NewPath("one", "two", "three"), 62 | pattern: NewPath("one", "two", "three"), 63 | expected: true, 64 | }, 65 | } 66 | for _, testcase := range testcases { 67 | assert.Check(t, is.Equal(testcase.expected, testcase.path.Matches(testcase.pattern))) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /types/bytes.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package types 18 | 19 | import ( 20 | "fmt" 21 | 22 | "github.com/docker/go-units" 23 | ) 24 | 25 | // UnitBytes is the bytes type 26 | type UnitBytes int64 27 | 28 | // MarshalYAML makes UnitBytes implement yaml.Marshaller 29 | func (u UnitBytes) MarshalYAML() (interface{}, error) { 30 | return fmt.Sprintf("%d", u), nil 31 | } 32 | 33 | // MarshalJSON makes UnitBytes implement json.Marshaler 34 | func (u UnitBytes) MarshalJSON() ([]byte, error) { 35 | return []byte(fmt.Sprintf(`"%d"`, u)), nil 36 | } 37 | 38 | func (u *UnitBytes) DecodeMapstructure(value interface{}) error { 39 | switch v := value.(type) { 40 | case int: 41 | *u = UnitBytes(v) 42 | case string: 43 | b, err := units.RAMInBytes(fmt.Sprint(value)) 44 | *u = UnitBytes(b) 45 | return err 46 | } 47 | return nil 48 | } 49 | -------------------------------------------------------------------------------- /types/command.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package types 18 | 19 | import "github.com/mattn/go-shellwords" 20 | 21 | // ShellCommand is a string or list of string args. 22 | // 23 | // When marshaled to YAML, nil command fields will be omitted if `omitempty` 24 | // is specified as a struct tag. Explicitly empty commands (i.e. `[]` or 25 | // empty string will serialize to an empty array (`[]`). 26 | // 27 | // When marshaled to JSON, the `omitempty` struct must NOT be specified. 28 | // If the command field is nil, it will be serialized as `null`. 29 | // Explicitly empty commands (i.e. `[]` or empty string) will serialize to 30 | // an empty array (`[]`). 31 | // 32 | // The distinction between nil and explicitly empty is important to distinguish 33 | // between an unset value and a provided, but empty, value, which should be 34 | // preserved so that it can override any base value (e.g. container entrypoint). 35 | // 36 | // The different semantics between YAML and JSON are due to limitations with 37 | // JSON marshaling + `omitempty` in the Go stdlib, while gopkg.in/yaml.v3 gives 38 | // us more flexibility via the yaml.IsZeroer interface. 39 | // 40 | // In the future, it might make sense to make fields of this type be 41 | // `*ShellCommand` to avoid this situation, but that would constitute a 42 | // breaking change. 43 | type ShellCommand []string 44 | 45 | // IsZero returns true if the slice is nil. 46 | // 47 | // Empty (but non-nil) slices are NOT considered zero values. 48 | func (s ShellCommand) IsZero() bool { 49 | // we do NOT want len(s) == 0, ONLY explicitly nil 50 | return s == nil 51 | } 52 | 53 | // MarshalYAML returns nil (which will be serialized as `null`) for nil slices 54 | // and delegates to the standard marshaller behavior otherwise. 55 | // 56 | // NOTE: Typically the nil case here is not hit because IsZero has already 57 | // short-circuited marshalling, but this ensures that the type serializes 58 | // accurately if the `omitempty` struct tag is omitted/forgotten. 59 | // 60 | // A similar MarshalJSON() implementation is not needed because the Go stdlib 61 | // already serializes nil slices to `null`, whereas gopkg.in/yaml.v3 by default 62 | // serializes nil slices to `[]`. 63 | func (s ShellCommand) MarshalYAML() (interface{}, error) { 64 | if s == nil { 65 | return nil, nil 66 | } 67 | return []string(s), nil 68 | } 69 | 70 | func (s *ShellCommand) DecodeMapstructure(value interface{}) error { 71 | switch v := value.(type) { 72 | case string: 73 | cmd, err := shellwords.Parse(v) 74 | if err != nil { 75 | return err 76 | } 77 | *s = cmd 78 | case []interface{}: 79 | cmd := make([]string, len(v)) 80 | for i, s := range v { 81 | cmd[i] = s.(string) 82 | } 83 | *s = cmd 84 | } 85 | return nil 86 | } 87 | -------------------------------------------------------------------------------- /types/cpus.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package types 18 | 19 | import ( 20 | "fmt" 21 | "strconv" 22 | ) 23 | 24 | type NanoCPUs float32 25 | 26 | func (n *NanoCPUs) DecodeMapstructure(a any) error { 27 | switch v := a.(type) { 28 | case string: 29 | f, err := strconv.ParseFloat(v, 64) 30 | if err != nil { 31 | return err 32 | } 33 | *n = NanoCPUs(f) 34 | case int: 35 | *n = NanoCPUs(v) 36 | case float32: 37 | *n = NanoCPUs(v) 38 | case float64: 39 | *n = NanoCPUs(v) 40 | default: 41 | return fmt.Errorf("unexpected value type %T for cpus", v) 42 | } 43 | return nil 44 | } 45 | 46 | func (n *NanoCPUs) Value() float32 { 47 | return float32(*n) 48 | } 49 | -------------------------------------------------------------------------------- /types/develop.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package types 18 | 19 | type DevelopConfig struct { 20 | Watch []Trigger `yaml:"watch,omitempty" json:"watch,omitempty"` 21 | 22 | Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"` 23 | } 24 | 25 | type WatchAction string 26 | 27 | const ( 28 | WatchActionSync WatchAction = "sync" 29 | WatchActionRebuild WatchAction = "rebuild" 30 | WatchActionRestart WatchAction = "restart" 31 | WatchActionSyncRestart WatchAction = "sync+restart" 32 | WatchActionSyncExec WatchAction = "sync+exec" 33 | ) 34 | 35 | type Trigger struct { 36 | Path string `yaml:"path" json:"path"` 37 | Action WatchAction `yaml:"action" json:"action"` 38 | Target string `yaml:"target,omitempty" json:"target,omitempty"` 39 | Exec ServiceHook `yaml:"exec,omitempty" json:"exec,omitempty"` 40 | Include []string `yaml:"include,omitempty" json:"include,omitempty"` 41 | Ignore []string `yaml:"ignore,omitempty" json:"ignore,omitempty"` 42 | Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"` 43 | } 44 | -------------------------------------------------------------------------------- /types/device.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package types 18 | 19 | import ( 20 | "fmt" 21 | "strconv" 22 | "strings" 23 | ) 24 | 25 | type DeviceRequest struct { 26 | Capabilities []string `yaml:"capabilities,omitempty" json:"capabilities,omitempty"` 27 | Driver string `yaml:"driver,omitempty" json:"driver,omitempty"` 28 | Count DeviceCount `yaml:"count,omitempty" json:"count,omitempty"` 29 | IDs []string `yaml:"device_ids,omitempty" json:"device_ids,omitempty"` 30 | Options Mapping `yaml:"options,omitempty" json:"options,omitempty"` 31 | } 32 | 33 | type DeviceCount int64 34 | 35 | func (c *DeviceCount) DecodeMapstructure(value interface{}) error { 36 | switch v := value.(type) { 37 | case int: 38 | *c = DeviceCount(v) 39 | case string: 40 | if strings.ToLower(v) == "all" { 41 | *c = -1 42 | return nil 43 | } 44 | i, err := strconv.ParseInt(v, 10, 64) 45 | if err != nil { 46 | return fmt.Errorf("invalid value %q, the only value allowed is 'all' or a number", v) 47 | } 48 | *c = DeviceCount(i) 49 | default: 50 | return fmt.Errorf("invalid type %T for device count", v) 51 | } 52 | return nil 53 | } 54 | -------------------------------------------------------------------------------- /types/duration.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package types 18 | 19 | import ( 20 | "encoding/json" 21 | "fmt" 22 | "strings" 23 | "time" 24 | 25 | "github.com/xhit/go-str2duration/v2" 26 | ) 27 | 28 | // Duration is a thin wrapper around time.Duration with improved JSON marshalling 29 | type Duration time.Duration 30 | 31 | func (d Duration) String() string { 32 | return time.Duration(d).String() 33 | } 34 | 35 | func (d *Duration) DecodeMapstructure(value interface{}) error { 36 | v, err := str2duration.ParseDuration(fmt.Sprint(value)) 37 | if err != nil { 38 | return err 39 | } 40 | *d = Duration(v) 41 | return nil 42 | } 43 | 44 | // MarshalJSON makes Duration implement json.Marshaler 45 | func (d Duration) MarshalJSON() ([]byte, error) { 46 | return json.Marshal(d.String()) 47 | } 48 | 49 | // MarshalYAML makes Duration implement yaml.Marshaler 50 | func (d Duration) MarshalYAML() (interface{}, error) { 51 | return d.String(), nil 52 | } 53 | 54 | func (d *Duration) UnmarshalJSON(b []byte) error { 55 | s := strings.Trim(string(b), "\"") 56 | timeDuration, err := time.ParseDuration(s) 57 | if err != nil { 58 | return err 59 | } 60 | *d = Duration(timeDuration) 61 | return nil 62 | } 63 | -------------------------------------------------------------------------------- /types/envfile.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package types 18 | 19 | import ( 20 | "encoding/json" 21 | ) 22 | 23 | type EnvFile struct { 24 | Path string `yaml:"path,omitempty" json:"path,omitempty"` 25 | Required bool `yaml:"required" json:"required"` 26 | Format string `yaml:"format,omitempty" json:"format,omitempty"` 27 | } 28 | 29 | // MarshalYAML makes EnvFile implement yaml.Marshaler 30 | func (e EnvFile) MarshalYAML() (interface{}, error) { 31 | if e.Required { 32 | return e.Path, nil 33 | } 34 | return map[string]any{ 35 | "path": e.Path, 36 | "required": e.Required, 37 | }, nil 38 | } 39 | 40 | // MarshalJSON makes EnvFile implement json.Marshaler 41 | func (e *EnvFile) MarshalJSON() ([]byte, error) { 42 | if e.Required { 43 | return json.Marshal(e.Path) 44 | } 45 | // Pass as a value to avoid re-entering this method and use the default implementation 46 | return json.Marshal(*e) 47 | } 48 | -------------------------------------------------------------------------------- /types/fixtures/base.env: -------------------------------------------------------------------------------- 1 | FOO=foo_from_base.env 2 | INTERPOLATED_FOO=${FOO} 3 | BAR=bar_from_base.env 4 | INTERPOLATED_BAR=${BAR} 5 | ZOT=zot_from_base.env 6 | INTERPOLATED_ZOT=${ZOT} 7 | -------------------------------------------------------------------------------- /types/fixtures/override.env: -------------------------------------------------------------------------------- 1 | FOO=foo_from_override.env 2 | INTERPOLATED_FOO=${FOO} 3 | BAR=bar_from_override.env 4 | INTERPOLATED_BAR=${BAR} 5 | -------------------------------------------------------------------------------- /types/healthcheck.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package types 18 | 19 | import ( 20 | "fmt" 21 | ) 22 | 23 | // HealthCheckConfig the healthcheck configuration for a service 24 | type HealthCheckConfig struct { 25 | Test HealthCheckTest `yaml:"test,omitempty" json:"test,omitempty"` 26 | Timeout *Duration `yaml:"timeout,omitempty" json:"timeout,omitempty"` 27 | Interval *Duration `yaml:"interval,omitempty" json:"interval,omitempty"` 28 | Retries *uint64 `yaml:"retries,omitempty" json:"retries,omitempty"` 29 | StartPeriod *Duration `yaml:"start_period,omitempty" json:"start_period,omitempty"` 30 | StartInterval *Duration `yaml:"start_interval,omitempty" json:"start_interval,omitempty"` 31 | Disable bool `yaml:"disable,omitempty" json:"disable,omitempty"` 32 | 33 | Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"` 34 | } 35 | 36 | // HealthCheckTest is the command run to test the health of a service 37 | type HealthCheckTest []string 38 | 39 | func (l *HealthCheckTest) DecodeMapstructure(value interface{}) error { 40 | switch v := value.(type) { 41 | case string: 42 | *l = []string{"CMD-SHELL", v} 43 | case []interface{}: 44 | seq := make([]string, len(v)) 45 | for i, e := range v { 46 | seq[i] = e.(string) 47 | } 48 | *l = seq 49 | default: 50 | return fmt.Errorf("unexpected value type %T for healthcheck.test", value) 51 | } 52 | return nil 53 | } 54 | -------------------------------------------------------------------------------- /types/hooks.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package types 18 | 19 | // ServiceHook is a command to exec inside container by some lifecycle events 20 | type ServiceHook struct { 21 | Command ShellCommand `yaml:"command,omitempty" json:"command"` 22 | User string `yaml:"user,omitempty" json:"user,omitempty"` 23 | Privileged bool `yaml:"privileged,omitempty" json:"privileged,omitempty"` 24 | WorkingDir string `yaml:"working_dir,omitempty" json:"working_dir,omitempty"` 25 | Environment MappingWithEquals `yaml:"environment,omitempty" json:"environment,omitempty"` 26 | 27 | Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"` 28 | } 29 | -------------------------------------------------------------------------------- /types/labels.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package types 18 | 19 | import ( 20 | "fmt" 21 | "strings" 22 | ) 23 | 24 | // Labels is a mapping type for labels 25 | type Labels map[string]string 26 | 27 | func NewLabelsFromMappingWithEquals(mapping MappingWithEquals) Labels { 28 | labels := Labels{} 29 | for k, v := range mapping { 30 | if v != nil { 31 | labels[k] = *v 32 | } 33 | } 34 | return labels 35 | } 36 | 37 | func (l Labels) Add(key, value string) Labels { 38 | if l == nil { 39 | l = Labels{} 40 | } 41 | l[key] = value 42 | return l 43 | } 44 | 45 | func (l Labels) AsList() []string { 46 | s := make([]string, len(l)) 47 | i := 0 48 | for k, v := range l { 49 | s[i] = fmt.Sprintf("%s=%s", k, v) 50 | i++ 51 | } 52 | return s 53 | } 54 | 55 | func (l Labels) ToMappingWithEquals() MappingWithEquals { 56 | mapping := MappingWithEquals{} 57 | for k, v := range l { 58 | mapping[k] = &v 59 | } 60 | return mapping 61 | } 62 | 63 | // label value can be a string | number | boolean | null (empty) 64 | func labelValue(e interface{}) string { 65 | if e == nil { 66 | return "" 67 | } 68 | switch v := e.(type) { 69 | case string: 70 | return v 71 | default: 72 | return fmt.Sprint(v) 73 | } 74 | } 75 | 76 | func (l *Labels) DecodeMapstructure(value interface{}) error { 77 | switch v := value.(type) { 78 | case map[string]interface{}: 79 | labels := make(map[string]string, len(v)) 80 | for k, e := range v { 81 | labels[k] = labelValue(e) 82 | } 83 | *l = labels 84 | case []interface{}: 85 | labels := make(map[string]string, len(v)) 86 | for _, s := range v { 87 | k, e, _ := strings.Cut(fmt.Sprint(s), "=") 88 | labels[k] = labelValue(e) 89 | } 90 | *l = labels 91 | default: 92 | return fmt.Errorf("unexpected value type %T for labels", value) 93 | } 94 | return nil 95 | } 96 | -------------------------------------------------------------------------------- /types/labels_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package types 18 | 19 | import ( 20 | "testing" 21 | 22 | "gotest.tools/v3/assert" 23 | ) 24 | 25 | func TestDecodeLabel(t *testing.T) { 26 | l := Labels{} 27 | err := l.DecodeMapstructure([]any{ 28 | "a=b", 29 | "c", 30 | }) 31 | assert.NilError(t, err) 32 | assert.Equal(t, l["a"], "b") 33 | assert.Equal(t, l["c"], "") 34 | } 35 | -------------------------------------------------------------------------------- /types/options.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package types 18 | 19 | import "fmt" 20 | 21 | // Options is a mapping type for options we pass as-is to container runtime 22 | type Options map[string]string 23 | 24 | func (d *Options) DecodeMapstructure(value interface{}) error { 25 | switch v := value.(type) { 26 | case map[string]interface{}: 27 | m := make(map[string]string) 28 | for key, e := range v { 29 | if e == nil { 30 | m[key] = "" 31 | } else { 32 | m[key] = fmt.Sprint(e) 33 | } 34 | } 35 | *d = m 36 | case map[string]string: 37 | *d = v 38 | default: 39 | return fmt.Errorf("invalid type %T for options", value) 40 | } 41 | return nil 42 | } 43 | 44 | // MultiOptions allow option to be repeated 45 | type MultiOptions map[string][]string 46 | 47 | func (d *MultiOptions) DecodeMapstructure(value interface{}) error { 48 | switch v := value.(type) { 49 | case map[string]interface{}: 50 | m := make(map[string][]string) 51 | for key, e := range v { 52 | switch e := e.(type) { 53 | case []interface{}: 54 | for _, v := range e { 55 | m[key] = append(m[key], fmt.Sprint(v)) 56 | } 57 | default: 58 | m[key] = append(m[key], fmt.Sprint(e)) 59 | } 60 | } 61 | *d = m 62 | default: 63 | return fmt.Errorf("invalid type %T for options", value) 64 | } 65 | return nil 66 | } 67 | -------------------------------------------------------------------------------- /types/services.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package types 18 | 19 | // Services is a map of ServiceConfig 20 | type Services map[string]ServiceConfig 21 | 22 | // GetProfiles retrieve the profiles implicitly enabled by explicitly targeting selected services 23 | func (s Services) GetProfiles() []string { 24 | set := map[string]struct{}{} 25 | for _, service := range s { 26 | for _, p := range service.Profiles { 27 | set[p] = struct{}{} 28 | } 29 | } 30 | var profiles []string 31 | for k := range set { 32 | profiles = append(profiles, k) 33 | } 34 | return profiles 35 | } 36 | 37 | func (s Services) Filter(predicate func(ServiceConfig) bool) Services { 38 | services := Services{} 39 | for name, service := range s { 40 | if predicate(service) { 41 | services[name] = service 42 | } 43 | } 44 | return services 45 | } 46 | -------------------------------------------------------------------------------- /types/ssh.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package types 18 | 19 | import ( 20 | "fmt" 21 | ) 22 | 23 | type SSHKey struct { 24 | ID string `yaml:"id,omitempty" json:"id,omitempty"` 25 | Path string `path:"path,omitempty" json:"path,omitempty"` 26 | } 27 | 28 | // SSHConfig is a mapping type for SSH build config 29 | type SSHConfig []SSHKey 30 | 31 | func (s SSHConfig) Get(id string) (string, error) { 32 | for _, sshKey := range s { 33 | if sshKey.ID == id { 34 | return sshKey.Path, nil 35 | } 36 | } 37 | return "", fmt.Errorf("ID %s not found in SSH keys", id) 38 | } 39 | 40 | // MarshalYAML makes SSHKey implement yaml.Marshaller 41 | func (s SSHKey) MarshalYAML() (interface{}, error) { 42 | if s.Path == "" { 43 | return s.ID, nil 44 | } 45 | return fmt.Sprintf("%s: %s", s.ID, s.Path), nil 46 | } 47 | 48 | // MarshalJSON makes SSHKey implement json.Marshaller 49 | func (s SSHKey) MarshalJSON() ([]byte, error) { 50 | if s.Path == "" { 51 | return []byte(fmt.Sprintf(`%q`, s.ID)), nil 52 | } 53 | return []byte(fmt.Sprintf(`%q: %s`, s.ID, s.Path)), nil 54 | } 55 | 56 | func (s *SSHConfig) DecodeMapstructure(value interface{}) error { 57 | v, ok := value.(map[string]any) 58 | if !ok { 59 | return fmt.Errorf("invalid ssh config type %T", value) 60 | } 61 | result := make(SSHConfig, len(v)) 62 | i := 0 63 | for id, path := range v { 64 | key := SSHKey{ID: id} 65 | if path != nil { 66 | key.Path = fmt.Sprint(path) 67 | } 68 | result[i] = key 69 | i++ 70 | } 71 | *s = result 72 | return nil 73 | } 74 | -------------------------------------------------------------------------------- /types/stringOrList.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package types 18 | 19 | import "fmt" 20 | 21 | // StringList is a type for fields that can be a string or list of strings 22 | type StringList []string 23 | 24 | func (l *StringList) DecodeMapstructure(value interface{}) error { 25 | switch v := value.(type) { 26 | case string: 27 | *l = []string{v} 28 | case []interface{}: 29 | list := make([]string, len(v)) 30 | for i, e := range v { 31 | val, ok := e.(string) 32 | if !ok { 33 | return fmt.Errorf("invalid type %T for string list", value) 34 | } 35 | list[i] = val 36 | } 37 | *l = list 38 | default: 39 | return fmt.Errorf("invalid type %T for string list", value) 40 | } 41 | return nil 42 | } 43 | 44 | // StringOrNumberList is a type for fields that can be a list of strings or numbers 45 | type StringOrNumberList []string 46 | 47 | func (l *StringOrNumberList) DecodeMapstructure(value interface{}) error { 48 | switch v := value.(type) { 49 | case string: 50 | *l = []string{v} 51 | case []interface{}: 52 | list := make([]string, len(v)) 53 | for i, e := range v { 54 | list[i] = fmt.Sprint(e) 55 | } 56 | *l = list 57 | default: 58 | return fmt.Errorf("invalid type %T for string list", value) 59 | } 60 | return nil 61 | } 62 | -------------------------------------------------------------------------------- /utils/collectionutils.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package utils 18 | 19 | import ( 20 | "cmp" 21 | "maps" 22 | "slices" 23 | ) 24 | 25 | func MapKeys[T cmp.Ordered, U any](theMap map[T]U) []T { 26 | return slices.Sorted(maps.Keys(theMap)) 27 | } 28 | 29 | func MapsAppend[T comparable, U any](target map[T]U, source map[T]U) map[T]U { 30 | if target == nil { 31 | return source 32 | } 33 | if source == nil { 34 | return target 35 | } 36 | for key, value := range source { 37 | if _, ok := target[key]; !ok { 38 | target[key] = value 39 | } 40 | } 41 | return target 42 | } 43 | 44 | func ArrayContains[T comparable](source []T, toCheck []T) bool { 45 | for _, value := range toCheck { 46 | if !slices.Contains(source, value) { 47 | return false 48 | } 49 | } 50 | return true 51 | } 52 | 53 | func RemoveDuplicates[T comparable](slice []T) []T { 54 | // Create a map to store unique elements 55 | seen := make(map[T]bool) 56 | result := []T{} 57 | 58 | // Loop through the slice, adding elements to the map if they haven't been seen before 59 | for _, val := range slice { 60 | if _, ok := seen[val]; !ok { 61 | seen[val] = true 62 | result = append(result, val) 63 | } 64 | } 65 | return result 66 | } 67 | -------------------------------------------------------------------------------- /utils/pathutils.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package utils 18 | 19 | import ( 20 | "os" 21 | "path/filepath" 22 | "strings" 23 | ) 24 | 25 | // ResolveSymbolicLink converts the section of an absolute path if it is a 26 | // symbolic link 27 | // 28 | // Parameters: 29 | // - path: an absolute path 30 | // 31 | // Returns: 32 | // - converted path if it has a symbolic link or the same path if there is 33 | // no symbolic link 34 | func ResolveSymbolicLink(path string) (string, error) { 35 | sym, part, err := getSymbolinkLink(path) 36 | if err != nil { 37 | return "", err 38 | } 39 | if sym == "" && part == "" { 40 | // no symbolic link detected 41 | return path, nil 42 | } 43 | return strings.Replace(path, part, sym, 1), nil 44 | } 45 | 46 | // getSymbolinkLink parses all parts of the path and returns the 47 | // the symbolic link part as well as the correspondent original part 48 | // Parameters: 49 | // - path: an absolute path 50 | // 51 | // Returns: 52 | // - string section of the path that is a symbolic link 53 | // - string correspondent path section of the symbolic link 54 | // - An error 55 | func getSymbolinkLink(path string) (string, string, error) { 56 | parts := strings.Split(path, string(os.PathSeparator)) 57 | 58 | // Reconstruct the path step by step, checking each component 59 | var currentPath string 60 | if filepath.IsAbs(path) { 61 | currentPath = string(os.PathSeparator) 62 | } 63 | 64 | for _, part := range parts { 65 | if part == "" { 66 | continue 67 | } 68 | currentPath = filepath.Join(currentPath, part) 69 | 70 | if isSymLink := isSymbolicLink(currentPath); isSymLink { 71 | // return symbolic link, and correspondent part 72 | target, err := filepath.EvalSymlinks(currentPath) 73 | if err != nil { 74 | return "", "", err 75 | } 76 | return target, currentPath, nil 77 | } 78 | } 79 | return "", "", nil // no symbolic link 80 | } 81 | 82 | // isSymbolicLink validates if the path is a symbolic link 83 | func isSymbolicLink(path string) bool { 84 | info, err := os.Lstat(path) 85 | if err != nil { 86 | return false 87 | } 88 | 89 | // Check if the file mode indicates a symbolic link 90 | return info.Mode()&os.ModeSymlink != 0 91 | } 92 | -------------------------------------------------------------------------------- /utils/set.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package utils 18 | 19 | type Set[T comparable] map[T]struct{} 20 | 21 | func NewSet[T comparable](v ...T) Set[T] { 22 | if len(v) == 0 { 23 | return make(Set[T]) 24 | } 25 | 26 | out := make(Set[T], len(v)) 27 | for i := range v { 28 | out.Add(v[i]) 29 | } 30 | return out 31 | } 32 | 33 | func (s Set[T]) Has(v T) bool { 34 | _, ok := s[v] 35 | return ok 36 | } 37 | 38 | func (s Set[T]) Add(v T) { 39 | s[v] = struct{}{} 40 | } 41 | 42 | func (s Set[T]) AddAll(v ...T) { 43 | for _, e := range v { 44 | s[e] = struct{}{} 45 | } 46 | } 47 | 48 | func (s Set[T]) Remove(v T) bool { 49 | _, ok := s[v] 50 | if ok { 51 | delete(s, v) 52 | } 53 | return ok 54 | } 55 | 56 | func (s Set[T]) Clear() { 57 | for v := range s { 58 | delete(s, v) 59 | } 60 | } 61 | 62 | func (s Set[T]) Elements() []T { 63 | elements := make([]T, 0, len(s)) 64 | for v := range s { 65 | elements = append(elements, v) 66 | } 67 | return elements 68 | } 69 | 70 | func (s Set[T]) RemoveAll(elements ...T) { 71 | for _, e := range elements { 72 | s.Remove(e) 73 | } 74 | } 75 | 76 | func (s Set[T]) Diff(other Set[T]) Set[T] { 77 | out := make(Set[T]) 78 | for k := range s { 79 | if _, ok := other[k]; !ok { 80 | out[k] = struct{}{} 81 | } 82 | } 83 | return out 84 | } 85 | 86 | func (s Set[T]) Union(other Set[T]) Set[T] { 87 | out := make(Set[T]) 88 | for k := range s { 89 | out[k] = struct{}{} 90 | } 91 | for k := range other { 92 | out[k] = struct{}{} 93 | } 94 | return out 95 | } 96 | -------------------------------------------------------------------------------- /utils/set_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package utils 18 | 19 | import ( 20 | "testing" 21 | 22 | "github.com/stretchr/testify/require" 23 | ) 24 | 25 | func TestSet_Has(t *testing.T) { 26 | x := NewSet[string]("value") 27 | require.True(t, x.Has("value")) 28 | require.False(t, x.Has("VALUE")) 29 | } 30 | 31 | func TestSet_Diff(t *testing.T) { 32 | a := NewSet[int](1, 2) 33 | b := NewSet[int](2, 3) 34 | require.ElementsMatch(t, []int{1}, a.Diff(b).Elements()) 35 | require.ElementsMatch(t, []int{3}, b.Diff(a).Elements()) 36 | } 37 | 38 | func TestSet_Union(t *testing.T) { 39 | a := NewSet[int](1, 2) 40 | b := NewSet[int](2, 3) 41 | require.ElementsMatch(t, []int{1, 2, 3}, a.Union(b).Elements()) 42 | require.ElementsMatch(t, []int{1, 2, 3}, b.Union(a).Elements()) 43 | } 44 | -------------------------------------------------------------------------------- /utils/stringutils.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package utils 18 | 19 | import ( 20 | "fmt" 21 | "strconv" 22 | "strings" 23 | ) 24 | 25 | // StringToBool converts a string to a boolean ignoring errors 26 | func StringToBool(s string) bool { 27 | b, _ := strconv.ParseBool(strings.ToLower(strings.TrimSpace(s))) 28 | return b 29 | } 30 | 31 | // GetAsEqualsMap split key=value formatted strings into a key : value map 32 | func GetAsEqualsMap(em []string) map[string]string { 33 | m := make(map[string]string) 34 | for _, v := range em { 35 | key, val, found := strings.Cut(v, "=") 36 | if found { 37 | m[key] = val 38 | } 39 | } 40 | return m 41 | } 42 | 43 | // GetAsEqualsMap format a key : value map into key=value strings 44 | func GetAsStringList(em map[string]string) []string { 45 | m := make([]string, 0, len(em)) 46 | for k, v := range em { 47 | m = append(m, fmt.Sprintf("%s=%s", k, v)) 48 | } 49 | return m 50 | } 51 | -------------------------------------------------------------------------------- /validation/external.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package validation 18 | 19 | import ( 20 | "fmt" 21 | "strings" 22 | 23 | "github.com/compose-spec/compose-go/v2/consts" 24 | "github.com/compose-spec/compose-go/v2/tree" 25 | ) 26 | 27 | func checkExternal(v map[string]any, p tree.Path) error { 28 | b, ok := v["external"] 29 | if !ok { 30 | return nil 31 | } 32 | if !b.(bool) { 33 | return nil 34 | } 35 | 36 | for k := range v { 37 | switch k { 38 | case "name", "external", consts.Extensions: 39 | continue 40 | default: 41 | if strings.HasPrefix(k, "x-") { 42 | // custom extension, ignored 43 | continue 44 | } 45 | return fmt.Errorf("%s: conflicting parameters \"external\" and %q specified", p, k) 46 | } 47 | } 48 | return nil 49 | } 50 | -------------------------------------------------------------------------------- /validation/validation_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package validation 18 | 19 | import ( 20 | "testing" 21 | 22 | "github.com/compose-spec/compose-go/v2/tree" 23 | "gopkg.in/yaml.v3" 24 | "gotest.tools/v3/assert" 25 | ) 26 | 27 | func TestValidateSecret(t *testing.T) { 28 | checker := checks["configs.*"] 29 | tests := []struct { 30 | name string 31 | input string 32 | err string 33 | }{ 34 | { 35 | name: "file config", 36 | input: ` 37 | name: test 38 | file: ./httpd.conf 39 | `, 40 | err: "", 41 | }, 42 | { 43 | name: "environment config", 44 | input: ` 45 | name: test 46 | environment: CONFIG 47 | `, 48 | err: "", 49 | }, 50 | { 51 | name: "inlined config", 52 | input: ` 53 | name: test 54 | content: foo=bar 55 | `, 56 | err: "", 57 | }, 58 | { 59 | name: "conflict config", 60 | input: ` 61 | name: test 62 | environment: CONFIG 63 | content: foo=bar 64 | `, 65 | err: "configs.test: file|environment|content attributes are mutually exclusive", 66 | }, 67 | { 68 | name: "missing config", 69 | input: ` 70 | name: test 71 | `, 72 | err: "configs.test: one of file|environment|content must be set", 73 | }, 74 | { 75 | name: "external config", 76 | input: ` 77 | name: test 78 | external: true 79 | `, 80 | err: "", 81 | }, 82 | } 83 | for _, tt := range tests { 84 | t.Run(tt.name, func(t *testing.T) { 85 | var input map[string]any 86 | err := yaml.Unmarshal([]byte(tt.input), &input) 87 | assert.NilError(t, err) 88 | err = checker(input, tree.NewPath("configs.test")) 89 | if tt.err == "" { 90 | assert.NilError(t, err) 91 | } else { 92 | assert.Equal(t, tt.err, err.Error()) 93 | } 94 | }) 95 | } 96 | } 97 | 98 | func TestIPAddress(t *testing.T) { 99 | checker := checks["services.*.ports.*"] 100 | tests := []struct { 101 | name string 102 | input string 103 | err string 104 | }{ 105 | { 106 | name: "port long syntax, invalid IP", 107 | input: ` 108 | host_ip: notavalidip 109 | target: 1234 110 | published: "1234" 111 | `, 112 | err: "configs.test.ports[0]: invalid ip address: notavalidip", 113 | }, 114 | { 115 | name: "port long syntax, no IP", 116 | input: ` 117 | target: 1234 118 | published: "1234" 119 | `, 120 | }, 121 | { 122 | name: "port long syntax, valid IP", 123 | input: ` 124 | host_ip: 192.168.3.4 125 | target: 1234 126 | published: "1234" 127 | `, 128 | }, 129 | } 130 | 131 | for _, tt := range tests { 132 | var input map[string]any 133 | err := yaml.Unmarshal([]byte(tt.input), &input) 134 | assert.NilError(t, err) 135 | err = checker(input, tree.NewPath("configs.test.ports[0]")) 136 | if tt.err == "" { 137 | assert.NilError(t, err) 138 | } else { 139 | assert.Equal(t, tt.err, err.Error()) 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /validation/volume.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Compose Specification Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package validation 18 | 19 | import ( 20 | "fmt" 21 | 22 | "github.com/compose-spec/compose-go/v2/tree" 23 | ) 24 | 25 | func checkVolume(value any, p tree.Path) error { 26 | if value == nil { 27 | return nil 28 | } 29 | v, ok := value.(map[string]any) 30 | if !ok { 31 | return fmt.Errorf("expected volume, got %s", value) 32 | } 33 | 34 | err := checkExternal(v, p) 35 | if err != nil { 36 | return err 37 | } 38 | return nil 39 | } 40 | --------------------------------------------------------------------------------