├── .dockerignore ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Dockerfile ├── Jenkinsfile ├── LICENSE ├── MAINTAINERS ├── Makefile ├── README.md ├── cli ├── app │ ├── app.go │ ├── events.go │ ├── types.go │ └── version.go ├── command │ └── command.go ├── docker │ └── app │ │ ├── commands.go │ │ ├── factory.go │ │ └── factory_test.go ├── logger │ ├── color_logger.go │ └── colors.go └── main │ └── main.go ├── config ├── convert.go ├── convert_test.go ├── hash.go ├── interpolation.go ├── interpolation_test.go ├── marshal_config_test.go ├── merge.go ├── merge_fixtures_test.go ├── merge_test.go ├── merge_v1.go ├── merge_v2.go ├── schema.go ├── schema_helpers.go ├── testdata │ ├── .env │ ├── build-image.v2.yml │ ├── build.v1.yml │ ├── build.v2.yml │ ├── command.v1.yml │ ├── depends-on.v2.yml │ ├── dns.v1.yml │ ├── entrypoint.v1.yml │ ├── entrypoint.v2.yml │ ├── logging.v1.yml │ ├── logging.v2.yml │ ├── network-mode.v2.yml │ ├── networks-definition.v2.yml │ ├── networks.v2.yml │ ├── ulimits.v1.yml │ ├── ulimits.v2.yml │ ├── volumes-definition.v2.yml │ ├── volumes.v1.yml │ └── volumes.v2.yml ├── types.go ├── utils.go ├── validation.go └── validation_test.go ├── docker ├── auth │ └── auth.go ├── builder │ ├── builder.go │ └── builder_test.go ├── client │ ├── client.go │ ├── client_factory.go │ ├── client_factory_test.go │ ├── client_test.go │ └── fixtures │ │ ├── ca.pem │ │ ├── cert.pem │ │ └── key.pem ├── container │ ├── container.go │ └── functions.go ├── ctx │ └── context.go ├── image │ └── image.go ├── network │ ├── factory.go │ ├── network.go │ └── network_test.go ├── project.go ├── service │ ├── convert.go │ ├── convert_test.go │ ├── name.go │ ├── name_test.go │ ├── service.go │ ├── service_create.go │ ├── service_factory.go │ ├── service_test.go │ └── utils.go └── volume │ ├── volume.go │ └── volume_test.go ├── example └── main.go ├── go.mod ├── go.sum ├── hack ├── .integration-daemon-start ├── .integration-daemon-stop ├── .validate ├── binary ├── clean ├── config_schema_v1.json ├── config_schema_v2.0.json ├── cross-binary ├── dind ├── dockerversion ├── generate-sums ├── inline_schema.go ├── make.sh ├── release ├── schema_template.go ├── test-acceptance ├── test-integration ├── test-unit ├── validate-dco ├── validate-git-marks ├── validate-gofmt ├── validate-lint └── validate-vet ├── integration ├── api_event_test.go ├── api_test.go ├── assets │ ├── build │ │ ├── Dockerfile │ │ ├── docker-compose.yml │ │ └── one │ │ │ └── Dockerfile │ ├── env │ │ └── .env │ ├── interpolation │ │ └── docker-compose.yml │ ├── multiple-composefiles-default │ │ ├── docker-compose.override.yml │ │ └── docker-compose.yml │ ├── multiple │ │ ├── one.yml │ │ └── two.yml │ ├── networks │ │ ├── bridge.yml │ │ ├── default-network-config.yml │ │ ├── docker-compose.yml │ │ ├── external-default.yml │ │ ├── external-networks.yml │ │ ├── missing-network.yml │ │ ├── network-aliases.yml │ │ ├── network-mode.yml │ │ └── network-static-addresses.yml │ ├── regression │ │ ├── 60-volume_from.yml │ │ └── volume_from_container_name.yml │ ├── run │ │ └── docker-compose.yml │ ├── simple-build │ │ ├── docker-compose.yml │ │ └── one │ │ │ └── Dockerfile │ ├── v2-build-args │ │ ├── Dockerfile │ │ └── docker-compose.yml │ ├── v2-dependencies │ │ └── docker-compose.yml │ ├── v2-full │ │ ├── Dockerfile │ │ └── docker-compose.yml │ ├── v2-simple │ │ ├── docker-compose.yml │ │ └── links-invalid.yml │ ├── validation │ │ ├── invalid │ │ │ ├── docker-compose.v1.yml │ │ │ └── docker-compose.v2.yml │ │ └── valid │ │ │ ├── docker-compose.v1.yml │ │ │ └── docker-compose.v2.yml │ └── volumes │ │ └── relative-volumes.yml ├── build_test.go ├── common_test.go ├── create_test.go ├── down_test.go ├── env_test.go ├── kill_test.go ├── pause_unpause_test.go ├── ps_test.go ├── pull_test.go ├── requirements.go ├── restart_test.go ├── rm_test.go ├── run_test.go ├── scale_test.go ├── start_test.go ├── stop_test.go ├── up_test.go └── volume_test.go ├── labels ├── labels.go └── labels_test.go ├── logger ├── null.go ├── raw_logger.go └── types.go ├── lookup ├── composable.go ├── composable_test.go ├── envfile.go ├── envfile_test.go ├── file.go ├── file_test.go ├── simple_env.go └── simple_env_test.go ├── package.go ├── project ├── container.go ├── context.go ├── empty.go ├── events │ ├── events.go │ └── events_test.go ├── info.go ├── interface.go ├── listener.go ├── network.go ├── options │ ├── types.go │ └── types_test.go ├── project.go ├── project_build.go ├── project_config.go ├── project_containers.go ├── project_create.go ├── project_delete.go ├── project_down.go ├── project_events.go ├── project_kill.go ├── project_log.go ├── project_pause.go ├── project_port.go ├── project_ps.go ├── project_pull.go ├── project_restart.go ├── project_run.go ├── project_scale.go ├── project_start.go ├── project_stop.go ├── project_test.go ├── project_unpause.go ├── project_up.go ├── service-wrapper.go ├── service.go ├── utils.go └── volume.go ├── samples └── compose.yml ├── utils ├── util.go ├── util_inparallel_test.go └── util_test.go ├── version ├── version.go └── version_test.go └── yaml ├── build.go ├── build_test.go ├── command.go ├── command_test.go ├── external.go ├── external_test.go ├── network.go ├── network_test.go ├── types_yaml.go ├── types_yaml_test.go ├── ulimit.go ├── ulimit_test.go ├── volume.go └── volume_test.go /.dockerignore: -------------------------------------------------------------------------------- 1 | bundles/ 2 | **/*.test 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /main/main 3 | /docker-compose* 4 | /libcompose-cli* 5 | *.log 6 | *.swp 7 | bundles 8 | .gopath 9 | .idea 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========== 3 | 4 | # 0.0.0 (2015-07-09) 5 | 6 | ## Features 7 | - We started 8 | - No where to go but up from here 9 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to libcompose 2 | 3 | Want to hack on Libcompose? Awesome! Here are instructions to get you 4 | started. 5 | 6 | Libcompose is part of the [Docker](https://www.docker.com) project, and 7 | follows the same rules and principles. If you're already familiar with 8 | the way Docker does things, you'll feel right at home. 9 | 10 | Otherwise, go read Docker's 11 | [contributions guidelines](https://github.com/docker/docker/blob/master/CONTRIBUTING.md), 12 | [issue triaging](https://github.com/docker/docker/blob/master/project/ISSUE-TRIAGE.md), 13 | [review process](https://github.com/docker/docker/blob/master/project/REVIEWING.md) and 14 | [branches and tags](https://github.com/docker/docker/blob/master/project/BRANCHES-AND-TAGS.md). 15 | 16 | Happy hacking! 17 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # This file describes the standard way to build libcompose, using docker 2 | FROM golang:1.12.7 3 | 4 | # virtualenv is necessary to run acceptance tests 5 | RUN apt-get update && \ 6 | apt-get install -y iptables build-essential --no-install-recommends && \ 7 | apt-get install -y python-pip && \ 8 | pip install virtualenv 9 | 10 | # Install build dependencies 11 | RUN GO111MODULE=off go get github.com/aktau/github-release && \ 12 | GO111MODULE=off go get golang.org/x/tools/cmd/cover && \ 13 | GO111MODULE=off go get golang.org/x/lint/golint 14 | 15 | # Which docker version to test on and what default one to use 16 | ENV DOCKER_VERSIONS 1.9.1 1.10.3 1.13.1 17.03.2 17.06.0 17 | ENV DEFAULT_DOCKER_VERSION 17.03.2 18 | 19 | # Download docker 20 | RUN set -e; set -x; \ 21 | for v in $(echo ${DOCKER_VERSIONS} | cut -f1); do \ 22 | if test "${v}" = "1.9.1" || test "${v}" = "1.10.3"; then \ 23 | mkdir -p /usr/local/bin/docker-${v}/; \ 24 | curl https://get.docker.com/builds/Linux/x86_64/docker-${v} -o /usr/local/bin/docker-${v}/docker; \ 25 | chmod +x /usr/local/bin/docker-${v}/docker; \ 26 | elif test "${v}" = "1.13.1"; then \ 27 | curl https://get.docker.com/builds/Linux/x86_64/docker-${v}.tgz -o docker-${v}.tgz; \ 28 | tar xzf docker-${v}.tgz -C /usr/local/bin/; \ 29 | mv /usr/local/bin/docker /usr/local/bin/docker-${v}; \ 30 | rm docker-${v}.tgz; \ 31 | else \ 32 | curl https://download.docker.com/linux/static/stable/x86_64/docker-${v}-ce.tgz -o docker-${v}.tgz; \ 33 | tar xzf docker-${v}.tgz -C /usr/local/bin/; \ 34 | mv /usr/local/bin/docker /usr/local/bin/docker-${v}; \ 35 | rm docker-${v}.tgz; \ 36 | fi \ 37 | done 38 | 39 | # Set the default Docker to be run 40 | RUN ln -s /usr/local/bin/docker-${DEFAULT_DOCKER_VERSION} /usr/local/bin/docker 41 | 42 | WORKDIR /go/src/github.com/docker/libcompose 43 | 44 | # Compose COMMIT for acceptance test version, update that commit when 45 | # you want to update the acceptance test version to support. 46 | ENV COMPOSE_COMMIT 1.9.0 47 | RUN virtualenv venv && \ 48 | git clone https://github.com/docker/compose.git venv/compose && \ 49 | cd venv/compose && \ 50 | git checkout -q "$COMPOSE_COMMIT" && \ 51 | ../bin/pip install \ 52 | -r requirements.txt \ 53 | -r requirements-dev.txt 54 | 55 | ENV COMPOSE_BINARY /go/src/github.com/docker/libcompose/libcompose-cli 56 | ENV USER root 57 | 58 | # Wrap all commands in the "docker-in-docker" script to allow nested containers 59 | ENTRYPOINT ["hack/dind"] 60 | 61 | COPY . /go/src/github.com/docker/libcompose 62 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | 2 | wrappedNode(label: 'linux && x86_64') { 3 | deleteDir() 4 | checkout scm 5 | def image 6 | try { 7 | stage "build image" 8 | image = docker.build("dockerbuildbot/libcompose:${gitCommit()}") 9 | 10 | stage "validate" 11 | makeTask(image, "validate") 12 | 13 | stage "test" 14 | makeTask(image, "test", ["DAEMON_VERSION=all", "SHOWWARNING=false"]) 15 | 16 | stage "build" 17 | makeTask(image, "cross-binary") 18 | } finally { 19 | try { archive "bundles" } catch (Exception exc) {} 20 | if (image) { sh "docker rmi ${image.id} ||:" } 21 | } 22 | } 23 | 24 | def makeTask(image, task, envVars=null) { 25 | // could send in the full list of envVars for each call or provide default env vars like this: 26 | withEnv((envVars ?: []) + ["LIBCOMPOSE_IMAGE=${image.id}"]) { // would need `def image` at top level of file instead of in the nested block 27 | withChownWorkspace { 28 | timeout(60) { 29 | sh "make -e ${task}" 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /MAINTAINERS: -------------------------------------------------------------------------------- 1 | # Libcompose maintainers file 2 | # 3 | # This file describes who runs the docker/libcompose project and how. 4 | # This is a living document - if you see something out of date or missing, speak up! 5 | # 6 | # It is structured to be consumable by both humans and programs. 7 | # To extract its contents programmatically, use any TOML-compliant parser. 8 | # 9 | # This file is compiled into the MAINTAINERS file in docker/opensource. 10 | # 11 | [Org] 12 | [Org."Core maintainers"] 13 | people = [ 14 | "dnephin", 15 | "vdemeester", 16 | ] 17 | 18 | [people] 19 | 20 | # A reference list of all people associated with the project. 21 | # All other sections should refer to people by their canonical key 22 | # in the people section. 23 | 24 | # ADD YOURSELF HERE IN ALPHABETICAL ORDER 25 | [people.dnephin] 26 | Name = "Daniel Nephin" 27 | Email = "dnephin@gmail.com" 28 | GitHub = "dnephin" 29 | 30 | [people.vdemeester] 31 | Name = "Vincent Demeester" 32 | Email = "vincent@sbr.pm" 33 | GitHub = "vdemeester" 34 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all build binary clean cross-binary help test test-unit test-integration test-acceptance validate 2 | 3 | LIBCOMPOSE_ENVS := \ 4 | -e OS_PLATFORM_ARG \ 5 | -e OS_ARCH_ARG \ 6 | -e DOCKER_TEST_HOST \ 7 | -e TESTDIRS \ 8 | -e TESTFLAGS \ 9 | -e SHOWWARNING \ 10 | -e TESTVERBOSE 11 | 12 | # (default to no bind mount if DOCKER_HOST is set) 13 | BIND_DIR := $(if $(DOCKER_HOST),,bundles) 14 | LIBCOMPOSE_MOUNT := $(if $(BIND_DIR),-v "$(CURDIR)/$(BIND_DIR):/go/src/github.com/docker/libcompose/$(BIND_DIR)") 15 | 16 | GIT_BRANCH := $(shell git rev-parse --abbrev-ref HEAD 2>/dev/null) 17 | GIT_BRANCH_CLEAN := $(shell echo $(GIT_BRANCH) | sed -e "s/[^[:alnum:]]/-/g") 18 | LIBCOMPOSE_IMAGE := libcompose-dev$(if $(GIT_BRANCH_CLEAN),:$(GIT_BRANCH_CLEAN)) 19 | 20 | DAEMON_VERSION := $(if $(DAEMON_VERSION),$(DAEMON_VERSION),"default") 21 | TTY := $(shell [ -t 0 ] && echo "-t") 22 | DOCKER_RUN_LIBCOMPOSE := docker run --rm -i $(TTY) --privileged -e DAEMON_VERSION="$(DAEMON_VERSION)" $(LIBCOMPOSE_ENVS) $(LIBCOMPOSE_MOUNT) "$(LIBCOMPOSE_IMAGE)" 23 | 24 | default: binary 25 | 26 | all: build ## validate all checks, build linux binary, run all tests\ncross build non-linux binaries 27 | $(DOCKER_RUN_LIBCOMPOSE) ./hack/make.sh 28 | 29 | binary: build ## build the linux binary 30 | $(DOCKER_RUN_LIBCOMPOSE) ./hack/make.sh binary 31 | 32 | cross-binary: build ## cross build the non linux binaries (windows, darwin) 33 | $(DOCKER_RUN_LIBCOMPOSE) ./hack/make.sh cross-binary 34 | 35 | test: build ## run the unit, integration and acceptance tests 36 | $(DOCKER_RUN_LIBCOMPOSE) ./hack/make.sh binary test-unit test-integration test-acceptance 37 | 38 | test-unit: build ## run the unit tests 39 | $(DOCKER_RUN_LIBCOMPOSE) ./hack/make.sh test-unit 40 | 41 | test-integration: build ## run the integration tests 42 | $(DOCKER_RUN_LIBCOMPOSE) ./hack/make.sh binary test-integration 43 | 44 | test-acceptance: build ## run the acceptance tests 45 | $(DOCKER_RUN_LIBCOMPOSE) ./hack/make.sh binary test-acceptance 46 | 47 | validate: build ## validate DCO, git conflicts marks, gofmt, golint and go vet 48 | $(DOCKER_RUN_LIBCOMPOSE) ./hack/make.sh validate-dco validate-git-marks validate-gofmt validate-lint validate-vet 49 | 50 | shell: build ## start a shell inside the build env 51 | $(DOCKER_RUN_LIBCOMPOSE) bash 52 | 53 | # Build the docker image, should be prior almost any other goals 54 | build: bundles 55 | docker build -t "$(LIBCOMPOSE_IMAGE)" . 56 | 57 | bundles: 58 | mkdir bundles 59 | 60 | clean: 61 | $(DOCKER_RUN_LIBCOMPOSE) ./hack/make.sh clean 62 | 63 | help: ## this help 64 | @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {sub("\\\\n",sprintf("\n%22c"," "), $$2);printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) 65 | -------------------------------------------------------------------------------- /cli/app/events.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | "strings" 8 | 9 | "golang.org/x/net/context" 10 | 11 | "github.com/docker/libcompose/project" 12 | "github.com/docker/libcompose/project/events" 13 | "github.com/sirupsen/logrus" 14 | "github.com/urfave/cli" 15 | ) 16 | 17 | // ProjectEvents listen for real-time events of containers. 18 | func ProjectEvents(p project.APIProject, c *cli.Context) error { 19 | evts, err := p.Events(context.Background(), c.Args()...) 20 | if err != nil { 21 | return err 22 | } 23 | var printfn func(events.ContainerEvent) 24 | 25 | if c.Bool("json") { 26 | printfn = printJSON 27 | } else { 28 | printfn = printStd 29 | } 30 | for event := range evts { 31 | printfn(event) 32 | } 33 | return nil 34 | } 35 | 36 | func printStd(event events.ContainerEvent) { 37 | output := os.Stdout 38 | fmt.Fprintf(output, "%s ", event.Time.Format("2006-01-02 15:04:05.999999999")) 39 | fmt.Fprintf(output, "%s %s %s", event.Type, event.Event, event.ID) 40 | attrs := []string{} 41 | for attr, value := range event.Attributes { 42 | attrs = append(attrs, fmt.Sprintf("%s=%s", attr, value)) 43 | } 44 | 45 | fmt.Fprintf(output, " (%s)", strings.Join(attrs, ", ")) 46 | fmt.Fprint(output, "\n") 47 | } 48 | 49 | func printJSON(event events.ContainerEvent) { 50 | json, err := json.Marshal(event) 51 | if err != nil { 52 | logrus.Warn(err) 53 | } 54 | output := os.Stdout 55 | fmt.Fprintf(output, "%s", json) 56 | } 57 | -------------------------------------------------------------------------------- /cli/app/types.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "github.com/docker/libcompose/project" 5 | "github.com/urfave/cli" 6 | ) 7 | 8 | // ProjectFactory is an interface that helps creating libcompose project. 9 | type ProjectFactory interface { 10 | // Create creates a libcompose project from the command line options (urfave cli context). 11 | Create(c *cli.Context) (project.APIProject, error) 12 | } 13 | -------------------------------------------------------------------------------- /cli/app/version.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "runtime" 7 | "text/template" 8 | 9 | "github.com/docker/libcompose/version" 10 | "github.com/sirupsen/logrus" 11 | "github.com/urfave/cli" 12 | ) 13 | 14 | var versionTemplate = `Version: {{.Version}} ({{.GitCommit}}) 15 | Go version: {{.GoVersion}} 16 | Built: {{.BuildTime}} 17 | OS/Arch: {{.Os}}/{{.Arch}}` 18 | 19 | // Version prints the libcompose version number and additionnal informations. 20 | func Version(c *cli.Context) error { 21 | if c.Bool("short") { 22 | fmt.Println(version.VERSION) 23 | return nil 24 | } 25 | 26 | tmpl, err := template.New("").Parse(versionTemplate) 27 | if err != nil { 28 | logrus.Fatal(err) 29 | } 30 | 31 | v := struct { 32 | Version string 33 | GitCommit string 34 | GoVersion string 35 | BuildTime string 36 | Os string 37 | Arch string 38 | }{ 39 | Version: version.VERSION, 40 | GitCommit: version.GITCOMMIT, 41 | GoVersion: runtime.Version(), 42 | BuildTime: version.BUILDTIME, 43 | Os: runtime.GOOS, 44 | Arch: runtime.GOARCH, 45 | } 46 | 47 | if err := tmpl.Execute(os.Stdout, v); err != nil { 48 | logrus.Fatal(err) 49 | } 50 | fmt.Printf("\n") 51 | return nil 52 | } 53 | -------------------------------------------------------------------------------- /cli/docker/app/commands.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "github.com/docker/libcompose/cli/command" 5 | "github.com/docker/libcompose/docker/client" 6 | "github.com/docker/libcompose/docker/ctx" 7 | "github.com/sirupsen/logrus" 8 | "github.com/urfave/cli" 9 | ) 10 | 11 | // DockerClientFlags defines the flags that are specific to the docker client, 12 | // like configdir or tls related flags. 13 | func DockerClientFlags() []cli.Flag { 14 | return []cli.Flag{ 15 | cli.BoolFlag{ 16 | Name: "tls", 17 | Usage: "Use TLS; implied by --tlsverify", 18 | }, 19 | cli.BoolFlag{ 20 | Name: "tlsverify", 21 | Usage: "Use TLS and verify the remote", 22 | EnvVar: "DOCKER_TLS_VERIFY", 23 | }, 24 | cli.StringFlag{ 25 | Name: "tlscacert", 26 | Usage: "Trust certs signed only by this CA", 27 | }, 28 | cli.StringFlag{ 29 | Name: "tlscert", 30 | Usage: "Path to TLS certificate file", 31 | }, 32 | cli.StringFlag{ 33 | Name: "tlskey", 34 | Usage: "Path to TLS key file", 35 | }, 36 | cli.StringFlag{ 37 | Name: "configdir", 38 | Usage: "Path to docker config dir, default ${HOME}/.docker", 39 | }, 40 | } 41 | } 42 | 43 | // Populate updates the specified docker context based on command line arguments and subcommands. 44 | func Populate(context *ctx.Context, c *cli.Context) { 45 | command.Populate(&context.Context, c) 46 | 47 | context.ConfigDir = c.String("configdir") 48 | 49 | opts := client.Options{} 50 | opts.TLS = c.GlobalBool("tls") 51 | opts.TLSVerify = c.GlobalBool("tlsverify") 52 | opts.TLSOptions.CAFile = c.GlobalString("tlscacert") 53 | opts.TLSOptions.CertFile = c.GlobalString("tlscert") 54 | opts.TLSOptions.KeyFile = c.GlobalString("tlskey") 55 | 56 | clientFactory, err := client.NewDefaultFactory(opts) 57 | if err != nil { 58 | logrus.Fatalf("Failed to construct Docker client: %v", err) 59 | } 60 | 61 | context.ClientFactory = clientFactory 62 | } 63 | -------------------------------------------------------------------------------- /cli/docker/app/factory.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "github.com/docker/libcompose/cli/logger" 5 | "github.com/docker/libcompose/docker" 6 | "github.com/docker/libcompose/docker/ctx" 7 | "github.com/docker/libcompose/project" 8 | "github.com/urfave/cli" 9 | ) 10 | 11 | // ProjectFactory is a struct that holds the app.ProjectFactory implementation. 12 | type ProjectFactory struct { 13 | } 14 | 15 | // Create implements ProjectFactory.Create using docker client. 16 | func (p *ProjectFactory) Create(c *cli.Context) (project.APIProject, error) { 17 | context := &ctx.Context{} 18 | context.LoggerFactory = logger.NewColorLoggerFactory() 19 | Populate(context, c) 20 | return docker.NewProject(context, nil) 21 | } 22 | -------------------------------------------------------------------------------- /cli/logger/color_logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "strconv" 8 | 9 | "github.com/docker/libcompose/logger" 10 | "golang.org/x/crypto/ssh/terminal" 11 | ) 12 | 13 | // ColorLoggerFactory implements logger.Factory interface using ColorLogger. 14 | type ColorLoggerFactory struct { 15 | maxLength int 16 | tty bool 17 | } 18 | 19 | // ColorLogger implements logger.Logger interface with color support. 20 | type ColorLogger struct { 21 | name string 22 | colorPrefix string 23 | factory *ColorLoggerFactory 24 | } 25 | 26 | // NewColorLoggerFactory creates a new ColorLoggerFactory. 27 | func NewColorLoggerFactory() *ColorLoggerFactory { 28 | return &ColorLoggerFactory{ 29 | tty: terminal.IsTerminal(int(os.Stdout.Fd())), 30 | } 31 | } 32 | 33 | // CreateContainerLogger implements logger.Factory.CreateContainerLogger. 34 | func (c *ColorLoggerFactory) CreateContainerLogger(name string) logger.Logger { 35 | return c.create(name) 36 | } 37 | 38 | // CreateBuildLogger implements logger.Factory.CreateBuildLogger. 39 | func (c *ColorLoggerFactory) CreateBuildLogger(name string) logger.Logger { 40 | return &logger.RawLogger{} 41 | } 42 | 43 | // CreatePullLogger implements logger.Factory.CreatePullLogger. 44 | func (c *ColorLoggerFactory) CreatePullLogger(name string) logger.Logger { 45 | return &logger.NullLogger{} 46 | } 47 | 48 | // CreateBuildLogger implements logger.Factory.CreateContainerLogger. 49 | func (c *ColorLoggerFactory) create(name string) logger.Logger { 50 | if c.maxLength < len(name) { 51 | c.maxLength = len(name) 52 | } 53 | 54 | return &ColorLogger{ 55 | name: name, 56 | factory: c, 57 | colorPrefix: <-colorPrefix, 58 | } 59 | } 60 | 61 | // Out implements logger.Logger.Out. 62 | func (c *ColorLogger) Out(bytes []byte) { 63 | if len(bytes) == 0 { 64 | return 65 | } 66 | logFmt, name := c.getLogFmt() 67 | message := fmt.Sprintf(logFmt, name, string(bytes)) 68 | fmt.Print(message) 69 | } 70 | 71 | // Err implements logger.Logger.Err. 72 | func (c *ColorLogger) Err(bytes []byte) { 73 | if len(bytes) == 0 { 74 | return 75 | } 76 | logFmt, name := c.getLogFmt() 77 | message := fmt.Sprintf(logFmt, name, string(bytes)) 78 | fmt.Fprint(os.Stderr, message) 79 | } 80 | 81 | // OutWriter returns the base writer 82 | func (c *ColorLogger) OutWriter() io.Writer { 83 | return os.Stdout 84 | } 85 | 86 | // ErrWriter returns the base writer 87 | func (c *ColorLogger) ErrWriter() io.Writer { 88 | return os.Stderr 89 | } 90 | 91 | func (c *ColorLogger) getLogFmt() (string, string) { 92 | pad := c.factory.maxLength 93 | 94 | logFmt := "%s | %s" 95 | if c.factory.tty { 96 | logFmt = c.colorPrefix + " %s" 97 | } 98 | 99 | name := fmt.Sprintf("%-"+strconv.Itoa(pad)+"s", c.name) 100 | 101 | return logFmt, name 102 | } 103 | -------------------------------------------------------------------------------- /cli/logger/colors.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import "fmt" 4 | 5 | var ( 6 | colorPrefix = make(chan string) 7 | ) 8 | 9 | func generateColors() { 10 | i := 0 11 | colorOrder := []string{ 12 | "36", // cyan 13 | "33", // yellow 14 | "32", // green 15 | "35", // magenta 16 | "31", // red 17 | "34", // blue 18 | "36;1", // intense cyan 19 | "33;1", // intense yellow 20 | "32;1", // intense green 21 | "35;1", // intense magenta 22 | "31;1", // intense red 23 | "34;1", // intense blue 24 | } 25 | 26 | for { 27 | colorPrefix <- fmt.Sprintf("\033[%sm%%s |\033[0m", colorOrder[i]) 28 | i = (i + 1) % len(colorOrder) 29 | } 30 | } 31 | 32 | func init() { 33 | go generateColors() 34 | } 35 | -------------------------------------------------------------------------------- /cli/main/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "path" 6 | 7 | cliApp "github.com/docker/libcompose/cli/app" 8 | "github.com/docker/libcompose/cli/command" 9 | dockerApp "github.com/docker/libcompose/cli/docker/app" 10 | "github.com/docker/libcompose/version" 11 | "github.com/urfave/cli" 12 | ) 13 | 14 | func main() { 15 | factory := &dockerApp.ProjectFactory{} 16 | 17 | cli.AppHelpTemplate = `Usage: {{.Name}} {{if .Flags}}[OPTIONS] {{end}}COMMAND [arg...] 18 | 19 | {{.Usage}} 20 | 21 | Version: {{.Version}}{{if or .Author .Email}} 22 | 23 | Author:{{if .Author}} 24 | {{.Author}}{{if .Email}} - <{{.Email}}>{{end}}{{else}} 25 | {{.Email}}{{end}}{{end}} 26 | {{if .Flags}} 27 | Options: 28 | {{range .Flags}}{{.}} 29 | {{end}}{{end}} 30 | Commands: 31 | {{range .Commands}}{{.Name}}{{with .ShortName}}, {{.}}{{end}}{{ "\t" }}{{.Usage}} 32 | {{end}} 33 | Run '{{.Name}} COMMAND --help' for more information on a command. 34 | ` 35 | cli.CommandHelpTemplate = `Usage: ` + path.Base(os.Args[0]) + ` {{.Name}}{{if .Flags}} [OPTIONS] 36 | 37 | {{.Usage}} 38 | 39 | Options: 40 | {{range .Flags}}{{.}} 41 | {{end}}{{end}} 42 | ` 43 | 44 | app := cli.NewApp() 45 | app.Name = "libcompose-cli" 46 | app.Usage = "Command line interface for libcompose." 47 | app.Version = version.VERSION + " (" + version.GITCOMMIT + ")" 48 | app.Author = "Docker Compose Contributors" 49 | app.Email = "https://github.com/docker/libcompose" 50 | app.Before = cliApp.BeforeApp 51 | app.Flags = append(command.CommonFlags(), dockerApp.DockerClientFlags()...) 52 | app.Commands = []cli.Command{ 53 | command.BuildCommand(factory), 54 | command.ConfigCommand(factory), 55 | command.CreateCommand(factory), 56 | command.EventsCommand(factory), 57 | command.DownCommand(factory), 58 | command.KillCommand(factory), 59 | command.LogsCommand(factory), 60 | command.PauseCommand(factory), 61 | command.PortCommand(factory), 62 | command.PsCommand(factory), 63 | command.PullCommand(factory), 64 | command.RestartCommand(factory), 65 | command.RmCommand(factory), 66 | command.RunCommand(factory), 67 | command.ScaleCommand(factory), 68 | command.StartCommand(factory), 69 | command.StopCommand(factory), 70 | command.UnpauseCommand(factory), 71 | command.UpCommand(factory), 72 | command.VersionCommand(factory), 73 | } 74 | 75 | app.Run(os.Args) 76 | 77 | } 78 | -------------------------------------------------------------------------------- /config/convert.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/docker/libcompose/utils" 5 | "github.com/docker/libcompose/yaml" 6 | ) 7 | 8 | // ConvertServices converts a set of v1 service configs to v2 service configs 9 | func ConvertServices(v1Services map[string]*ServiceConfigV1) (map[string]*ServiceConfig, error) { 10 | v2Services := make(map[string]*ServiceConfig) 11 | replacementFields := make(map[string]*ServiceConfig) 12 | 13 | for name, service := range v1Services { 14 | replacementFields[name] = &ServiceConfig{ 15 | Build: yaml.Build{ 16 | Context: service.Build, 17 | Dockerfile: service.Dockerfile, 18 | }, 19 | Logging: Log{ 20 | Driver: service.LogDriver, 21 | Options: service.LogOpt, 22 | }, 23 | NetworkMode: service.Net, 24 | } 25 | 26 | v1Services[name].Build = "" 27 | v1Services[name].Dockerfile = "" 28 | v1Services[name].LogDriver = "" 29 | v1Services[name].LogOpt = nil 30 | v1Services[name].Net = "" 31 | } 32 | 33 | if err := utils.Convert(v1Services, &v2Services); err != nil { 34 | return nil, err 35 | } 36 | 37 | for name := range v2Services { 38 | v2Services[name].Build = replacementFields[name].Build 39 | v2Services[name].Logging = replacementFields[name].Logging 40 | v2Services[name].NetworkMode = replacementFields[name].NetworkMode 41 | } 42 | 43 | return v2Services, nil 44 | } 45 | -------------------------------------------------------------------------------- /config/convert_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/docker/libcompose/yaml" 8 | ) 9 | 10 | func TestBuild(t *testing.T) { 11 | v2Services, err := ConvertServices(map[string]*ServiceConfigV1{ 12 | "test": { 13 | Build: ".", 14 | Dockerfile: "Dockerfile", 15 | }, 16 | }) 17 | if err != nil { 18 | t.Fatal(err) 19 | } 20 | 21 | v2Config := v2Services["test"] 22 | 23 | expectedBuild := yaml.Build{ 24 | Context: ".", 25 | Dockerfile: "Dockerfile", 26 | } 27 | 28 | if !reflect.DeepEqual(v2Config.Build, expectedBuild) { 29 | t.Fatal("Failed to convert build", v2Config.Build) 30 | } 31 | } 32 | 33 | func TestLogging(t *testing.T) { 34 | v2Services, err := ConvertServices(map[string]*ServiceConfigV1{ 35 | "test": { 36 | LogDriver: "json-file", 37 | LogOpt: map[string]string{ 38 | "key": "value", 39 | }, 40 | }, 41 | }) 42 | if err != nil { 43 | t.Fatal(err) 44 | } 45 | 46 | v2Config := v2Services["test"] 47 | 48 | expectedLogging := Log{ 49 | Driver: "json-file", 50 | Options: map[string]string{ 51 | "key": "value", 52 | }, 53 | } 54 | 55 | if !reflect.DeepEqual(v2Config.Logging, expectedLogging) { 56 | t.Fatal("Failed to convert logging", v2Config.Logging) 57 | } 58 | } 59 | 60 | func TestNetworkMode(t *testing.T) { 61 | v2Services, err := ConvertServices(map[string]*ServiceConfigV1{ 62 | "test": { 63 | Net: "host", 64 | }, 65 | }) 66 | if err != nil { 67 | t.Fatal(err) 68 | } 69 | 70 | v2Config := v2Services["test"] 71 | 72 | if v2Config.NetworkMode != "host" { 73 | t.Fatal("Failed to convert network mode", v2Config.NetworkMode) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /config/hash.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "crypto/sha1" 5 | "encoding/hex" 6 | "fmt" 7 | "io" 8 | "reflect" 9 | "sort" 10 | 11 | "github.com/docker/libcompose/yaml" 12 | ) 13 | 14 | // GetServiceHash computes and returns a hash that will identify a service. 15 | // This hash will be then used to detect if the service definition/configuration 16 | // have changed and needs to be recreated. 17 | func GetServiceHash(name string, config *ServiceConfig) string { 18 | hash := sha1.New() 19 | 20 | io.WriteString(hash, name) 21 | 22 | //Get values of Service through reflection 23 | val := reflect.ValueOf(config).Elem() 24 | 25 | //Create slice to sort the keys in Service Config, which allow constant hash ordering 26 | serviceKeys := []string{} 27 | 28 | //Create a data structure of map of values keyed by a string 29 | unsortedKeyValue := make(map[string]interface{}) 30 | 31 | //Get all keys and values in Service Configuration 32 | for i := 0; i < val.NumField(); i++ { 33 | valueField := val.Field(i) 34 | keyField := val.Type().Field(i) 35 | 36 | serviceKeys = append(serviceKeys, keyField.Name) 37 | unsortedKeyValue[keyField.Name] = valueField.Interface() 38 | } 39 | 40 | //Sort serviceKeys alphabetically 41 | sort.Strings(serviceKeys) 42 | 43 | //Go through keys and write hash 44 | for _, serviceKey := range serviceKeys { 45 | serviceValue := unsortedKeyValue[serviceKey] 46 | 47 | io.WriteString(hash, fmt.Sprintf("\n %v: ", serviceKey)) 48 | 49 | switch s := serviceValue.(type) { 50 | case yaml.SliceorMap: 51 | sliceKeys := []string{} 52 | for lkey := range s { 53 | sliceKeys = append(sliceKeys, lkey) 54 | } 55 | sort.Strings(sliceKeys) 56 | 57 | for _, sliceKey := range sliceKeys { 58 | io.WriteString(hash, fmt.Sprintf("%s=%v, ", sliceKey, s[sliceKey])) 59 | } 60 | case yaml.MaporEqualSlice: 61 | for _, sliceKey := range s { 62 | io.WriteString(hash, fmt.Sprintf("%s, ", sliceKey)) 63 | } 64 | case yaml.MaporColonSlice: 65 | for _, sliceKey := range s { 66 | io.WriteString(hash, fmt.Sprintf("%s, ", sliceKey)) 67 | } 68 | case yaml.MaporSpaceSlice: 69 | for _, sliceKey := range s { 70 | io.WriteString(hash, fmt.Sprintf("%s, ", sliceKey)) 71 | } 72 | case yaml.Command: 73 | for _, sliceKey := range s { 74 | io.WriteString(hash, fmt.Sprintf("%s, ", sliceKey)) 75 | } 76 | case yaml.Stringorslice: 77 | sort.Strings(s) 78 | 79 | for _, sliceKey := range s { 80 | io.WriteString(hash, fmt.Sprintf("%s, ", sliceKey)) 81 | } 82 | case []string: 83 | sliceKeys := s 84 | sort.Strings(sliceKeys) 85 | 86 | for _, sliceKey := range sliceKeys { 87 | io.WriteString(hash, fmt.Sprintf("%s, ", sliceKey)) 88 | } 89 | case *yaml.Networks: 90 | io.WriteString(hash, fmt.Sprintf("%s, ", s.HashString())) 91 | case *yaml.Volumes: 92 | io.WriteString(hash, fmt.Sprintf("%s, ", s.HashString())) 93 | default: 94 | io.WriteString(hash, fmt.Sprintf("%v, ", serviceValue)) 95 | } 96 | } 97 | 98 | return hex.EncodeToString(hash.Sum(nil)) 99 | } 100 | -------------------------------------------------------------------------------- /config/marshal_config_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "testing" 5 | 6 | yamlTypes "github.com/docker/libcompose/yaml" 7 | "github.com/stretchr/testify/assert" 8 | "gopkg.in/yaml.v2" 9 | ) 10 | 11 | type TestConfig struct { 12 | SystemContainers map[string]*ServiceConfig 13 | } 14 | 15 | func newTestConfig() TestConfig { 16 | return TestConfig{ 17 | SystemContainers: map[string]*ServiceConfig{ 18 | "udev": { 19 | Image: "udev", 20 | Restart: "always", 21 | NetworkMode: "host", 22 | Privileged: true, 23 | DNS: []string{"8.8.8.8", "8.8.4.4"}, 24 | Environment: yamlTypes.MaporEqualSlice([]string{ 25 | "DAEMON=true", 26 | }), 27 | Labels: yamlTypes.SliceorMap{ 28 | "io.rancher.os.detach": "true", 29 | "io.rancher.os.scope": "system", 30 | }, 31 | VolumesFrom: []string{ 32 | "system-volumes", 33 | }, 34 | Ulimits: yamlTypes.Ulimits{ 35 | Elements: []yamlTypes.Ulimit{ 36 | yamlTypes.NewUlimit("nproc", 65557, 65557), 37 | }, 38 | }, 39 | }, 40 | "system-volumes": { 41 | Image: "state", 42 | NetworkMode: "none", 43 | ReadOnly: true, 44 | Privileged: true, 45 | Labels: yamlTypes.SliceorMap{ 46 | "io.rancher.os.createonly": "true", 47 | "io.rancher.os.scope": "system", 48 | }, 49 | Volumes: &yamlTypes.Volumes{ 50 | Volumes: []*yamlTypes.Volume{ 51 | { 52 | Source: "/dev", 53 | Destination: "/host/dev", 54 | }, 55 | { 56 | Source: "/var/lib/rancher/conf", 57 | Destination: "/var/lib/rancher/conf", 58 | }, 59 | { 60 | Source: "/etc/ssl/certs/ca-certificates.crt", 61 | Destination: "/etc/ssl/certs/ca-certificates.crt.rancher", 62 | }, 63 | { 64 | Source: "/lib/modules", 65 | Destination: "lib/modules", 66 | }, 67 | { 68 | Source: "/lib/firmware", 69 | Destination: "/lib/firmware", 70 | }, 71 | { 72 | Source: "/var/run", 73 | Destination: "/var/run", 74 | }, 75 | { 76 | Source: "/var/log", 77 | Destination: "/var/log", 78 | }, 79 | }, 80 | }, 81 | Logging: Log{ 82 | Driver: "json-file", 83 | }, 84 | }, 85 | }, 86 | } 87 | } 88 | 89 | func TestMarshalConfig(t *testing.T) { 90 | config := newTestConfig() 91 | bytes, err := yaml.Marshal(config) 92 | assert.Nil(t, err) 93 | 94 | config2 := TestConfig{} 95 | 96 | err = yaml.Unmarshal(bytes, &config2) 97 | assert.Nil(t, err) 98 | 99 | assert.Equal(t, config, config2) 100 | } 101 | 102 | func TestMarshalServiceConfig(t *testing.T) { 103 | configPtr := newTestConfig().SystemContainers["udev"] 104 | bytes, err := yaml.Marshal(configPtr) 105 | assert.Nil(t, err) 106 | 107 | configPtr2 := &ServiceConfig{} 108 | 109 | err = yaml.Unmarshal(bytes, configPtr2) 110 | assert.Nil(t, err) 111 | 112 | assert.Equal(t, configPtr, configPtr2) 113 | } 114 | -------------------------------------------------------------------------------- /config/merge_fixtures_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "io/ioutil" 5 | "path/filepath" 6 | "strings" 7 | "testing" 8 | ) 9 | 10 | func TestMergeOnValidFixtures(t *testing.T) { 11 | files, err := ioutil.ReadDir("testdata/") 12 | if err != nil { 13 | t.Fatal(err) 14 | } 15 | for _, file := range files { 16 | if file.IsDir() || !strings.HasSuffix(file.Name(), ".yml") { 17 | continue 18 | } 19 | data, err := ioutil.ReadFile(filepath.Join("testdata", file.Name())) 20 | if err != nil { 21 | t.Fatalf("error reading %q: %v", file.Name(), err) 22 | } 23 | _, _, _, _, err = Merge(NewServiceConfigs(), nil, nil, file.Name(), data, nil) 24 | if err != nil { 25 | t.Errorf("error loading %q: %v\n %v", file.Name(), string(data), err) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /config/testdata/.env: -------------------------------------------------------------------------------- 1 | # This is a comment line 2 | 3 | FOO=foo 4 | BAR=bar 5 | -------------------------------------------------------------------------------- /config/testdata/build-image.v2.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | 3 | services: 4 | simple: 5 | build: . 6 | image: myimage 7 | -------------------------------------------------------------------------------- /config/testdata/build.v1.yml: -------------------------------------------------------------------------------- 1 | simple1: 2 | build: . 3 | simple2: 4 | build: . 5 | dockerfile: alternate 6 | -------------------------------------------------------------------------------- /config/testdata/build.v2.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | 3 | services: 4 | simple1: 5 | build: . 6 | simple2: 7 | build: 8 | context: ./dir 9 | simple3: 10 | build: 11 | context: ./another 12 | dockerfile: alternate 13 | args: 14 | buildno: 1 15 | user: vincent 16 | simple4: 17 | build: 18 | context: ./another 19 | args: 20 | buildno: 2 21 | user: josh 22 | -------------------------------------------------------------------------------- /config/testdata/command.v1.yml: -------------------------------------------------------------------------------- 1 | simple1: 2 | image: myimage 3 | command: bundle exec thi-p 3000 4 | simple2: 5 | image: myimage 6 | command: [bundle, exec, thin, -p, "3000"] 7 | -------------------------------------------------------------------------------- /config/testdata/depends-on.v2.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | web: 4 | build: . 5 | depends_on: 6 | - db 7 | - redis 8 | redis: 9 | image: redis 10 | db: 11 | image: postgres 12 | -------------------------------------------------------------------------------- /config/testdata/dns.v1.yml: -------------------------------------------------------------------------------- 1 | simple: 2 | build: . 3 | dns: 8.8.8.8 4 | dns: 5 | - 8.8.8.8 6 | - 9.9.9.9 7 | -------------------------------------------------------------------------------- /config/testdata/entrypoint.v1.yml: -------------------------------------------------------------------------------- 1 | simple1: 2 | build: . 3 | entrypoint: 4 | - php 5 | - -d 6 | - zend_extension=/usr/local/lib/php/extensions/no-debug-non-zts-20100525/xdebug.so 7 | - -d 8 | - memory_limit=-1 9 | - vendor/bin/phpunit 10 | simple2: 11 | build: . 12 | entrypoint: /code/entrypoint.sh 13 | -------------------------------------------------------------------------------- /config/testdata/entrypoint.v2.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | services: 3 | simple1: 4 | build: . 5 | entrypoint: 6 | - php 7 | - -d 8 | - zend_extension=/usr/local/lib/php/extensions/no-debug-non-zts-20100525/xdebug.so 9 | - -d 10 | - memory_limit=-1 11 | - vendor/bin/phpunit 12 | simple2: 13 | build: . 14 | entrypoint: /code/entrypoint.sh 15 | -------------------------------------------------------------------------------- /config/testdata/logging.v1.yml: -------------------------------------------------------------------------------- 1 | simple1: 2 | image: myimage 3 | log_driver: syslog 4 | log_opt: 5 | syslog-address: "tcp://192.168.0.42:123" 6 | simple2: 7 | image: myimage 8 | log_driver: "none" 9 | -------------------------------------------------------------------------------- /config/testdata/logging.v2.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | services: 3 | simple1: 4 | image: myimage 5 | logging: 6 | driver: syslog 7 | options: 8 | syslog-address: "tcp://192.168.0.42:123" 9 | simple2: 10 | image: myimage 11 | logging: 12 | driver: "none" 13 | -------------------------------------------------------------------------------- /config/testdata/network-mode.v2.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | services: 3 | simple1: 4 | image: myimage 5 | network_mode: bridge 6 | simple2: 7 | image: myimage 8 | network_mode: "service:bridge" 9 | simple3: 10 | image: myimage 11 | network_mode: "container:test_container" 12 | simple4: 13 | image: myimage 14 | network_mode: host 15 | simple5: 16 | image: myimage 17 | network_mode: none 18 | -------------------------------------------------------------------------------- /config/testdata/networks-definition.v2.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | networks: 3 | default: 4 | driver: custom-driver 5 | network1: 6 | driver: bridge 7 | driver_opts: 8 | com.docker.network.enable_ipv6: "true" 9 | ipam: 10 | driver: default 11 | config: 12 | - subnet: 172.16.238.0/24 13 | gateway: 172.16.238.1 14 | - subnet: 2001:3984:3989::/64 15 | gateway: 2001:3984:3989::1 16 | network2: 17 | external: true 18 | network3: 19 | external: 20 | name: name-of-network 21 | network3: {} 22 | -------------------------------------------------------------------------------- /config/testdata/networks.v2.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | services: 3 | simple1: 4 | image: myimage 5 | networks: 6 | - network1 7 | - network2 8 | simple2: 9 | image: myimage 10 | networks: 11 | network1: 12 | aliases: 13 | - alias1 14 | - alias3 15 | network2: 16 | aliases: 17 | - alias2 18 | simple3: 19 | image: myimage 20 | networks: 21 | network1: 22 | ipv4_address: 172.16.238.10 23 | ipv6_address: 2001:3984:3989::10 24 | simple4: 25 | image: myimage 26 | networks: ["network1"] 27 | simple5: 28 | image: myimage 29 | networks: ["network1", "network2"] 30 | -------------------------------------------------------------------------------- /config/testdata/ulimits.v1.yml: -------------------------------------------------------------------------------- 1 | simple1: 2 | image: myimage 3 | ulimits: 4 | nproc: 65535 5 | nofile: 6 | soft: 20000 7 | hard: 40000 8 | -------------------------------------------------------------------------------- /config/testdata/ulimits.v2.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | services: 3 | simple1: 4 | image: myimage 5 | ulimits: 6 | nproc: 65535 7 | nofile: 8 | soft: 20000 9 | hard: 40000 10 | -------------------------------------------------------------------------------- /config/testdata/volumes-definition.v2.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | volumes: 3 | volume1: 4 | driver: foo 5 | volume2: 6 | driver: bar 7 | driver_opts: 8 | foo: "bar" 9 | baz: "" 10 | volume3: 11 | external: true 12 | volume4: 13 | external: 14 | name: name-of-volume 15 | -------------------------------------------------------------------------------- /config/testdata/volumes.v1.yml: -------------------------------------------------------------------------------- 1 | simple1: 2 | image: myimage 3 | volumes: 4 | - /var/lib/mysql 5 | - /opt/data:/var/lib/mysql 6 | - ./cache:/tmp/cache 7 | - ~configs:/etc/configs/:ro 8 | - datavolume:/var/lib/mysql 9 | volume_driver: mydriver 10 | volumes_from: 11 | - service_name 12 | - service_name:ro 13 | - container_name 14 | - container_name:rw 15 | -------------------------------------------------------------------------------- /config/testdata/volumes.v2.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | services: 3 | simple1: 4 | image: myimage 5 | volumes: 6 | - /var/lib/mysql 7 | - /opt/data:/var/lib/mysql 8 | - ./cache:/tmp/cache 9 | - ~configs:/etc/configs/:ro 10 | - datavolume:/var/lib/mysql 11 | volume_driver: mydriver 12 | -------------------------------------------------------------------------------- /config/utils.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | func merge(existing, value interface{}) interface{} { 4 | // append strings 5 | if left, lok := existing.([]interface{}); lok { 6 | if right, rok := value.([]interface{}); rok { 7 | return append(left, right...) 8 | } 9 | } 10 | 11 | //merge maps 12 | if left, lok := existing.(map[interface{}]interface{}); lok { 13 | if right, rok := value.(map[interface{}]interface{}); rok { 14 | newLeft := make(map[interface{}]interface{}) 15 | for k, v := range left { 16 | newLeft[k] = v 17 | } 18 | for k, v := range right { 19 | newLeft[k] = v 20 | } 21 | return newLeft 22 | } 23 | } 24 | 25 | return value 26 | } 27 | 28 | func clone(in RawService) RawService { 29 | result := RawService{} 30 | for k, v := range in { 31 | result[k] = v 32 | } 33 | 34 | return result 35 | } 36 | 37 | func asString(obj interface{}) string { 38 | if v, ok := obj.(string); ok { 39 | return v 40 | } 41 | return "" 42 | } 43 | -------------------------------------------------------------------------------- /docker/auth/auth.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "github.com/docker/cli/cli/config/configfile" 5 | clitypes "github.com/docker/cli/cli/config/types" 6 | "github.com/docker/docker/api/types" 7 | "github.com/docker/docker/registry" 8 | ) 9 | 10 | // Lookup defines a method for looking up authentication information 11 | type Lookup interface { 12 | All() map[string]types.AuthConfig 13 | Lookup(repoInfo *registry.RepositoryInfo) types.AuthConfig 14 | } 15 | 16 | // ConfigLookup implements AuthLookup by reading a Docker config file 17 | type ConfigLookup struct { 18 | *configfile.ConfigFile 19 | } 20 | 21 | // NewConfigLookup creates a new ConfigLookup for a given context 22 | func NewConfigLookup(configfile *configfile.ConfigFile) *ConfigLookup { 23 | return &ConfigLookup{ 24 | ConfigFile: configfile, 25 | } 26 | } 27 | 28 | // Lookup uses a Docker config file to lookup authentication information 29 | func (c *ConfigLookup) Lookup(repoInfo *registry.RepositoryInfo) types.AuthConfig { 30 | if c.ConfigFile == nil || repoInfo == nil || repoInfo.Index == nil { 31 | return types.AuthConfig{} 32 | } 33 | return registry.ResolveAuthConfig(convert(c.ConfigFile.AuthConfigs), repoInfo.Index) 34 | } 35 | 36 | // All uses a Docker config file to get all authentication information 37 | func (c *ConfigLookup) All() map[string]types.AuthConfig { 38 | if c.ConfigFile == nil { 39 | return map[string]types.AuthConfig{} 40 | } 41 | return convert(c.ConfigFile.AuthConfigs) 42 | } 43 | 44 | func convert(acs map[string]clitypes.AuthConfig) map[string]types.AuthConfig { 45 | if acs == nil { 46 | return nil 47 | } 48 | 49 | result := map[string]types.AuthConfig{} 50 | for k, v := range acs { 51 | result[k] = types.AuthConfig{ 52 | Username: v.Username, 53 | Password: v.Password, 54 | Auth: v.Auth, 55 | Email: v.Email, 56 | ServerAddress: v.ServerAddress, 57 | IdentityToken: v.IdentityToken, 58 | RegistryToken: v.RegistryToken, 59 | } 60 | } 61 | return result 62 | } 63 | -------------------------------------------------------------------------------- /docker/client/client.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "os" 7 | "path/filepath" 8 | "runtime" 9 | 10 | cliconfig "github.com/docker/cli/cli/config" 11 | "github.com/docker/docker/client" 12 | "github.com/docker/docker/pkg/homedir" 13 | "github.com/docker/go-connections/tlsconfig" 14 | "github.com/docker/libcompose/version" 15 | ) 16 | 17 | const ( 18 | // DefaultAPIVersion is the default docker API version set by libcompose 19 | DefaultAPIVersion = "v1.20" 20 | defaultTrustKeyFile = "key.json" 21 | defaultCaFile = "ca.pem" 22 | defaultKeyFile = "key.pem" 23 | defaultCertFile = "cert.pem" 24 | ) 25 | 26 | var ( 27 | dockerCertPath = os.Getenv("DOCKER_CERT_PATH") 28 | ) 29 | 30 | func init() { 31 | if dockerCertPath == "" { 32 | dockerCertPath = cliconfig.Dir() 33 | } 34 | } 35 | 36 | // Options holds docker client options (host, tls, ..) 37 | type Options struct { 38 | TLS bool 39 | TLSVerify bool 40 | TLSOptions tlsconfig.Options 41 | TrustKey string 42 | Host string 43 | APIVersion string 44 | } 45 | 46 | // Create creates a docker client based on the specified options. 47 | func Create(c Options) (client.APIClient, error) { 48 | if c.Host == "" { 49 | if os.Getenv("DOCKER_API_VERSION") == "" { 50 | os.Setenv("DOCKER_API_VERSION", DefaultAPIVersion) 51 | } 52 | client, err := client.NewEnvClient() 53 | if err != nil { 54 | return nil, err 55 | } 56 | return client, nil 57 | } 58 | 59 | apiVersion := c.APIVersion 60 | if apiVersion == "" { 61 | apiVersion = DefaultAPIVersion 62 | } 63 | 64 | if c.TLSOptions.CAFile == "" { 65 | c.TLSOptions.CAFile = filepath.Join(dockerCertPath, defaultCaFile) 66 | } 67 | if c.TLSOptions.CertFile == "" { 68 | c.TLSOptions.CertFile = filepath.Join(dockerCertPath, defaultCertFile) 69 | } 70 | if c.TLSOptions.KeyFile == "" { 71 | c.TLSOptions.KeyFile = filepath.Join(dockerCertPath, defaultKeyFile) 72 | } 73 | if c.TrustKey == "" { 74 | c.TrustKey = filepath.Join(homedir.Get(), ".docker", defaultTrustKeyFile) 75 | } 76 | if c.TLSVerify { 77 | c.TLS = true 78 | } 79 | if c.TLS { 80 | c.TLSOptions.InsecureSkipVerify = !c.TLSVerify 81 | } 82 | 83 | var httpClient *http.Client 84 | if c.TLS { 85 | config, err := tlsconfig.Client(c.TLSOptions) 86 | if err != nil { 87 | return nil, err 88 | } 89 | 90 | httpClient = &http.Client{ 91 | Transport: &http.Transport{ 92 | TLSClientConfig: config, 93 | }, 94 | } 95 | } 96 | 97 | customHeaders := map[string]string{} 98 | customHeaders["User-Agent"] = fmt.Sprintf("Libcompose-Client/%s (%s)", version.VERSION, runtime.GOOS) 99 | 100 | client, err := client.NewClientWithOpts( 101 | client.WithHTTPClient(httpClient), 102 | client.WithHost(c.Host), 103 | client.WithVersion(apiVersion), 104 | client.WithHTTPHeaders(customHeaders), 105 | ) 106 | if err != nil { 107 | return nil, err 108 | } 109 | return client, nil 110 | } 111 | -------------------------------------------------------------------------------- /docker/client/client_factory.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "github.com/docker/docker/client" 5 | "github.com/docker/libcompose/project" 6 | ) 7 | 8 | // Factory is a factory to create docker clients. 9 | type Factory interface { 10 | // Create constructs a Docker client for the given service. The passed in 11 | // config may be nil in which case a generic client for the project should 12 | // be returned. 13 | Create(service project.Service) client.APIClient 14 | } 15 | 16 | type defaultFactory struct { 17 | client client.APIClient 18 | } 19 | 20 | // NewDefaultFactory creates and returns the default client factory that uses 21 | // github.com/docker/docker client. 22 | func NewDefaultFactory(opts Options) (Factory, error) { 23 | client, err := Create(opts) 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | return &defaultFactory{ 29 | client: client, 30 | }, nil 31 | } 32 | 33 | func (s *defaultFactory) Create(service project.Service) client.APIClient { 34 | return s.client 35 | } 36 | -------------------------------------------------------------------------------- /docker/client/client_factory_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | ) 7 | 8 | func TestFactoryWithEnv(t *testing.T) { 9 | cases := []struct { 10 | envs map[string]string 11 | expectedError string 12 | expectedVersion string 13 | }{ 14 | { 15 | envs: map[string]string{}, 16 | expectedVersion: "v1.20", 17 | }, 18 | { 19 | envs: map[string]string{ 20 | "DOCKER_CERT_PATH": "invalid/path", 21 | }, 22 | expectedError: "Could not load X509 key pair: open invalid/path/cert.pem: no such file or directory", 23 | expectedVersion: "v1.20", 24 | }, 25 | { 26 | envs: map[string]string{ 27 | "DOCKER_API_VERSION": "1.22", 28 | }, 29 | expectedVersion: "1.22", 30 | }, 31 | } 32 | for _, c := range cases { 33 | recoverEnvs := setupEnvs(t, c.envs) 34 | factory, err := NewDefaultFactory(Options{}) 35 | if c.expectedError != "" { 36 | if err == nil || !strings.Contains(err.Error(), c.expectedError) { 37 | t.Errorf("expected an error %s, got %s, for %v", c.expectedError, err.Error(), c) 38 | } 39 | } else { 40 | if err != nil { 41 | t.Error(err) 42 | } 43 | apiclient := factory.Create(nil) 44 | version := apiclient.ClientVersion() 45 | if version != c.expectedVersion { 46 | t.Errorf("expected %s, got %s, for %v", c.expectedVersion, version, c) 47 | } 48 | } 49 | recoverEnvs(t) 50 | } 51 | } 52 | 53 | func TestFactoryWithOptions(t *testing.T) { 54 | cases := []struct { 55 | options Options 56 | expectedError string 57 | expectedVersion string 58 | }{ 59 | { 60 | options: Options{ 61 | Host: "host", 62 | }, 63 | expectedError: "unable to parse docker host", 64 | }, 65 | { 66 | options: Options{ 67 | Host: "invalid://host", 68 | }, 69 | expectedVersion: "v1.20", 70 | }, 71 | { 72 | options: Options{ 73 | Host: "tcp://host", 74 | APIVersion: "v1.22", 75 | }, 76 | expectedVersion: "v1.22", 77 | }, 78 | } 79 | for _, c := range cases { 80 | factory, err := NewDefaultFactory(c.options) 81 | if c.expectedError != "" { 82 | if err == nil || !strings.Contains(err.Error(), c.expectedError) { 83 | t.Errorf("expected an error %s, got %s, for %v", c.expectedError, err.Error(), c) 84 | } 85 | } else { 86 | if err != nil { 87 | t.Error(err) 88 | } 89 | apiclient := factory.Create(nil) 90 | version := apiclient.ClientVersion() 91 | if version != c.expectedVersion { 92 | t.Errorf("expected %s, got %s, for %v", c.expectedVersion, version, c) 93 | } 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /docker/client/fixtures/ca.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIC0jCCAbqgAwIBAgIRAILlP5WWLaHkQ/m2ASHP7SowDQYJKoZIhvcNAQELBQAw 3 | EjEQMA4GA1UEChMHdmluY2VudDAeFw0xNjAzMjQxMDE5MDBaFw0xOTAzMDkxMDE5 4 | MDBaMBIxEDAOBgNVBAoTB3ZpbmNlbnQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw 5 | ggEKAoIBAQD0yZPKAGncoaxaU/QW9tWEHbrvDoGVF/65L8Si/jBrlAgLjhmmV1di 6 | vKG9QPzuU8snxHro3/uCwyA6kTqw0U8bGwHxJq2Bpa6JBYj8N2jMJ+M+sjXgSo2t 7 | E0zIzjTW2Pir3C8qwfrVL6NFp9xClwMD23SFZ0UsEH36NkfyrKBVeM8IOjJd4Wjs 8 | xIcuvF3BTVkji84IJBW2JIKf9ZrzJwUlSCPgptRp4Evdbyp5d+UPxtwxD7qjW4lM 9 | yQQ8vfcC4lKkVx5s/RNJ4fzd5uEgLdEbZ20qt7Zt/bLcxFHpUhH2teA0QjmrOWFh 10 | gbL83s95/+hbSVhsO4hoFW7vTeiCCY4xAgMBAAGjIzAhMA4GA1UdDwEB/wQEAwIC 11 | rDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBY51RHajuDuhO2 12 | tcm26jeNROzfffnjhvbOVPjSEdo9vI3JpMU/RuQw+nbNcLwJrdjL6UH7tD/36Y+q 13 | NXH+xSIjWFH0zXGxrIUsVrvt6f8CbOvw7vD+gygOG+849PDQMbL6czP8rvXY7vZV 14 | 9pdpQfrENk4b5kePRW/6HaGSTvtgN7XOrYD9fp3pm/G534T2e3IxgYMRNwdB9Ul9 15 | bLwMqQqf4eiqqMs6x4IVmZUkGVMKiFKcvkNg9a+Ozx5pMizHeAezWMcZ5V+QJZVT 16 | 8lElSCKZ2Yy2xkcl7aeQMLwcAeZwfTp+Yu9dVzlqXiiBTLd1+LtAQCuKHzmw4Q8k 17 | EvD5m49l 18 | -----END CERTIFICATE----- 19 | -------------------------------------------------------------------------------- /docker/client/fixtures/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIC8DCCAdigAwIBAgIRAJAS1glgcke4q7eCaretwgUwDQYJKoZIhvcNAQELBQAw 3 | EjEQMA4GA1UEChMHdmluY2VudDAeFw0xNjAzMjQxMDE5MDBaFw0xOTAzMDkxMDE5 4 | MDBaMB4xHDAaBgNVBAoME3ZpbmNlbnQuPGJvb3RzdHJhcD4wggEiMA0GCSqGSIb3 5 | DQEBAQUAA4IBDwAwggEKAoIBAQClpvG442dGEvrRgmCrqY4kBml1LVlw2Y7ZDn6B 6 | TKa52+MuGDmfXbO1UhclNqTXjLgAwKjPz/OvnPRxNEUoQEDbBd+Xev7rxTY5TvYI 7 | 27YH3fMH2LL2j62jum649abfhZ6ekD5eD8tCn3mnrEOgqRIlK7efPIVixq/ZqU1H 8 | 7ez0ggB7dmWHlhnUaxyQOCSnAX/7nKYQXqZgVvGhDeR2jp7GcnhbK/qPrZ/mOm83 9 | 2IjCeYN145opYlzTSp64GYIZz7uqMNcnDKK37ZbS8MYcTjrRaHEiqZVVdIC+ghbx 10 | qYqzbZRVfgztI9jwmifn0mYrN4yt+nhNYwBcRJ4Pv3uLFbo7AgMBAAGjNTAzMA4G 11 | A1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAjAMBgNVHRMBAf8EAjAA 12 | MA0GCSqGSIb3DQEBCwUAA4IBAQDg1r7nksjYgDFYEcBbrRrRHddIoK+RVmSBTTrq 13 | 8giC77m0srKdh9XTVWK1PUbGfODV1oD8m9QhPE8zPDyYQ8jeXNRSU5wXdkrTRmmY 14 | w/T3SREqmE7CObMtusokHidjYFuqqCR07sJzqBKRlzr3o0EGe3tuEhUlF5ARY028 15 | eipaDcVlT5ChGcDa6LeJ4e05u4cVap0dd6Rp1w3Rx1AYAecdgtgBMnw1iWdl/nrC 16 | sp26ZXNaAhFOUovlY9VY257AMd9hQV7WvAK4yNEHcckVu3uXTBmDgNSOPtl0QLsL 17 | Kjlj75ksCx8nCln/hCut/0+kGTsGZqdV5c6ktgcGYRir/5Hs 18 | -----END CERTIFICATE----- 19 | -------------------------------------------------------------------------------- /docker/client/fixtures/key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEApabxuONnRhL60YJgq6mOJAZpdS1ZcNmO2Q5+gUymudvjLhg5 3 | n12ztVIXJTak14y4AMCoz8/zr5z0cTRFKEBA2wXfl3r+68U2OU72CNu2B93zB9iy 4 | 9o+to7puuPWm34WenpA+Xg/LQp95p6xDoKkSJSu3nzyFYsav2alNR+3s9IIAe3Zl 5 | h5YZ1GsckDgkpwF/+5ymEF6mYFbxoQ3kdo6exnJ4Wyv6j62f5jpvN9iIwnmDdeOa 6 | KWJc00qeuBmCGc+7qjDXJwyit+2W0vDGHE460WhxIqmVVXSAvoIW8amKs22UVX4M 7 | 7SPY8Jon59JmKzeMrfp4TWMAXESeD797ixW6OwIDAQABAoIBAHfyAAleL8NfrtnR 8 | S+pApbmUIvxD0AWUooispBE/zWG6xC72P5MTqDJctIGvpYCmVf3Fgvamns7EGYN2 9 | 07Sngc6V3Ca1WqyhaffpIuGbJZ1gqr89u6gotRRexBmNVj13ZTlvPJmjWgxtqQsu 10 | AvHsOkVL+HOGwRaaw24Z1umEcBVCepl7PGTqsLeJUtBUZBiqdJTu4JYLAB6BggBI 11 | OxhHoTWvlNWwzezo2C/IXkXcXD/tp3i5vTn5rAXHSMQkdMAUh7/xJ73Fl36gxZhp 12 | W7NoPKaS9qNh8jhs6p54S7tInb6+mrKtvRFKl5XAR3istXrXteT5UaukpuBbQ/5d 13 | qf4BXuECgYEAzoOKxMee5tG/G9iC6ImNq5xGAZm0OnmteNgIEQj49If1Q68av525 14 | FioqdC9zV+blfHQqXEIUeum4JAou4xqmB8Lw2H0lYwOJ1IkpUy3QJjU1IrI+U5Qy 15 | ryZuA9cxSTLf1AJFbROsoZDpjaBh0uUQkD/4PHpwXMgHu/3CaJ4nTEkCgYEAzVjE 16 | VWgczWJGyRxmHSeR51ft1jrlChZHEd3HwgLfo854JIj+MGUH4KPLSMIkYNuyiwNQ 17 | W7zdXCB47U8afSL/lPTv1M5+ZsWY6sZAT6gtp/IeU0Va943h9cj10fAOBJaz1H6M 18 | jnZS4jjWhVInE7wpCDVCwDRoHHJ84kb6JeflamMCgYBDQDcKie9HP3q6uLE4xMKr 19 | 5gIuNz2n5UQGnGNUGNXp2/SVDArr55MEksqsd19aesi01KeOz74XoNDke6R1NJJo 20 | 6KTB+08XhWl3GwuoGL02FBGvsNf3I8W1oBAnlAZqzfRx+CNfuA55ttU318jDgvD3 21 | 6L0QBNdef411PNf4dbhacQKBgAd/e0PHFm4lbYJAaDYeUMSKwGN3KQ/SOmwblgSu 22 | iC36BwcGfYmU1tHMCUsx05Q50W4kA9Ylskt/4AqCPexdz8lHnE4/7/uesXO5I3YF 23 | JQ2h2Jufx6+MXbjUyq0Mv+ZI/m3+5PD6vxIFk0ew9T5SO4lSMIrGHxsSzx6QCuhB 24 | bG4TAoGBAJ5PWG7d2CyCjLtfF8J4NxykRvIQ8l/3kDvDdNrXiXbgonojo2lgRYaM 25 | 5LoK9ApN8KHdedpTRipBaDA22Sp5SjMcUE7A6q42PJCL9r+BRYF0foFQx/rqpCff 26 | pVWKgwIPoKnfxDqN1RUgyFcx1jbA3XVJZCuT+wbMuDQ9nlvulD1W 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /docker/container/functions.go: -------------------------------------------------------------------------------- 1 | package container 2 | 3 | import ( 4 | "github.com/docker/docker/api/types" 5 | "github.com/docker/docker/api/types/filters" 6 | "github.com/docker/docker/client" 7 | "golang.org/x/net/context" 8 | ) 9 | 10 | // ListByFilter looks up the hosts containers with the specified filters and 11 | // returns a list of container matching it, or an error. 12 | func ListByFilter(ctx context.Context, clientInstance client.ContainerAPIClient, containerFilters ...map[string][]string) ([]types.Container, error) { 13 | filterArgs := filters.NewArgs() 14 | 15 | // FIXME(vdemeester) I don't like 3 for loops >_< 16 | for _, filter := range containerFilters { 17 | for key, filterValue := range filter { 18 | for _, value := range filterValue { 19 | filterArgs.Add(key, value) 20 | } 21 | } 22 | } 23 | 24 | return clientInstance.ContainerList(ctx, types.ContainerListOptions{ 25 | All: true, 26 | Filters: filterArgs, 27 | }) 28 | } 29 | 30 | // Get looks up the hosts containers with the specified ID 31 | // or name and returns it, or an error. 32 | func Get(ctx context.Context, clientInstance client.ContainerAPIClient, id string) (*types.ContainerJSON, error) { 33 | container, err := clientInstance.ContainerInspect(ctx, id) 34 | if err != nil { 35 | if client.IsErrNotFound(err) { 36 | return nil, nil 37 | } 38 | return nil, err 39 | } 40 | return &container, nil 41 | } 42 | -------------------------------------------------------------------------------- /docker/ctx/context.go: -------------------------------------------------------------------------------- 1 | package ctx 2 | 3 | import ( 4 | cliconfig "github.com/docker/cli/cli/config" 5 | "github.com/docker/cli/cli/config/configfile" 6 | "github.com/docker/libcompose/docker/auth" 7 | "github.com/docker/libcompose/docker/client" 8 | "github.com/docker/libcompose/project" 9 | ) 10 | 11 | // Context holds context meta information about a libcompose project and docker 12 | // client information (like configuration file, builder to use, …) 13 | type Context struct { 14 | project.Context 15 | ClientFactory client.Factory 16 | ConfigDir string 17 | ConfigFile *configfile.ConfigFile 18 | AuthLookup auth.Lookup 19 | } 20 | 21 | // LookupConfig tries to load the docker configuration files, if any. 22 | func (c *Context) LookupConfig() error { 23 | if c.ConfigFile != nil { 24 | return nil 25 | } 26 | 27 | config, err := cliconfig.Load(c.ConfigDir) 28 | if err != nil { 29 | return err 30 | } 31 | 32 | c.ConfigFile = config 33 | 34 | return nil 35 | } 36 | -------------------------------------------------------------------------------- /docker/image/image.go: -------------------------------------------------------------------------------- 1 | package image 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "os" 9 | 10 | "github.com/docker/distribution/reference" 11 | "github.com/docker/docker/api/types" 12 | "github.com/docker/docker/client" 13 | "github.com/docker/docker/pkg/jsonmessage" 14 | "github.com/docker/docker/pkg/term" 15 | "github.com/docker/docker/registry" 16 | "github.com/docker/libcompose/docker/auth" 17 | "github.com/sirupsen/logrus" 18 | "golang.org/x/net/context" 19 | ) 20 | 21 | // Exists return whether or not the service image already exists 22 | func Exists(ctx context.Context, clt client.ImageAPIClient, image string) (bool, error) { 23 | _, err := InspectImage(ctx, clt, image) 24 | if err != nil { 25 | if client.IsErrNotFound(err) { 26 | return false, nil 27 | } 28 | return false, err 29 | } 30 | return true, nil 31 | } 32 | 33 | // InspectImage inspect the specified image (can be a name, an id or a digest) 34 | // with the specified client. 35 | func InspectImage(ctx context.Context, client client.ImageAPIClient, image string) (types.ImageInspect, error) { 36 | imageInspect, _, err := client.ImageInspectWithRaw(ctx, image) 37 | return imageInspect, err 38 | } 39 | 40 | // RemoveImage removes the specified image (can be a name, an id or a digest) 41 | // from the daemon store with the specified client. 42 | func RemoveImage(ctx context.Context, client client.ImageAPIClient, image string) error { 43 | _, err := client.ImageRemove(ctx, image, types.ImageRemoveOptions{}) 44 | return err 45 | } 46 | 47 | // PullImage pulls the specified image (can be a name, an id or a digest) 48 | // to the daemon store with the specified client. 49 | func PullImage(ctx context.Context, client client.ImageAPIClient, serviceName string, authLookup auth.Lookup, image string) error { 50 | fmt.Fprintf(os.Stderr, "Pulling %s (%s)...\n", serviceName, image) 51 | distributionRef, err := reference.ParseNormalizedNamed(image) 52 | if err != nil { 53 | return err 54 | } 55 | 56 | repoInfo, err := registry.ParseRepositoryInfo(distributionRef) 57 | if err != nil { 58 | return err 59 | } 60 | 61 | authConfig := authLookup.Lookup(repoInfo) 62 | 63 | // Use ConfigFile.SaveToWriter to not re-define encodeAuthToBase64 64 | encodedAuth, err := encodeAuthToBase64(authConfig) 65 | if err != nil { 66 | return err 67 | } 68 | 69 | options := types.ImagePullOptions{ 70 | RegistryAuth: encodedAuth, 71 | } 72 | responseBody, err := client.ImagePull(ctx, distributionRef.String(), options) 73 | if err != nil { 74 | logrus.Errorf("Failed to pull image %s: %v", image, err) 75 | return err 76 | } 77 | defer responseBody.Close() 78 | 79 | var writeBuff io.Writer = os.Stderr 80 | 81 | outFd, isTerminalOut := term.GetFdInfo(os.Stderr) 82 | 83 | err = jsonmessage.DisplayJSONMessagesStream(responseBody, writeBuff, outFd, isTerminalOut, nil) 84 | if err != nil { 85 | if jerr, ok := err.(*jsonmessage.JSONError); ok { 86 | // If no error code is set, default to 1 87 | if jerr.Code == 0 { 88 | jerr.Code = 1 89 | } 90 | fmt.Fprintf(os.Stderr, "%s", writeBuff) 91 | return fmt.Errorf("Status: %s, Code: %d", jerr.Message, jerr.Code) 92 | } 93 | } 94 | return err 95 | } 96 | 97 | // encodeAuthToBase64 serializes the auth configuration as JSON base64 payload 98 | func encodeAuthToBase64(authConfig types.AuthConfig) (string, error) { 99 | buf, err := json.Marshal(authConfig) 100 | if err != nil { 101 | return "", err 102 | } 103 | return base64.URLEncoding.EncodeToString(buf), nil 104 | } 105 | -------------------------------------------------------------------------------- /docker/network/factory.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import ( 4 | "github.com/docker/libcompose/config" 5 | composeclient "github.com/docker/libcompose/docker/client" 6 | "github.com/docker/libcompose/project" 7 | ) 8 | 9 | // DockerFactory implements project.NetworksFactory 10 | type DockerFactory struct { 11 | ClientFactory composeclient.Factory 12 | } 13 | 14 | // Create implements project.NetworksFactory Create method. 15 | // It creates a Networks (that implements project.Networks) from specified configurations. 16 | func (f *DockerFactory) Create(projectName string, networkConfigs map[string]*config.NetworkConfig, serviceConfigs *config.ServiceConfigs, networkEnabled bool) (project.Networks, error) { 17 | cli := f.ClientFactory.Create(nil) 18 | return NetworksFromServices(cli, projectName, networkConfigs, serviceConfigs, networkEnabled) 19 | } 20 | -------------------------------------------------------------------------------- /docker/project.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "golang.org/x/net/context" 5 | 6 | "github.com/docker/docker/api/types" 7 | "github.com/docker/docker/api/types/filters" 8 | "github.com/docker/libcompose/config" 9 | "github.com/docker/libcompose/docker/auth" 10 | "github.com/docker/libcompose/docker/client" 11 | "github.com/docker/libcompose/docker/ctx" 12 | "github.com/docker/libcompose/docker/network" 13 | "github.com/docker/libcompose/docker/service" 14 | "github.com/docker/libcompose/docker/volume" 15 | "github.com/docker/libcompose/labels" 16 | "github.com/docker/libcompose/project" 17 | "github.com/sirupsen/logrus" 18 | ) 19 | 20 | // NewProject creates a Project with the specified context. 21 | func NewProject(context *ctx.Context, parseOptions *config.ParseOptions) (project.APIProject, error) { 22 | 23 | if err := context.LookupConfig(); err != nil { 24 | logrus.Errorf("Failed to load docker config: %v", err) 25 | } 26 | 27 | if context.AuthLookup == nil { 28 | context.AuthLookup = auth.NewConfigLookup(context.ConfigFile) 29 | } 30 | 31 | if context.ServiceFactory == nil { 32 | context.ServiceFactory = service.NewFactory(context) 33 | } 34 | 35 | if context.ClientFactory == nil { 36 | factory, err := client.NewDefaultFactory(client.Options{}) 37 | if err != nil { 38 | return nil, err 39 | } 40 | context.ClientFactory = factory 41 | } 42 | 43 | if context.NetworksFactory == nil { 44 | networksFactory := &network.DockerFactory{ 45 | ClientFactory: context.ClientFactory, 46 | } 47 | context.NetworksFactory = networksFactory 48 | } 49 | 50 | if context.VolumesFactory == nil { 51 | volumesFactory := &volume.DockerFactory{ 52 | ClientFactory: context.ClientFactory, 53 | } 54 | context.VolumesFactory = volumesFactory 55 | } 56 | 57 | // FIXME(vdemeester) Remove the context duplication ? 58 | runtime := &Project{ 59 | clientFactory: context.ClientFactory, 60 | } 61 | p := project.NewProject(&context.Context, runtime, parseOptions) 62 | 63 | err := p.Parse() 64 | if err != nil { 65 | return nil, err 66 | } 67 | 68 | return p, err 69 | } 70 | 71 | // Project implements project.RuntimeProject and define docker runtime specific methods. 72 | type Project struct { 73 | clientFactory client.Factory 74 | } 75 | 76 | // RemoveOrphans implements project.RuntimeProject.RemoveOrphans. 77 | // It will remove orphan containers that are part of the project but not to any services. 78 | func (p *Project) RemoveOrphans(ctx context.Context, projectName string, serviceConfigs *config.ServiceConfigs) error { 79 | client := p.clientFactory.Create(nil) 80 | filter := filters.NewArgs() 81 | filter.Add("label", labels.PROJECT.EqString(projectName)) 82 | containers, err := client.ContainerList(ctx, types.ContainerListOptions{ 83 | Filters: filter, 84 | }) 85 | if err != nil { 86 | return err 87 | } 88 | currentServices := map[string]struct{}{} 89 | for _, serviceName := range serviceConfigs.Keys() { 90 | currentServices[serviceName] = struct{}{} 91 | } 92 | for _, container := range containers { 93 | serviceLabel := container.Labels[labels.SERVICE.Str()] 94 | if _, ok := currentServices[serviceLabel]; !ok { 95 | if err := client.ContainerKill(ctx, container.ID, "SIGKILL"); err != nil { 96 | return err 97 | } 98 | if err := client.ContainerRemove(ctx, container.ID, types.ContainerRemoveOptions{ 99 | Force: true, 100 | }); err != nil { 101 | return err 102 | } 103 | } 104 | } 105 | return nil 106 | } 107 | -------------------------------------------------------------------------------- /docker/service/name.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | 7 | "golang.org/x/net/context" 8 | 9 | "github.com/docker/docker/api/types" 10 | "github.com/docker/docker/api/types/filters" 11 | "github.com/docker/docker/client" 12 | "github.com/docker/libcompose/labels" 13 | ) 14 | 15 | const format = "%s_%s_%d" 16 | 17 | // Namer defines method to provide container name. 18 | type Namer interface { 19 | Next() (string, int) 20 | } 21 | 22 | type defaultNamer struct { 23 | project string 24 | service string 25 | oneOff bool 26 | currentNumber int 27 | } 28 | 29 | type singleNamer struct { 30 | name string 31 | } 32 | 33 | // NewSingleNamer returns a namer that only allows a single name. 34 | func NewSingleNamer(name string) Namer { 35 | return &singleNamer{name} 36 | } 37 | 38 | // NewNamer returns a namer that returns names based on the specified project and 39 | // service name and an inner counter, e.g. project_service_1, project_service_2… 40 | func NewNamer(ctx context.Context, client client.ContainerAPIClient, project, service string, oneOff bool) (Namer, error) { 41 | namer := &defaultNamer{ 42 | project: project, 43 | service: service, 44 | oneOff: oneOff, 45 | } 46 | 47 | filter := filters.NewArgs() 48 | filter.Add("label", fmt.Sprintf("%s=%s", labels.PROJECT.Str(), project)) 49 | filter.Add("label", fmt.Sprintf("%s=%s", labels.SERVICE.Str(), service)) 50 | if oneOff { 51 | filter.Add("label", fmt.Sprintf("%s=%s", labels.ONEOFF.Str(), "True")) 52 | } else { 53 | filter.Add("label", fmt.Sprintf("%s=%s", labels.ONEOFF.Str(), "False")) 54 | } 55 | 56 | containers, err := client.ContainerList(ctx, types.ContainerListOptions{ 57 | All: true, 58 | Filters: filter, 59 | }) 60 | if err != nil { 61 | return nil, err 62 | } 63 | 64 | maxNumber := 0 65 | for _, container := range containers { 66 | number, err := strconv.Atoi(container.Labels[labels.NUMBER.Str()]) 67 | if err != nil { 68 | return nil, err 69 | } 70 | if number > maxNumber { 71 | maxNumber = number 72 | } 73 | } 74 | namer.currentNumber = maxNumber + 1 75 | 76 | return namer, nil 77 | } 78 | 79 | func (i *defaultNamer) Next() (string, int) { 80 | service := i.service 81 | if i.oneOff { 82 | service = i.service + "_run" 83 | } 84 | name := fmt.Sprintf(format, i.project, service, i.currentNumber) 85 | number := i.currentNumber 86 | i.currentNumber = i.currentNumber + 1 87 | return name, number 88 | } 89 | 90 | func (s *singleNamer) Next() (string, int) { 91 | return s.name, 1 92 | } 93 | -------------------------------------------------------------------------------- /docker/service/service_factory.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "github.com/docker/libcompose/config" 5 | "github.com/docker/libcompose/docker/ctx" 6 | "github.com/docker/libcompose/project" 7 | ) 8 | 9 | // Factory is an implementation of project.ServiceFactory. 10 | type Factory struct { 11 | context *ctx.Context 12 | } 13 | 14 | // NewFactory creates a new service factory for the given context 15 | func NewFactory(context *ctx.Context) *Factory { 16 | return &Factory{ 17 | context: context, 18 | } 19 | } 20 | 21 | // Create creates a Service based on the specified project, name and service configuration. 22 | func (s *Factory) Create(project *project.Project, name string, serviceConfig *config.ServiceConfig) (project.Service, error) { 23 | return NewService(name, serviceConfig, s.context), nil 24 | } 25 | -------------------------------------------------------------------------------- /docker/service/service_test.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/docker/libcompose/config" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestSpecifiesHostPort(t *testing.T) { 11 | servicesWithHostPort := []Service{ 12 | {serviceConfig: &config.ServiceConfig{Ports: []string{"8000:8000"}}}, 13 | {serviceConfig: &config.ServiceConfig{Ports: []string{"127.0.0.1:8000:8000"}}}, 14 | } 15 | 16 | for _, service := range servicesWithHostPort { 17 | assert.True(t, service.specificiesHostPort()) 18 | } 19 | 20 | servicesWithoutHostPort := []Service{ 21 | {serviceConfig: &config.ServiceConfig{Ports: []string{"8000"}}}, 22 | {serviceConfig: &config.ServiceConfig{Ports: []string{"127.0.0.1::8000"}}}, 23 | } 24 | 25 | for _, service := range servicesWithoutHostPort { 26 | assert.False(t, service.specificiesHostPort()) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /docker/service/utils.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "github.com/docker/docker/api/types/container" 5 | "github.com/docker/libcompose/project" 6 | ) 7 | 8 | // DefaultDependentServices return the dependent services (as an array of ServiceRelationship) 9 | // for the specified project and service. It looks for : links, volumesFrom, net and ipc configuration. 10 | // It uses default project implementation and append some docker specific ones. 11 | func DefaultDependentServices(p *project.Project, s project.Service) []project.ServiceRelationship { 12 | result := project.DefaultDependentServices(p, s) 13 | 14 | result = appendNs(p, result, s.Config().NetworkMode, project.RelTypeNetNamespace) 15 | result = appendNs(p, result, s.Config().Ipc, project.RelTypeIpcNamespace) 16 | 17 | return result 18 | } 19 | 20 | func appendNs(p *project.Project, rels []project.ServiceRelationship, conf string, relType project.ServiceRelationshipType) []project.ServiceRelationship { 21 | service := GetContainerFromIpcLikeConfig(p, conf) 22 | if service != "" { 23 | rels = append(rels, project.NewServiceRelationship(service, relType)) 24 | } 25 | return rels 26 | } 27 | 28 | // GetContainerFromIpcLikeConfig returns name of the service that shares the IPC 29 | // namespace with the specified service. 30 | func GetContainerFromIpcLikeConfig(p *project.Project, conf string) string { 31 | ipc := container.IpcMode(conf) 32 | if !ipc.IsContainer() { 33 | return "" 34 | } 35 | 36 | name := ipc.Container() 37 | if name == "" { 38 | return "" 39 | } 40 | 41 | if p.ServiceConfigs.Has(name) { 42 | return name 43 | } 44 | return "" 45 | } 46 | -------------------------------------------------------------------------------- /docker/volume/volume_test.go: -------------------------------------------------------------------------------- 1 | package volume 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/docker/docker/api/types/volume" 7 | "github.com/docker/docker/client" 8 | "github.com/docker/libcompose/config" 9 | ) 10 | 11 | func TestVolumesFromServices(t *testing.T) { 12 | cases := []struct { 13 | volumeConfigs map[string]*config.VolumeConfig 14 | services map[string]*config.ServiceConfig 15 | volumeEnabled bool 16 | expectedVolumes []*Volume 17 | expectedError bool 18 | }{ 19 | {}, 20 | { 21 | volumeConfigs: map[string]*config.VolumeConfig{ 22 | "vol1": {}, 23 | }, 24 | services: map[string]*config.ServiceConfig{}, 25 | expectedVolumes: []*Volume{ 26 | { 27 | name: "vol1", 28 | projectName: "prj", 29 | }, 30 | }, 31 | expectedError: false, 32 | }, 33 | { 34 | volumeConfigs: map[string]*config.VolumeConfig{ 35 | "vol1": nil, 36 | }, 37 | services: map[string]*config.ServiceConfig{}, 38 | expectedVolumes: []*Volume{ 39 | { 40 | name: "vol1", 41 | projectName: "prj", 42 | }, 43 | }, 44 | expectedError: false, 45 | }, 46 | } 47 | 48 | for index, c := range cases { 49 | services := config.NewServiceConfigs() 50 | for name, service := range c.services { 51 | services.Add(name, service) 52 | } 53 | 54 | volumes, err := VolumesFromServices(&volumeClient{}, "prj", c.volumeConfigs, services, c.volumeEnabled) 55 | if c.expectedError { 56 | if err == nil { 57 | t.Fatalf("%d: expected an error, got nothing", index) 58 | } 59 | } else { 60 | if err != nil { 61 | t.Fatalf("%d: didn't expect an error, got one %s", index, err.Error()) 62 | } 63 | } 64 | if volumes.volumeEnabled != c.volumeEnabled { 65 | t.Fatalf("%d: expected volume enabled %v, got %v", index, c.volumeEnabled, volumes.volumeEnabled) 66 | } 67 | if len(volumes.volumes) != len(c.expectedVolumes) { 68 | t.Fatalf("%d: expected %v, got %v", index, c.expectedVolumes, volumes.volumes) 69 | } 70 | for _, volume := range volumes.volumes { 71 | testExpectedContainsVolume(t, index, c.expectedVolumes, volume) 72 | } 73 | } 74 | } 75 | 76 | func testExpectedContainsVolume(t *testing.T, index int, expected []*Volume, volume *Volume) { 77 | found := false 78 | for _, e := range expected { 79 | if e.name == volume.name { 80 | found = true 81 | break 82 | } 83 | } 84 | if !found { 85 | t.Fatalf("%d: volume %v not found in %v", index, volume, expected) 86 | } 87 | } 88 | 89 | type volumeClient struct { 90 | client.Client 91 | expectedName string 92 | expectedVolumeCreate volume.VolumeCreateBody 93 | inspectError error 94 | inspectVolumeDriver string 95 | inspectVolumeOptions map[string]string 96 | removeError error 97 | } 98 | -------------------------------------------------------------------------------- /example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "golang.org/x/net/context" 7 | 8 | "github.com/docker/libcompose/docker" 9 | "github.com/docker/libcompose/docker/ctx" 10 | "github.com/docker/libcompose/project" 11 | "github.com/docker/libcompose/project/options" 12 | ) 13 | 14 | func main() { 15 | project, err := docker.NewProject(&ctx.Context{ 16 | Context: project.Context{ 17 | ComposeFiles: []string{"docker-compose.yml"}, 18 | ProjectName: "yeah-compose", 19 | }, 20 | }, nil) 21 | 22 | if err != nil { 23 | log.Fatal(err) 24 | } 25 | 26 | err = project.Up(context.Background(), options.Up{}) 27 | 28 | if err != nil { 29 | log.Fatal(err) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/docker/libcompose 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect 7 | github.com/Microsoft/go-winio v0.3.8 // indirect 8 | github.com/Microsoft/hcsshim v0.8.6 // indirect 9 | github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc // indirect 10 | github.com/docker/cli v0.0.0-20190711175710-5b38d82aa076 11 | github.com/docker/distribution v2.7.1+incompatible 12 | github.com/docker/docker v0.0.0-00010101000000-000000000000 13 | github.com/docker/docker-credential-helpers v0.6.3 // indirect 14 | github.com/docker/go-connections v0.3.0 15 | github.com/docker/go-metrics v0.0.0-20181218153428-b84716841b82 // indirect 16 | github.com/docker/go-units v0.4.0 17 | github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 // indirect 18 | github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 19 | github.com/gogo/protobuf v1.1.1 // indirect 20 | github.com/gorilla/context v1.1.1 // indirect 21 | github.com/gorilla/mux v0.0.0-20160317213430-0eeaf8392f5b // indirect 22 | github.com/kr/pty v0.0.0-20150511174710-5cf931ef8f76 23 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 24 | github.com/modern-go/reflect2 v1.0.1 // indirect 25 | github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c // indirect 26 | github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420 // indirect 27 | github.com/opencontainers/image-spec v0.0.0-20170515205857-f03dbe35d449 // indirect 28 | github.com/opencontainers/runc v0.0.0-20161109192122-51371867a01c // indirect 29 | github.com/pkg/errors v0.8.0 30 | github.com/prometheus/client_golang v1.1.0 // indirect 31 | github.com/sirupsen/logrus v1.2.0 32 | github.com/stretchr/testify v1.3.0 33 | github.com/urfave/cli v1.21.0 34 | github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect 35 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect 36 | github.com/xeipuuv/gojsonschema v1.1.0 37 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 38 | golang.org/x/net v0.0.0-20190613194153-d28f0bde5980 39 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 // indirect 40 | google.golang.org/grpc v1.22.1 // indirect 41 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 42 | gopkg.in/yaml.v2 v2.2.2 43 | gotest.tools v2.2.0+incompatible // indirect 44 | ) 45 | 46 | replace github.com/docker/docker => github.com/docker/engine v0.0.0-20190725163905-fa8dd90ceb7b 47 | -------------------------------------------------------------------------------- /hack/.integration-daemon-start: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # see test-integration-cli for example usage of this script 3 | 4 | export PATH="$ABS_DEST/../binary:/usr/local/bin/docker-${DOCKER_DAEMON_VERSION}:$PATH" 5 | 6 | if ! command -v docker &> /dev/null; then 7 | echo >&2 'error: docker binary should be present to be able to run test-integration' 8 | false 9 | fi 10 | 11 | # intentionally open a couple bogus file descriptors to help test that they get scrubbed in containers 12 | exec 41>&1 42>&2 13 | 14 | export DOCKER_GRAPH=/var/lib/docker/${DOCKER_DAEMON_VERSION} 15 | export DOCKER_GRAPHDRIVER=${DOCKER_GRAPHDRIVER:-vfs} 16 | export DOCKER_USERLANDPROXY=${DOCKER_USERLANDPROXY:-true} 17 | 18 | # example usage: DOCKER_STORAGE_OPTS="dm.basesize=20G,dm.loopdatasize=200G" 19 | storage_params="" 20 | if [ -n "$DOCKER_STORAGE_OPTS" ]; then 21 | IFS=',' 22 | for i in ${DOCKER_STORAGE_OPTS}; do 23 | storage_params="--storage-opt $i $storage_params" 24 | done 25 | unset IFS 26 | fi 27 | 28 | dockerd=dockerd 29 | command -v dockerd || { 30 | dockerd="docker daemon" 31 | } 32 | 33 | if [ -z "$DOCKER_TEST_HOST" ]; then 34 | export DOCKER_HOST="unix://$(cd "$DEST" && pwd)/docker.sock" # "pwd" tricks to make sure $DEST is an absolute path, not a relative one 35 | ( set -x; exec \ 36 | $dockerd --debug \ 37 | --host "$DOCKER_HOST" \ 38 | --storage-driver "$DOCKER_GRAPHDRIVER" \ 39 | --pidfile "$DEST/docker.pid" \ 40 | --userland-proxy="$DOCKER_USERLANDPROXY" \ 41 | --graph="$DOCKER_GRAPH" \ 42 | $storage_params \ 43 | &> "$DEST/docker.log" 44 | ) & 45 | # make sure that if the script exits unexpectedly, we stop this daemon we just started 46 | trap 'bundle .integration-daemon-stop' EXIT 47 | else 48 | export DOCKER_HOST="$DOCKER_TEST_HOST" 49 | fi 50 | 51 | # give it a second to come up so it's "ready" 52 | tries=5 53 | while ! docker version &> /dev/null; do 54 | (( tries-- )) 55 | if [ $tries -le 0 ]; then 56 | if [ -z "$DOCKER_HOST" ]; then 57 | echo >&2 "error: daemon failed to start" 58 | echo >&2 " check $DEST/docker.log for details" 59 | else 60 | echo >&2 "error: daemon at $DOCKER_HOST fails to 'docker version':" 61 | docker version >&2 || true 62 | fi 63 | false 64 | fi 65 | sleep 2 66 | done 67 | 68 | docker version 69 | -------------------------------------------------------------------------------- /hack/.integration-daemon-stop: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | trap - EXIT # reset EXIT trap applied in .integration-daemon-start 4 | 5 | for pidFile in $(find "$DEST" -name docker.pid); do 6 | pid=$(set -x; cat "$pidFile") 7 | ( set -x; kill "$pid" ) 8 | if ! wait "$pid"; then 9 | echo >&2 "warning: PID $pid from $pidFile had a nonzero exit code" 10 | fi 11 | done 12 | -------------------------------------------------------------------------------- /hack/.validate: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ -z "$VALIDATE_UPSTREAM" ]; then 4 | # this is kind of an expensive check, so let's not do this twice if we 5 | # are running more than one validate bundlescript 6 | 7 | VALIDATE_REPO='https://github.com/docker/libcompose.git' 8 | VALIDATE_BRANCH='master' 9 | 10 | if [ "$TRAVIS" = 'true' -a "$TRAVIS_PULL_REQUEST" != 'false' ]; then 11 | VALIDATE_REPO="https://github.com/${TRAVIS_REPO_SLUG}.git" 12 | VALIDATE_BRANCH="${TRAVIS_BRANCH}" 13 | fi 14 | 15 | VALIDATE_HEAD="$(git rev-parse --verify HEAD)" 16 | 17 | git fetch -q "$VALIDATE_REPO" "refs/heads/$VALIDATE_BRANCH" 18 | VALIDATE_UPSTREAM="$(git rev-parse --verify FETCH_HEAD)" 19 | 20 | VALIDATE_COMMIT_LOG="$VALIDATE_UPSTREAM..$VALIDATE_HEAD" 21 | VALIDATE_COMMIT_DIFF="$VALIDATE_UPSTREAM...$VALIDATE_HEAD" 22 | 23 | validate_diff() { 24 | if [ "$VALIDATE_UPSTREAM" != "$VALIDATE_HEAD" ]; then 25 | git diff "$VALIDATE_COMMIT_DIFF" "$@" 26 | fi 27 | } 28 | validate_log() { 29 | if [ "$VALIDATE_UPSTREAM" != "$VALIDATE_HEAD" ]; then 30 | git log "$VALIDATE_COMMIT_LOG" "$@" 31 | fi 32 | } 33 | fi 34 | -------------------------------------------------------------------------------- /hack/binary: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | # Get rid of existing binary 5 | rm -f libcompose-cli 6 | 7 | go generate 8 | 9 | BUILDTIME=$(date --rfc-3339 ns | sed -e 's/ /T/') &> /dev/null 10 | GITCOMMIT=$(git rev-parse --short HEAD) 11 | 12 | # Build binaries 13 | go build \ 14 | -ldflags="-w -X github.com/docker/libcompose/version.GITCOMMIT=${GITCOMMIT} -X github.com/docker/libcompose/version.BUILDTIME=${BUILDTIME} -X github.com/docker/libcompose/version.SHOWWARNING=${SHOWWARNING}" \ 15 | -o bundles/libcompose-cli \ 16 | ./cli/main 17 | -------------------------------------------------------------------------------- /hack/clean: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | cd $(dirname $0)/.. 5 | rm -rf bundles/libcompose-cli* || true 6 | -------------------------------------------------------------------------------- /hack/cross-binary: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | if [ -z "$1" ]; then 5 | # Remove windows platform because of 6 | # https://github.com/mailgun/log/issues/10 7 | OS_PLATFORM_ARG=(linux windows darwin) 8 | else 9 | OS_PLATFORM_ARG=($1) 10 | fi 11 | 12 | if [ -z "$2" ]; then 13 | OS_ARCH_ARG=(386 amd64 arm) 14 | else 15 | OS_ARCH_ARG=($2) 16 | fi 17 | 18 | # Get rid of existing binaries 19 | rm -f bundles/libcompose-cli* 20 | 21 | BUILDTIME=$(date --rfc-3339 ns | sed -e 's/ /T/') &> /dev/null 22 | GITCOMMIT=$(git rev-parse --short HEAD) 23 | 24 | # Build binaries 25 | for OS in ${OS_PLATFORM_ARG[@]}; do 26 | for ARCH in ${OS_ARCH_ARG[@]}; do 27 | OUTPUT_BIN="bundles/libcompose-cli_$OS-$ARCH" 28 | if test "$ARCH" = "arm"; then 29 | if test "$OS" = "windows" || test "$OS" = "darwin"; then 30 | # windows/arm and darwin/arm does not compile without cgo :-| 31 | continue 32 | fi 33 | fi 34 | if test "$OS" = "windows"; then 35 | OUTPUT_BIN="${OUTPUT_BIN}.exe" 36 | fi 37 | echo "Building binary for $OS/$ARCH..." 38 | GOARCH=$ARCH GOOS=$OS CGO_ENABLED=0 go build \ 39 | -ldflags="-w -X github.com/docker/libcompose/version.GITCOMMIT=${GITCOMMIT} -X github.com/docker/libcompose/version.BUILDTIME=${BUILDTIME} -X github.com/docker/libcompose/version.SHOWWARNING=${SHOWWARNING}" \ 40 | -o ${OUTPUT_BIN} ./cli/main 41 | done 42 | done 43 | -------------------------------------------------------------------------------- /hack/dind: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | # DinD: a wrapper script which allows docker to be run inside a docker container. 5 | # Original version by Jerome Petazzoni 6 | # See the blog post: https://blog.docker.com/2013/09/docker-can-now-run-within-docker/ 7 | # 8 | # This script should be executed inside a docker container in privilieged mode 9 | # ('docker run --privileged', introduced in docker 0.6). 10 | 11 | # Usage: dind CMD [ARG...] 12 | 13 | # apparmor sucks and Docker needs to know that it's in a container (c) @tianon 14 | export container=docker 15 | 16 | if [ -d /sys/kernel/security ] && ! mountpoint -q /sys/kernel/security; then 17 | mount -t securityfs none /sys/kernel/security || { 18 | echo >&2 'Could not mount /sys/kernel/security.' 19 | echo >&2 'AppArmor detection and --privileged mode might break.' 20 | } 21 | fi 22 | 23 | # Mount /tmp (conditionally) 24 | if ! mountpoint -q /tmp; then 25 | mount -t tmpfs none /tmp 26 | fi 27 | 28 | if [ $# -gt 0 ]; then 29 | exec "$@" 30 | fi 31 | 32 | echo >&2 'ERROR: No command specified.' 33 | echo >&2 'You probably want to run hack/make.sh, or maybe a shell?' 34 | -------------------------------------------------------------------------------- /hack/dockerversion: -------------------------------------------------------------------------------- 1 | // AUTOGENERATED FILE; see /go/src/github.com/docker/docker/hack/make/.go-autogen 2 | package dockerversion 3 | 4 | var ( 5 | GITCOMMIT string = "libcompose-import" 6 | VERSION string = "libcompose-import" 7 | BUILDTIME string = "libcompose-import" 8 | 9 | IAMSTATIC string = "libcompose-import" 10 | INITSHA1 string = "libcompose-import" 11 | INITPATH string = "libcompose-import" 12 | ) 13 | -------------------------------------------------------------------------------- /hack/generate-sums: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | for BINARY in docker-compose_*; do 5 | sha256sum $BINARY > $BINARY.sha256 6 | md5sum $BINARY >> $BINARY.md5 7 | done 8 | -------------------------------------------------------------------------------- /hack/inline_schema.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "text/template" 7 | ) 8 | 9 | func main() { 10 | t, err := template.New("schema_template.go").ParseFiles("./hack/schema_template.go") 11 | if err != nil { 12 | panic(err) 13 | } 14 | 15 | schemaV1, err := ioutil.ReadFile("./hack/config_schema_v1.json") 16 | if err != nil { 17 | panic(err) 18 | } 19 | schemaV2, err := ioutil.ReadFile("./hack/config_schema_v2.0.json") 20 | if err != nil { 21 | panic(err) 22 | } 23 | 24 | inlinedFile, err := os.Create("config/schema.go") 25 | if err != nil { 26 | panic(err) 27 | } 28 | 29 | err = t.Execute(inlinedFile, map[string]string{ 30 | "schemaV1": string(schemaV1), 31 | "schemaV2": string(schemaV2), 32 | }) 33 | 34 | if err != nil { 35 | panic(err) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /hack/make.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | export GO111MODULE=on 5 | export LIBCOMPOSE_PKG='github.com/docker/libcompose' 6 | 7 | # List of bundles to create when no argument is passed 8 | DEFAULT_BUNDLES=( 9 | validate-gofmt 10 | validate-dco 11 | validate-git-marks 12 | validate-lint 13 | validate-vet 14 | binary 15 | 16 | test-unit 17 | test-integration 18 | test-acceptance 19 | 20 | cross-binary 21 | ) 22 | bundle() { 23 | local bundle="$1"; shift 24 | echo "---> Making bundle: $(basename "$bundle") (in $DEST)" 25 | source "hack/$bundle" "$@" 26 | } 27 | 28 | if [ $# -lt 1 ]; then 29 | bundles=(${DEFAULT_BUNDLES[@]}) 30 | else 31 | bundles=($@) 32 | fi 33 | for bundle in ${bundles[@]}; do 34 | export DEST=. 35 | ABS_DEST="$(cd "$DEST" && pwd -P)" 36 | bundle "$bundle" 37 | echo 38 | done 39 | -------------------------------------------------------------------------------- /hack/release: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | if [ -z "$1" ]; then 4 | echo "Pass the version number as the first arg. E.g.: hack/release 1.2.3" 5 | exit 1 6 | fi 7 | VERSION=$1 8 | if [ -z "$GITHUB_TOKEN" ]; then 9 | echo "GITHUB_TOKEN must be set for github-release" 10 | exit 1 11 | fi 12 | 13 | hack/build 14 | 15 | docker run --rm -v `pwd`:/go/src/github.com/docker/libcompose docker-compose ./hack/generate-sums 16 | 17 | git tag $VERSION 18 | git push --tags 19 | docker run --rm -e GITHUB_TOKEN docker-compose github-release release \ 20 | --user docker \ 21 | --repo libcompose \ 22 | --tag $VERSION \ 23 | --name $VERSION \ 24 | --description "" \ 25 | --pre-release 26 | for BINARY in docker-compose_*; do 27 | docker run --rm -e GITHUB_TOKEN -v `pwd`:/go/src/github.com/docker/libcompose \ 28 | docker-compose github-release upload \ 29 | --user docker \ 30 | --repo libcompose \ 31 | --tag $VERSION \ 32 | --name $BINARY \ 33 | --file $BINARY 34 | done 35 | -------------------------------------------------------------------------------- /hack/schema_template.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | var schemaDataV1 = `{{.schemaV1}}` 4 | 5 | var servicesSchemaDataV2 = `{{.schemaV2}}` 6 | -------------------------------------------------------------------------------- /hack/test-acceptance: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | export SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 5 | export DEST=. 6 | 7 | # Temporarly only test on DEFAULT_DOCKER_VERSION 8 | export DOCKER_DAEMON_VERSION="${DEFAULT_DOCKER_VERSION}" 9 | 10 | bundle .integration-daemon-start 11 | 12 | cp bundles/libcompose-cli venv/bin/docker-compose 13 | . venv/bin/activate 14 | 15 | docker-compose --version 16 | cd venv/compose 17 | 18 | # if the tests fail, we still want to execute a few cleanup commands 19 | # so we save the result for the exit command at the end. 20 | # the "or" ensures that return code isn't trapped by the parent script. 21 | timeout --foreground 15m py.test -vs --tb=short $ACCEPTANCE_TEST_ARGS tests/acceptance || result=$? 22 | 23 | cd - 24 | bundle .integration-daemon-stop 25 | 26 | # TODO: exit with $result status when tests are more stable. 27 | return 0 28 | -------------------------------------------------------------------------------- /hack/test-integration: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 4 | export DEST=. 5 | 6 | TESTFLAGS="$TESTFLAGS -test.timeout=30m -check.v" 7 | 8 | run_integration_test() { 9 | echo "Running integration test against ${DOCKER_DAEMON_VERSION}" 10 | 11 | bundle .integration-daemon-start 12 | 13 | cd integration 14 | TESTVERBOSE=$TESTVERBOSE go test $TESTFLAGS 15 | cd .. 16 | 17 | bundle .integration-daemon-stop 18 | 19 | echo 20 | } 21 | 22 | if test -n "${DAEMON_VERSION}" && test "${DAEMON_VERSION}" != "all"; then 23 | if test "${DAEMON_VERSION}" = "default"; then 24 | DAEMON_VERSION="${DEFAULT_DOCKER_VERSION}" 25 | fi 26 | for version in $(echo ${DAEMON_VERSION} | cut -f1); do 27 | DOCKER_DAEMON_VERSION="${version}" run_integration_test 28 | done 29 | else 30 | for version in $(echo ${DOCKER_VERSIONS} | cut -f1); do 31 | DOCKER_DAEMON_VERSION="${version}" run_integration_test 32 | done 33 | fi 34 | 35 | 36 | -------------------------------------------------------------------------------- /hack/test-unit: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | RED=$'\033[31m' 5 | GREEN=$'\033[32m' 6 | TEXTRESET=$'\033[0m' # reset the foreground colour 7 | 8 | # This helper function walks the current directory looking for directories 9 | # holding certain files ($1 parameter), and prints their paths on standard 10 | # output, one per line. 11 | find_dirs() { 12 | find . -not \( \ 13 | \( \ 14 | -path './integration/*' \ 15 | -o -path './.git/*' \ 16 | -o -path './vendor/*' \ 17 | -o -path './bundles/*' \ 18 | \) \ 19 | -prune \ 20 | \) -name "$1" -print0 | xargs -0n1 dirname | sort -u 21 | } 22 | 23 | TESTFLAGS="-cover -coverprofile=cover.out ${TESTFLAGS}" 24 | 25 | if [ -z "$TESTDIRS" ]; then 26 | TESTDIRS=$(find_dirs '*_test.go') 27 | fi 28 | 29 | TESTS_FAILED=() 30 | 31 | set +e 32 | for dir in $TESTDIRS; do 33 | echo '+ go test -race' $TESTFLAGS "${LIBCOMPOSE_PKG}/${dir#./}" 34 | go test -race ${TESTFLAGS} "${LIBCOMPOSE_PKG}/${dir#./}" 35 | if [ $? != 0 ]; then 36 | TESTS_FAILED+=("$dir") 37 | echo 38 | echo "${RED}Tests failed: ${LIBCOMPOSE_PKG}${TEXTRESET}" 39 | sleep 1 # give it a second, so observers watching can take note 40 | fi 41 | done 42 | 43 | echo 44 | echo "Run non-race test (if any)" 45 | for dir in $TESTDIRS; do 46 | if [ -n "$(grep --include '*_test.go' -l -ri '!race' ${dir})" ]; then 47 | echo '+ go test ' $TESTFLAGS "${LIBCOMPOSE_PKG}/${dir#./}" 48 | go test ${TESTFLAGS} "${LIBCOMPOSE_PKG}/${dir#./}" 49 | if [ $? != 0 ]; then 50 | TESTS_FAILED+=("$dir") 51 | echo 52 | echo "${RED}Tests failed: ${LIBCOMPOSE_PKG}${TEXTRESET}" 53 | sleep 1 # give it a second, so observers watching can take note 54 | fi 55 | fi 56 | done 57 | set -e 58 | 59 | echo 60 | 61 | # if some tests fail, we want the bundlescript to fail, but we want to 62 | # try running ALL the tests first, hence TESTS_FAILED 63 | if [ "${#TESTS_FAILED[@]}" -gt 0 ]; then 64 | echo "${RED}Test failures in: ${TESTS_FAILED[@]}${TEXTRESET}" 65 | echo 66 | false 67 | else 68 | echo "${GREEN}Test success${TEXTRESET}" 69 | echo 70 | true 71 | fi 72 | -------------------------------------------------------------------------------- /hack/validate-dco: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | source "$(dirname "$BASH_SOURCE")/.validate" 4 | 5 | adds=$(validate_diff --numstat | awk '{ s += $1 } END { print s }') 6 | dels=$(validate_diff --numstat | awk '{ s += $2 } END { print s }') 7 | notDocs="$(validate_diff --numstat | awk '$3 !~ /^docs\// { print $3 }')" 8 | 9 | : ${adds:=0} 10 | : ${dels:=0} 11 | 12 | # "Username may only contain alphanumeric characters or dashes and cannot begin with a dash" 13 | githubUsernameRegex='[a-zA-Z0-9][a-zA-Z0-9-]+' 14 | 15 | # https://github.com/docker/docker/blob/master/CONTRIBUTING.md#sign-your-work 16 | dcoPrefix='Signed-off-by:' 17 | dcoRegex="^(Docker-DCO-1.1-)?$dcoPrefix ([^<]+) <([^<>@]+@[^<>]+)>( \\(github: ($githubUsernameRegex)\\))?$" 18 | 19 | check_dco() { 20 | grep -qE "$dcoRegex" 21 | } 22 | 23 | if [ $adds -eq 0 -a $dels -eq 0 ]; then 24 | echo '0 adds, 0 deletions; nothing to validate! :)' 25 | elif [ -z "$notDocs" -a $adds -le 1 -a $dels -le 1 ]; then 26 | echo 'Congratulations! DCO small-patch-exception material!' 27 | else 28 | commits=( $(validate_log --format='format:%H%n') ) 29 | badCommits=() 30 | for commit in "${commits[@]}"; do 31 | if [ -z "$(git log -1 --format='format:' --name-status "$commit")" ]; then 32 | # no content (ie, Merge commit, etc) 33 | continue 34 | fi 35 | if ! git log -1 --format='format:%B' "$commit" | check_dco; then 36 | badCommits+=( "$commit" ) 37 | fi 38 | done 39 | if [ ${#badCommits[@]} -eq 0 ]; then 40 | echo "Congratulations! All commits are properly signed with the DCO!" 41 | else 42 | { 43 | echo "These commits do not have a proper '$dcoPrefix' marker:" 44 | for commit in "${badCommits[@]}"; do 45 | echo " - $commit" 46 | done 47 | echo 48 | echo 'Please amend each commit to include a properly formatted DCO marker.' 49 | echo 50 | echo 'Visit the following URL for information about the Docker DCO:' 51 | echo ' https://github.com/docker/docker/blob/master/CONTRIBUTING.md#sign-your-work' 52 | echo 53 | } >&2 54 | false 55 | fi 56 | fi 57 | -------------------------------------------------------------------------------- /hack/validate-git-marks: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | source "$(dirname "$BASH_SOURCE")/.validate" 4 | 5 | # folders=$(find * -type d | egrep -v '^Godeps|bundles|.git') 6 | 7 | IFS=$'\n' 8 | files=( $(validate_diff --diff-filter=ACMR --name-only -- '*' | grep -v '^vendor/' || true) ) 9 | unset IFS 10 | 11 | badFiles=() 12 | for f in "${files[@]}"; do 13 | if [ $(grep -r "^<<<<<<<" $f) ]; then 14 | badFiles+=( "$f" ) 15 | continue 16 | fi 17 | 18 | if [ $(grep -r "^>>>>>>>" $f) ]; then 19 | badFiles+=( "$f" ) 20 | continue 21 | fi 22 | 23 | if [ $(grep -r "^=======$" $f) ]; then 24 | badFiles+=( "$f" ) 25 | continue 26 | fi 27 | set -e 28 | done 29 | 30 | 31 | if [ ${#badFiles[@]} -eq 0 ]; then 32 | echo 'Congratulations! There is no conflict.' 33 | else 34 | { 35 | echo "There is trace of conflict(s) in the following files :" 36 | for f in "${badFiles[@]}"; do 37 | echo " - $f" 38 | done 39 | echo 40 | echo 'Please fix the conflict(s) commit the result.' 41 | echo 42 | } >&2 43 | false 44 | fi 45 | -------------------------------------------------------------------------------- /hack/validate-gofmt: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | source "$(dirname "$BASH_SOURCE")/.validate" 4 | 5 | IFS=$'\n' 6 | files=( $(validate_diff --diff-filter=ACMR --name-only -- '*.go' | grep -v '^vendor/' || true) ) 7 | unset IFS 8 | 9 | badFiles=() 10 | for f in "${files[@]}"; do 11 | # we use "git show" here to validate that what's committed is formatted 12 | if [ "$(git show "$VALIDATE_HEAD:$f" | gofmt -s -l)" ]; then 13 | badFiles+=( "$f" ) 14 | fi 15 | done 16 | 17 | if [ ${#badFiles[@]} -eq 0 ]; then 18 | echo 'Congratulations! All Go source files are properly formatted.' 19 | else 20 | { 21 | echo "These files are not properly gofmt'd:" 22 | for f in "${badFiles[@]}"; do 23 | echo " - $f" 24 | done 25 | echo 26 | echo 'Please reformat the above files using "gofmt -s -w" and commit the result.' 27 | echo 28 | } >&2 29 | false 30 | fi 31 | -------------------------------------------------------------------------------- /hack/validate-lint: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | source "$(dirname "$BASH_SOURCE")/.validate" 4 | 5 | # We will eventually get to the point where packages should be the complete list 6 | # of subpackages, vendoring excluded, as given by: 7 | # 8 | IFS=$'\n' 9 | files=( $(validate_diff --diff-filter=ACMR --name-only -- '*.go' | grep -v '^vendor/\|^integration' || true) ) 10 | unset IFS 11 | 12 | errors=() 13 | for f in "${files[@]}"; do 14 | # we use "git show" here to validate that what's committed passes go lint 15 | failedLint=$(golint "$f") 16 | if [ "$failedLint" ]; then 17 | errors+=( "$failedLint" ) 18 | fi 19 | done 20 | 21 | if [ ${#errors[@]} -eq 0 ]; then 22 | echo 'Congratulations! All Go source files have been linted.' 23 | else 24 | { 25 | echo "Errors from golint:" 26 | for err in "${errors[@]}"; do 27 | echo "$err" 28 | done 29 | echo 30 | echo 'Please fix the above errors. You can test via "golint" and commit the result.' 31 | echo 32 | } >&2 33 | false 34 | fi 35 | -------------------------------------------------------------------------------- /hack/validate-vet: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | source "$(dirname "$BASH_SOURCE")/.validate" 4 | 5 | IFS=$'\n' 6 | files=( $(validate_diff --diff-filter=ACMR --name-only -- '*.go' | grep -v '^vendor/' || true) ) 7 | unset IFS 8 | 9 | errors=() 10 | for f in "${files[@]}"; do 11 | # we use "git show" here to validate that what's committed passes go vet 12 | failedVet=$(go vet "$f") 13 | if [ "$failedVet" ]; then 14 | errors+=( "$failedVet" ) 15 | fi 16 | done 17 | 18 | 19 | if [ ${#errors[@]} -eq 0 ]; then 20 | echo 'Congratulations! All Go source files have been vetted.' 21 | else 22 | { 23 | echo "Errors from go vet:" 24 | for err in "${errors[@]}"; do 25 | echo " - $err" 26 | done 27 | echo 28 | echo 'Please fix the above errors. You can test via "go vet" and commit the result.' 29 | echo 30 | } >&2 31 | false 32 | fi 33 | -------------------------------------------------------------------------------- /integration/api_event_test.go: -------------------------------------------------------------------------------- 1 | package integration 2 | 3 | import ( 4 | "time" 5 | 6 | "golang.org/x/net/context" 7 | check "gopkg.in/check.v1" 8 | 9 | "github.com/docker/libcompose/docker" 10 | "github.com/docker/libcompose/docker/ctx" 11 | "github.com/docker/libcompose/project" 12 | "github.com/docker/libcompose/project/events" 13 | "github.com/docker/libcompose/project/options" 14 | ) 15 | 16 | func (s *APISuite) TestEvents(c *check.C) { 17 | testRequires(c, not(DaemonVersionIs("1.9"))) 18 | composeFile := ` 19 | simple: 20 | image: busybox:latest 21 | command: top 22 | another: 23 | image: busybox:latest 24 | command: top 25 | ` 26 | project, err := docker.NewProject(&ctx.Context{ 27 | Context: project.Context{ 28 | ComposeBytes: [][]byte{[]byte(composeFile)}, 29 | ProjectName: "test-api-events", 30 | }, 31 | }, nil) 32 | c.Assert(err, check.IsNil) 33 | 34 | ctx, cancelFun := context.WithCancel(context.Background()) 35 | 36 | evts, err := project.Events(ctx) 37 | c.Assert(err, check.IsNil) 38 | 39 | go func() { 40 | c.Assert(project.Up(ctx, options.Up{}), check.IsNil) 41 | // Close after everything is done 42 | time.Sleep(250 * time.Millisecond) 43 | cancelFun() 44 | close(evts) 45 | }() 46 | 47 | actual := []events.ContainerEvent{} 48 | for event := range evts { 49 | actual = append(actual, event) 50 | } 51 | 52 | // Should be 4 events (2 create, 2 start) 53 | c.Assert(len(actual), check.Equals, 4, check.Commentf("%v", actual)) 54 | } 55 | -------------------------------------------------------------------------------- /integration/api_test.go: -------------------------------------------------------------------------------- 1 | package integration 2 | 3 | import ( 4 | . "gopkg.in/check.v1" 5 | 6 | "golang.org/x/net/context" 7 | 8 | "github.com/docker/libcompose/docker" 9 | "github.com/docker/libcompose/docker/ctx" 10 | "github.com/docker/libcompose/project" 11 | "github.com/docker/libcompose/project/options" 12 | ) 13 | 14 | func init() { 15 | Suite(&APISuite{}) 16 | } 17 | 18 | type APISuite struct{} 19 | 20 | func (s *APISuite) TestVolumeWithoutComposeFile(c *C) { 21 | service := ` 22 | service: 23 | image: busybox 24 | command: echo Hello world! 25 | volumes: 26 | - /etc/selinux:/etc/selinux` 27 | 28 | project, err := docker.NewProject(&ctx.Context{ 29 | Context: project.Context{ 30 | ComposeBytes: [][]byte{[]byte(service)}, 31 | ProjectName: "test-volume-without-compose-file", 32 | }, 33 | }, nil) 34 | 35 | c.Assert(err, IsNil) 36 | 37 | err = project.Up(context.Background(), options.Up{}) 38 | c.Assert(err, IsNil) 39 | } 40 | -------------------------------------------------------------------------------- /integration/assets/build/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM busybox 2 | CMD ["echo", "two"] 3 | -------------------------------------------------------------------------------- /integration/assets/build/docker-compose.yml: -------------------------------------------------------------------------------- 1 | one: 2 | build: one 3 | two: 4 | build: . 5 | links: 6 | - one 7 | -------------------------------------------------------------------------------- /integration/assets/build/one/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM busybox 2 | CMD ["echo", "one"] 3 | -------------------------------------------------------------------------------- /integration/assets/env/.env: -------------------------------------------------------------------------------- 1 | FOO=bar 2 | 3 | -------------------------------------------------------------------------------- /integration/assets/interpolation/docker-compose.yml: -------------------------------------------------------------------------------- 1 | base: 2 | image: $IMAGE 3 | -------------------------------------------------------------------------------- /integration/assets/multiple-composefiles-default/docker-compose.override.yml: -------------------------------------------------------------------------------- 1 | yetanother: 2 | image: busybox:latest 3 | command: top 4 | -------------------------------------------------------------------------------- /integration/assets/multiple-composefiles-default/docker-compose.yml: -------------------------------------------------------------------------------- 1 | simple: 2 | image: busybox:latest 3 | command: top 4 | another: 5 | image: busybox:latest 6 | command: top 7 | -------------------------------------------------------------------------------- /integration/assets/multiple/one.yml: -------------------------------------------------------------------------------- 1 | multiple: 2 | image: tianon/true 3 | environment: 4 | - KEY1=VAL1 5 | simple: 6 | image: busybox:latest 7 | command: top 8 | another: 9 | image: busybox:latest 10 | command: top 11 | -------------------------------------------------------------------------------- /integration/assets/multiple/two.yml: -------------------------------------------------------------------------------- 1 | multiple: 2 | image: busybox 3 | command: echo two 4 | environment: 5 | - KEY2=VAL2 6 | yetanother: 7 | image: busybox:latest 8 | command: top 9 | -------------------------------------------------------------------------------- /integration/assets/networks/bridge.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | 3 | services: 4 | web: 5 | image: busybox 6 | command: top 7 | networks: 8 | - bridge 9 | - default 10 | -------------------------------------------------------------------------------- /integration/assets/networks/default-network-config.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | services: 3 | simple: 4 | image: busybox:latest 5 | command: top 6 | another: 7 | image: busybox:latest 8 | command: top 9 | networks: 10 | default: 11 | driver: bridge 12 | driver_opts: 13 | "com.docker.network.bridge.enable_icc": "false" 14 | -------------------------------------------------------------------------------- /integration/assets/networks/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | 3 | services: 4 | web: 5 | image: busybox 6 | command: top 7 | networks: ["front"] 8 | app: 9 | image: busybox 10 | command: top 11 | networks: ["front", "back"] 12 | links: 13 | - "db:database" 14 | db: 15 | image: busybox 16 | command: top 17 | networks: ["back"] 18 | 19 | networks: 20 | front: {} 21 | back: {} 22 | -------------------------------------------------------------------------------- /integration/assets/networks/external-default.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | services: 3 | simple: 4 | image: busybox:latest 5 | command: top 6 | another: 7 | image: busybox:latest 8 | command: top 9 | networks: 10 | default: 11 | external: 12 | name: composetest_external_network 13 | -------------------------------------------------------------------------------- /integration/assets/networks/external-networks.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | 3 | services: 4 | web: 5 | image: busybox 6 | command: top 7 | networks: 8 | - networks_foo 9 | - bar 10 | 11 | networks: 12 | networks_foo: 13 | external: true 14 | bar: 15 | external: 16 | name: networks_bar 17 | -------------------------------------------------------------------------------- /integration/assets/networks/missing-network.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | 3 | services: 4 | web: 5 | image: busybox 6 | command: top 7 | networks: ["foo"] 8 | 9 | networks: 10 | bar: {} 11 | -------------------------------------------------------------------------------- /integration/assets/networks/network-aliases.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | 3 | services: 4 | web: 5 | image: busybox 6 | command: top 7 | networks: 8 | front: 9 | aliases: 10 | - forward_facing 11 | - ahead 12 | back: 13 | 14 | networks: 15 | front: {} 16 | back: {} 17 | -------------------------------------------------------------------------------- /integration/assets/networks/network-mode.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | 3 | services: 4 | bridge: 5 | image: busybox 6 | command: top 7 | network_mode: bridge 8 | 9 | service: 10 | image: busybox 11 | command: top 12 | network_mode: "service:bridge" 13 | 14 | container: 15 | image: busybox 16 | command: top 17 | network_mode: "container:composetest_network_mode_container" 18 | 19 | host: 20 | image: busybox 21 | command: top 22 | network_mode: host 23 | 24 | none: 25 | image: busybox 26 | command: top 27 | network_mode: none 28 | -------------------------------------------------------------------------------- /integration/assets/networks/network-static-addresses.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | 3 | services: 4 | web: 5 | image: busybox 6 | command: top 7 | networks: 8 | static_test: 9 | ipv4_address: 172.16.100.100 10 | ipv6_address: fe80::1001:100 11 | 12 | networks: 13 | static_test: 14 | driver: bridge 15 | driver_opts: 16 | com.docker.network.enable_ipv6: "true" 17 | ipam: 18 | driver: default 19 | config: 20 | - subnet: 172.16.100.0/24 21 | gateway: 172.16.100.1 22 | - subnet: fe80::/64 23 | gateway: fe80::1001:1 24 | -------------------------------------------------------------------------------- /integration/assets/regression/60-volume_from.yml: -------------------------------------------------------------------------------- 1 | first: 2 | image: busybox 3 | volumes: 4 | - /bundle 5 | second: 6 | image: busybox 7 | volumes_from: 8 | - first 9 | 10 | -------------------------------------------------------------------------------- /integration/assets/regression/volume_from_container_name.yml: -------------------------------------------------------------------------------- 1 | first: 2 | image: busybox 3 | container_name: first_container_name 4 | volumes: 5 | - /bundle 6 | second: 7 | image: busybox 8 | volumes_from: 9 | - first 10 | -------------------------------------------------------------------------------- /integration/assets/run/docker-compose.yml: -------------------------------------------------------------------------------- 1 | hello: 2 | image: busybox 3 | -------------------------------------------------------------------------------- /integration/assets/simple-build/docker-compose.yml: -------------------------------------------------------------------------------- 1 | one: 2 | build: one 3 | -------------------------------------------------------------------------------- /integration/assets/simple-build/one/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM busybox 2 | CMD ["echo", "one"] 3 | -------------------------------------------------------------------------------- /integration/assets/v2-build-args/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM busybox:latest 2 | ARG buildno=0 3 | RUN echo buildno is ${buildno} 4 | -------------------------------------------------------------------------------- /integration/assets/v2-build-args/docker-compose.yml: -------------------------------------------------------------------------------- 1 | 2 | version: "2" 3 | 4 | services: 5 | simple: 6 | build: 7 | context: . 8 | args: 9 | buildno: 1 10 | -------------------------------------------------------------------------------- /integration/assets/v2-dependencies/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "2.0" 2 | services: 3 | db: 4 | image: busybox:latest 5 | command: top 6 | web: 7 | image: busybox:latest 8 | command: top 9 | depends_on: 10 | - db 11 | console: 12 | image: busybox:latest 13 | command: top 14 | -------------------------------------------------------------------------------- /integration/assets/v2-full/Dockerfile: -------------------------------------------------------------------------------- 1 | 2 | FROM busybox:latest 3 | RUN echo something 4 | CMD top 5 | -------------------------------------------------------------------------------- /integration/assets/v2-full/docker-compose.yml: -------------------------------------------------------------------------------- 1 | 2 | version: "2" 3 | 4 | volumes: 5 | data: 6 | driver: local 7 | 8 | networks: 9 | front: {} 10 | 11 | services: 12 | web: 13 | build: 14 | context: . 15 | networks: 16 | - front 17 | - default 18 | volumes_from: 19 | - other 20 | 21 | other: 22 | image: busybox:latest 23 | command: top 24 | volumes: 25 | - /data 26 | -------------------------------------------------------------------------------- /integration/assets/v2-simple/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | services: 3 | simple: 4 | image: busybox:latest 5 | command: top 6 | another: 7 | image: busybox:latest 8 | command: top 9 | -------------------------------------------------------------------------------- /integration/assets/v2-simple/links-invalid.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | services: 3 | simple: 4 | image: busybox:latest 5 | command: top 6 | links: 7 | - another 8 | another: 9 | image: busybox:latest 10 | command: top 11 | -------------------------------------------------------------------------------- /integration/assets/validation/invalid/docker-compose.v1.yml: -------------------------------------------------------------------------------- 1 | base: 2 | image: busybox 3 | ports: invalid_type 4 | -------------------------------------------------------------------------------- /integration/assets/validation/invalid/docker-compose.v2.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | base: 4 | image: busybox 5 | ports: invalid_type 6 | -------------------------------------------------------------------------------- /integration/assets/validation/valid/docker-compose.v1.yml: -------------------------------------------------------------------------------- 1 | base: 2 | image: busybox 3 | -------------------------------------------------------------------------------- /integration/assets/validation/valid/docker-compose.v2.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | base: 4 | image: busybox 5 | -------------------------------------------------------------------------------- /integration/assets/volumes/relative-volumes.yml: -------------------------------------------------------------------------------- 1 | server: 2 | image: busybox 3 | volumes: 4 | - .:/path 5 | -------------------------------------------------------------------------------- /integration/build_test.go: -------------------------------------------------------------------------------- 1 | package integration 2 | 3 | import ( 4 | "fmt" 5 | "os/exec" 6 | "strings" 7 | 8 | "golang.org/x/net/context" 9 | 10 | . "gopkg.in/check.v1" 11 | ) 12 | 13 | func (s *CliSuite) TestBuild(c *C) { 14 | p := s.RandomProject() 15 | cmd := exec.Command(s.command, "-f", "./assets/build/docker-compose.yml", "-p", p, "build") 16 | err := cmd.Run() 17 | 18 | oneImageName := fmt.Sprintf("%s_one", p) 19 | twoImageName := fmt.Sprintf("%s_two", p) 20 | 21 | c.Assert(err, IsNil) 22 | 23 | client := GetClient(c) 24 | one, _, err := client.ImageInspectWithRaw(context.Background(), oneImageName) 25 | c.Assert(err, IsNil) 26 | c.Assert([]string(one.Config.Cmd), DeepEquals, []string{"echo", "one"}) 27 | 28 | two, _, err := client.ImageInspectWithRaw(context.Background(), twoImageName) 29 | c.Assert(err, IsNil) 30 | c.Assert([]string(two.Config.Cmd), DeepEquals, []string{"echo", "two"}) 31 | } 32 | 33 | func (s *CliSuite) TestBuildWithNoCache1(c *C) { 34 | p := s.RandomProject() 35 | cmd := exec.Command(s.command, "-f", "./assets/build/docker-compose.yml", "-p", p, "build") 36 | 37 | output, err := cmd.Output() 38 | c.Assert(err, IsNil) 39 | 40 | cmd = exec.Command(s.command, "-f", "./assets/build/docker-compose.yml", "-p", p, "build") 41 | output, err = cmd.Output() 42 | c.Assert(err, IsNil) 43 | out := string(output[:]) 44 | c.Assert(strings.Contains(out, 45 | "Using cache"), 46 | Equals, true, Commentf("%s", out)) 47 | } 48 | 49 | func (s *CliSuite) TestBuildWithNoCache2(c *C) { 50 | p := s.RandomProject() 51 | cmd := exec.Command(s.command, "-f", "./assets/build/docker-compose.yml", "-p", p, "build") 52 | 53 | output, err := cmd.Output() 54 | c.Assert(err, IsNil) 55 | 56 | cmd = exec.Command(s.command, "-f", "./assets/build/docker-compose.yml", "-p", p, "build", "--no-cache") 57 | output, err = cmd.Output() 58 | c.Assert(err, IsNil) 59 | out := string(output[:]) 60 | c.Assert(strings.Contains(out, 61 | "Using cache"), 62 | Equals, false, Commentf("%s", out)) 63 | } 64 | 65 | func (s *CliSuite) TestBuildWithNoCache3(c *C) { 66 | p := s.RandomProject() 67 | cmd := exec.Command(s.command, "-f", "./assets/build/docker-compose.yml", "-p", p, "build", "--no-cache") 68 | err := cmd.Run() 69 | 70 | oneImageName := fmt.Sprintf("%s_one", p) 71 | twoImageName := fmt.Sprintf("%s_two", p) 72 | 73 | c.Assert(err, IsNil) 74 | 75 | client := GetClient(c) 76 | one, _, err := client.ImageInspectWithRaw(context.Background(), oneImageName) 77 | c.Assert(err, IsNil) 78 | c.Assert([]string(one.Config.Cmd), DeepEquals, []string{"echo", "one"}) 79 | 80 | two, _, err := client.ImageInspectWithRaw(context.Background(), twoImageName) 81 | c.Assert(err, IsNil) 82 | c.Assert([]string(two.Config.Cmd), DeepEquals, []string{"echo", "two"}) 83 | } 84 | 85 | func (s *CliSuite) TestBuildWithArgs(c *C) { 86 | p := s.RandomProject() 87 | cmd := exec.Command(s.command, "-f", "./assets/v2-build-args/docker-compose.yml", "-p", p, "build") 88 | 89 | output, err := cmd.Output() 90 | c.Assert(err, IsNil) 91 | 92 | c.Assert(strings.Contains(string(output), "buildno is 1"), Equals, true, Commentf("Expected 'buildno is 1' in output, got \n%s", string(output))) 93 | c.Assert(strings.Contains(string(output), "buildno is 0"), Equals, false, Commentf("Expected to not find 'buildno is 0' in output, got \n%s", string(output))) 94 | } 95 | -------------------------------------------------------------------------------- /integration/down_test.go: -------------------------------------------------------------------------------- 1 | package integration 2 | 3 | import ( 4 | "fmt" 5 | 6 | . "gopkg.in/check.v1" 7 | ) 8 | 9 | func (s *CliSuite) TestDown(c *C) { 10 | p := s.ProjectFromText(c, "up", SimpleTemplate) 11 | 12 | name := fmt.Sprintf("%s_%s_1", p, "hello") 13 | 14 | cn := s.GetContainerByName(c, name) 15 | c.Assert(cn, NotNil) 16 | c.Assert(cn.State.Running, Equals, true) 17 | 18 | s.FromText(c, p, "down", SimpleTemplate) 19 | 20 | containers := s.GetContainersByProject(c, p) 21 | c.Assert(len(containers), Equals, 0) 22 | } 23 | 24 | func (s *CliSuite) TestDownMultiple(c *C) { 25 | p := s.ProjectFromText(c, "up", SimpleTemplate) 26 | 27 | s.FromText(c, p, "scale", "hello=2", SimpleTemplate) 28 | 29 | containers := s.GetContainersByProject(c, p) 30 | c.Assert(len(containers), Equals, 2) 31 | 32 | s.FromText(c, p, "down", SimpleTemplate) 33 | 34 | containers = s.GetContainersByProject(c, p) 35 | c.Assert(len(containers), Equals, 0) 36 | } 37 | -------------------------------------------------------------------------------- /integration/env_test.go: -------------------------------------------------------------------------------- 1 | package integration 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | 9 | . "gopkg.in/check.v1" 10 | ) 11 | 12 | func (s *CliSuite) TestCreateWithEnvInCurrentDir(c *C) { 13 | cwd, err := os.Getwd() 14 | c.Assert(err, IsNil) 15 | defer os.Chdir(cwd) 16 | 17 | c.Assert(os.Chdir("./assets/env"), IsNil, Commentf("Could not change current directory to ./assets/env")) 18 | 19 | projectName := s.RandomProject() 20 | cmd := exec.Command("../../../bundles/libcompose-cli", "--verbose", "-p", projectName, "-f", "-", "create") 21 | cmd.Stdin = bytes.NewBufferString(` 22 | hello: 23 | image: tianon/true 24 | labels: 25 | - "FOO=${FOO}" 26 | `) 27 | output, err := cmd.CombinedOutput() 28 | c.Assert(err, IsNil, Commentf("%s", output)) 29 | 30 | name := fmt.Sprintf("%s_%s_1", projectName, "hello") 31 | cn := s.GetContainerByName(c, name) 32 | c.Assert(cn, NotNil) 33 | 34 | c.Assert(len(cn.Config.Labels), Equals, 7, Commentf("%v", cn.Config.Env)) 35 | c.Assert(cn.Config.Labels["FOO"], Equals, "bar", Commentf("%v", cn.Config.Labels)) 36 | } 37 | 38 | func (s *CliSuite) TestCreateWithEnvNotInCurrentDir(c *C) { 39 | p := s.CreateProjectFromText(c, ` 40 | hello: 41 | image: tianon/true 42 | `) 43 | 44 | name := fmt.Sprintf("%s_%s_1", p, "hello") 45 | cn := s.GetContainerByName(c, name) 46 | c.Assert(cn, NotNil) 47 | 48 | c.Assert(len(cn.Config.Labels), Equals, 6, Commentf("%v", cn.Config.Labels)) 49 | } 50 | -------------------------------------------------------------------------------- /integration/kill_test.go: -------------------------------------------------------------------------------- 1 | package integration 2 | 3 | import ( 4 | "fmt" 5 | 6 | . "gopkg.in/check.v1" 7 | ) 8 | 9 | func (s *CliSuite) TestKill(c *C) { 10 | p := s.ProjectFromText(c, "up", SimpleTemplate) 11 | 12 | name := fmt.Sprintf("%s_%s_1", p, "hello") 13 | 14 | cn := s.GetContainerByName(c, name) 15 | c.Assert(cn, NotNil) 16 | c.Assert(cn.State.Running, Equals, true) 17 | 18 | s.FromText(c, p, "kill", SimpleTemplate) 19 | 20 | cn = s.GetContainerByName(c, name) 21 | c.Assert(cn, NotNil) 22 | c.Assert(cn.State.Running, Equals, false) 23 | } 24 | -------------------------------------------------------------------------------- /integration/pause_unpause_test.go: -------------------------------------------------------------------------------- 1 | package integration 2 | 3 | import ( 4 | "fmt" 5 | 6 | . "gopkg.in/check.v1" 7 | ) 8 | 9 | func (s *CliSuite) TestPause(c *C) { 10 | p := s.ProjectFromText(c, "up", SimpleTemplate) 11 | 12 | name := fmt.Sprintf("%s_%s_1", p, "hello") 13 | cn := s.GetContainerByName(c, name) 14 | c.Assert(cn, NotNil) 15 | c.Assert(cn.State.Running, Equals, true) 16 | c.Assert(cn.State.Paused, Equals, false) 17 | 18 | s.FromText(c, p, "pause", SimpleTemplate) 19 | 20 | cn = s.GetContainerByName(c, name) 21 | c.Assert(cn, NotNil) 22 | c.Assert(cn.State.Running, Equals, true) 23 | c.Assert(cn.State.Paused, Equals, true) 24 | } 25 | 26 | func (s *CliSuite) TestPauseAlreadyPausedService(c *C) { 27 | p := s.ProjectFromText(c, "up", SimpleTemplate) 28 | 29 | name := fmt.Sprintf("%s_%s_1", p, "hello") 30 | cn := s.GetContainerByName(c, name) 31 | c.Assert(cn, NotNil) 32 | 33 | c.Assert(cn.State.Running, Equals, true) 34 | c.Assert(cn.State.Paused, Equals, false) 35 | 36 | s.FromText(c, p, "pause", SimpleTemplate) 37 | 38 | cn = s.GetContainerByName(c, name) 39 | c.Assert(cn, NotNil) 40 | c.Assert(cn.State.Running, Equals, true) 41 | c.Assert(cn.State.Paused, Equals, true) 42 | 43 | s.FromText(c, p, "pause", SimpleTemplate) 44 | 45 | cn = s.GetContainerByName(c, name) 46 | c.Assert(cn, NotNil) 47 | c.Assert(cn.State.Running, Equals, true) 48 | c.Assert(cn.State.Paused, Equals, true) 49 | } 50 | 51 | func (s *CliSuite) TestUnpause(c *C) { 52 | p := s.ProjectFromText(c, "up", SimpleTemplate) 53 | 54 | name := fmt.Sprintf("%s_%s_1", p, "hello") 55 | cn := s.GetContainerByName(c, name) 56 | c.Assert(cn, NotNil) 57 | 58 | c.Assert(cn.State.Running, Equals, true) 59 | c.Assert(cn.State.Paused, Equals, false) 60 | 61 | s.FromText(c, p, "pause", SimpleTemplate) 62 | 63 | cn = s.GetContainerByName(c, name) 64 | c.Assert(cn, NotNil) 65 | c.Assert(cn.State.Running, Equals, true) 66 | c.Assert(cn.State.Paused, Equals, true) 67 | 68 | s.FromText(c, p, "unpause", SimpleTemplate) 69 | 70 | cn = s.GetContainerByName(c, name) 71 | c.Assert(cn, NotNil) 72 | c.Assert(cn.State.Running, Equals, true) 73 | c.Assert(cn.State.Paused, Equals, false) 74 | } 75 | 76 | func (s *CliSuite) TestUnpauseNotPausedService(c *C) { 77 | p := s.ProjectFromText(c, "up", SimpleTemplate) 78 | 79 | name := fmt.Sprintf("%s_%s_1", p, "hello") 80 | cn := s.GetContainerByName(c, name) 81 | c.Assert(cn, NotNil) 82 | 83 | c.Assert(cn.State.Running, Equals, true) 84 | c.Assert(cn.State.Paused, Equals, false) 85 | 86 | s.FromText(c, p, "unpause", SimpleTemplate) 87 | 88 | cn = s.GetContainerByName(c, name) 89 | c.Assert(cn, NotNil) 90 | c.Assert(cn.State.Running, Equals, true) 91 | c.Assert(cn.State.Paused, Equals, false) 92 | } 93 | -------------------------------------------------------------------------------- /integration/ps_test.go: -------------------------------------------------------------------------------- 1 | package integration 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | . "gopkg.in/check.v1" 8 | ) 9 | 10 | func (s *CliSuite) TestPs(c *C) { 11 | p := s.ProjectFromText(c, "up", SimpleTemplate) 12 | 13 | name := fmt.Sprintf("%s_%s_1", p, "hello") 14 | 15 | _, out := s.FromTextCaptureOutput(c, p, "ps", SimpleTemplate) 16 | 17 | c.Assert(strings.Contains(out, 18 | fmt.Sprintf(`%s sh Up Less than a second`, name)), 19 | Equals, true, Commentf("%s", out)) 20 | } 21 | 22 | func (s *CliSuite) TestPsQuiet(c *C) { 23 | p := s.ProjectFromText(c, "up", SimpleTemplate) 24 | 25 | name := fmt.Sprintf("%s_%s_1", p, "hello") 26 | container := s.GetContainerByName(c, name) 27 | 28 | _, out := s.FromTextCaptureOutput(c, p, "ps", "-q", SimpleTemplate) 29 | 30 | c.Assert(strings.Contains(out, 31 | fmt.Sprintf(`%s`, container.ID)), 32 | Equals, true, Commentf("%s", out)) 33 | } 34 | -------------------------------------------------------------------------------- /integration/pull_test.go: -------------------------------------------------------------------------------- 1 | package integration 2 | 3 | import ( 4 | . "gopkg.in/check.v1" 5 | ) 6 | 7 | func (s *CliSuite) TestPull(c *C) { 8 | //TODO: This doesn't test much 9 | s.ProjectFromText(c, "pull", ` 10 | hello: 11 | image: tianon/true 12 | stdin_open: true 13 | tty: true 14 | `) 15 | } 16 | -------------------------------------------------------------------------------- /integration/requirements.go: -------------------------------------------------------------------------------- 1 | package integration 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "os" 7 | "runtime" 8 | "strings" 9 | "time" 10 | 11 | check "gopkg.in/check.v1" 12 | ) 13 | 14 | type testCondition func() bool 15 | 16 | type testRequirement struct { 17 | Condition testCondition 18 | SkipMessage string 19 | } 20 | 21 | // List test requirements 22 | var ( 23 | IsWindows = testRequirement{ 24 | func() bool { return runtime.GOOS == "windows" }, 25 | "Test requires a Windows daemon", 26 | } 27 | IsLinux = testRequirement{ 28 | func() bool { return runtime.GOOS == "linux" }, 29 | "Test requires a Linux daemon", 30 | } 31 | Network = testRequirement{ 32 | func() bool { 33 | // Set a timeout on the GET at 15s 34 | var timeout = 15 * time.Second 35 | var url = "https://hub.docker.com" 36 | 37 | client := http.Client{ 38 | Timeout: timeout, 39 | } 40 | 41 | resp, err := client.Get(url) 42 | if err != nil && strings.Contains(err.Error(), "use of closed network connection") { 43 | panic(fmt.Sprintf("Timeout for GET request on %s", url)) 44 | } 45 | if resp != nil { 46 | resp.Body.Close() 47 | } 48 | return err == nil 49 | }, 50 | "Test requires network availability, environment variable set to none to run in a non-network enabled mode.", 51 | } 52 | ) 53 | 54 | func not(r testRequirement) testRequirement { 55 | return testRequirement{ 56 | func() bool { 57 | return !r.Condition() 58 | }, 59 | fmt.Sprintf("Not(%s)", r.SkipMessage), 60 | } 61 | } 62 | 63 | func DaemonVersionIs(version string) testRequirement { 64 | return testRequirement{ 65 | func() bool { 66 | return strings.Contains(os.Getenv("DOCKER_DAEMON_VERSION"), version) 67 | }, 68 | "Test requires the daemon version to be " + version, 69 | } 70 | } 71 | 72 | // testRequires checks if the environment satisfies the requirements 73 | // for the test to run or skips the tests. 74 | func testRequires(c *check.C, requirements ...testRequirement) { 75 | for _, r := range requirements { 76 | if !r.Condition() { 77 | c.Skip(r.SkipMessage) 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /integration/restart_test.go: -------------------------------------------------------------------------------- 1 | package integration 2 | 3 | import ( 4 | "fmt" 5 | 6 | . "gopkg.in/check.v1" 7 | ) 8 | 9 | func (s *CliSuite) TestRestart(c *C) { 10 | p := s.ProjectFromText(c, "up", SimpleTemplate) 11 | 12 | name := fmt.Sprintf("%s_%s_1", p, "hello") 13 | cn := s.GetContainerByName(c, name) 14 | c.Assert(cn, NotNil) 15 | 16 | c.Assert(cn.State.Running, Equals, true) 17 | time := cn.State.StartedAt 18 | 19 | s.FromText(c, p, "restart", SimpleTemplate) 20 | 21 | cn = s.GetContainerByName(c, name) 22 | c.Assert(cn, NotNil) 23 | c.Assert(cn.State.Running, Equals, true) 24 | 25 | c.Assert(time, Not(Equals), cn.State.StartedAt) 26 | } 27 | -------------------------------------------------------------------------------- /integration/rm_test.go: -------------------------------------------------------------------------------- 1 | package integration 2 | 3 | import ( 4 | "fmt" 5 | 6 | . "gopkg.in/check.v1" 7 | ) 8 | 9 | func (s *CliSuite) TestDelete(c *C) { 10 | p := s.ProjectFromText(c, "up", SimpleTemplate) 11 | 12 | name := fmt.Sprintf("%s_%s_1", p, "hello") 13 | 14 | cn := s.GetContainerByName(c, name) 15 | c.Assert(cn, NotNil) 16 | c.Assert(cn.State.Running, Equals, true) 17 | 18 | s.FromText(c, p, "stop", SimpleTemplate) 19 | s.FromText(c, p, "rm", "--force", SimpleTemplate) 20 | 21 | cn = s.GetContainerByName(c, name) 22 | c.Assert(cn, IsNil) 23 | } 24 | 25 | func (s *CliSuite) TestDeleteOnlyRemovesStopped(c *C) { 26 | projectTemplate := ` 27 | hello: 28 | image: busybox 29 | stdin_open: true 30 | tty: true 31 | bye: 32 | image: busybox 33 | stdin_open: true 34 | tty: true 35 | ` 36 | 37 | p := s.ProjectFromText(c, "up", projectTemplate) 38 | 39 | helloName := fmt.Sprintf("%s_%s_1", p, "hello") 40 | byeName := fmt.Sprintf("%s_%s_1", p, "bye") 41 | 42 | helloContainer := s.GetContainerByName(c, helloName) 43 | c.Assert(helloContainer, NotNil) 44 | c.Assert(helloContainer.State.Running, Equals, true) 45 | 46 | byeContainer := s.GetContainerByName(c, byeName) 47 | c.Assert(byeContainer, NotNil) 48 | c.Assert(byeContainer.State.Running, Equals, true) 49 | 50 | s.FromText(c, p, "stop", "bye", projectTemplate) 51 | 52 | byeContainer = s.GetContainerByName(c, byeName) 53 | c.Assert(byeContainer, NotNil) 54 | c.Assert(byeContainer.State.Running, Equals, false) 55 | 56 | s.FromText(c, p, "rm", "--force", projectTemplate) 57 | 58 | byeContainer = s.GetContainerByName(c, byeName) 59 | c.Assert(byeContainer, IsNil) 60 | 61 | helloContainer = s.GetContainerByName(c, helloName) 62 | c.Assert(helloContainer, NotNil) 63 | } 64 | 65 | func (s *CliSuite) TestDeleteWithVol(c *C) { 66 | p := s.ProjectFromText(c, "up", SimpleTemplate) 67 | 68 | name := fmt.Sprintf("%s_%s_1", p, "hello") 69 | 70 | cn := s.GetContainerByName(c, name) 71 | c.Assert(cn, NotNil) 72 | c.Assert(cn.State.Running, Equals, true) 73 | 74 | s.FromText(c, p, "stop", SimpleTemplate) 75 | s.FromText(c, p, "rm", "--force", "-v", SimpleTemplate) 76 | 77 | cn = s.GetContainerByName(c, name) 78 | c.Assert(cn, IsNil) 79 | } 80 | -------------------------------------------------------------------------------- /integration/run_test.go: -------------------------------------------------------------------------------- 1 | package integration 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "fmt" 7 | "io" 8 | "os" 9 | "os/exec" 10 | "strings" 11 | "syscall" 12 | 13 | . "gopkg.in/check.v1" 14 | 15 | "github.com/kr/pty" 16 | ) 17 | 18 | // FIXME find out why it fails with "inappropriate ioctl for device" 19 | func (s *CliSuite) TestRun(c *C) { 20 | p := s.RandomProject() 21 | cmd := exec.Command(s.command, "-f", "./assets/run/docker-compose.yml", "-p", p, "run", "hello", "echo", "test") 22 | var b bytes.Buffer 23 | wbuf := bufio.NewWriter(&b) 24 | 25 | tty, err := pty.Start(cmd) 26 | 27 | _, err = io.Copy(wbuf, tty) 28 | if e, ok := err.(*os.PathError); ok && e.Err == syscall.EIO { 29 | // We can safely ignore this error, because it's just 30 | // the PTY telling us that it closed successfully. 31 | // See: 32 | // https://github.com/buildkite/agent/pull/34#issuecomment-46080419 33 | err = nil 34 | } 35 | c.Assert(cmd.Wait(), IsNil) 36 | output := string(b.Bytes()) 37 | 38 | c.Assert(err, IsNil, Commentf("%s", output)) 39 | 40 | name := fmt.Sprintf("%s_%s_run_1", p, "hello") 41 | cn := s.GetContainerByName(c, name) 42 | c.Assert(cn, NotNil) 43 | 44 | lines := strings.Split(output, "\r\n") 45 | lastLine := lines[len(lines)-2 : len(lines)-1][0] 46 | 47 | c.Assert(cn.State.Running, Equals, false) 48 | c.Assert(lastLine, Equals, "test") 49 | } 50 | -------------------------------------------------------------------------------- /integration/scale_test.go: -------------------------------------------------------------------------------- 1 | package integration 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | . "gopkg.in/check.v1" 8 | ) 9 | 10 | func (s *CliSuite) TestScale(c *C) { 11 | p := s.ProjectFromText(c, "up", SimpleTemplate) 12 | 13 | name := fmt.Sprintf("%s_%s_1", p, "hello") 14 | name2 := fmt.Sprintf("%s_%s_2", p, "hello") 15 | cn := s.GetContainerByName(c, name) 16 | c.Assert(cn, NotNil) 17 | 18 | c.Assert(cn.State.Running, Equals, true) 19 | 20 | containers := s.GetContainersByProject(c, p) 21 | c.Assert(1, Equals, len(containers)) 22 | 23 | s.FromText(c, p, "scale", "hello=2", SimpleTemplate) 24 | 25 | containers = s.GetContainersByProject(c, p) 26 | c.Assert(2, Equals, len(containers)) 27 | 28 | for _, name := range []string{name, name2} { 29 | cn := s.GetContainerByName(c, name) 30 | c.Assert(cn, NotNil) 31 | c.Assert(cn.State.Running, Equals, true) 32 | } 33 | 34 | s.FromText(c, p, "scale", "--timeout", "0", "hello=1", SimpleTemplate) 35 | containers = s.GetContainersByProject(c, p) 36 | c.Assert(1, Equals, len(containers)) 37 | 38 | cn = s.GetContainerByName(c, name) 39 | c.Assert(cn, IsNil) 40 | 41 | cn = s.GetContainerByName(c, name2) 42 | c.Assert(cn, NotNil) 43 | c.Assert(cn.State.Running, Equals, true) 44 | } 45 | 46 | func (s *CliSuite) TestScaleWithHostPortWarning(c *C) { 47 | template := ` 48 | test: 49 | image: busybox 50 | ports: 51 | - 8001:8001 52 | ` 53 | p := s.ProjectFromText(c, "up", template) 54 | 55 | name := fmt.Sprintf("%s_%s_1", p, "test") 56 | cn := s.GetContainerByName(c, name) 57 | c.Assert(cn, NotNil) 58 | 59 | c.Assert(cn.State.Running, Equals, true) 60 | 61 | containers := s.GetContainersByProject(c, p) 62 | c.Assert(1, Equals, len(containers)) 63 | 64 | _, output := s.FromTextCaptureOutput(c, p, "scale", "test=2", template) 65 | 66 | // Assert warning is given when trying to scale a service that specifies a host port 67 | c.Assert(strings.Contains(output, "If multiple containers for this service are created on a single host, the port will clash."), Equals, true, Commentf(output)) 68 | } 69 | -------------------------------------------------------------------------------- /integration/start_test.go: -------------------------------------------------------------------------------- 1 | package integration 2 | 3 | import ( 4 | "fmt" 5 | 6 | . "gopkg.in/check.v1" 7 | ) 8 | 9 | func (s *CliSuite) TestStart(c *C) { 10 | p := s.ProjectFromText(c, "create", SimpleTemplate) 11 | 12 | name := fmt.Sprintf("%s_%s_1", p, "hello") 13 | 14 | cn := s.GetContainerByName(c, name) 15 | c.Assert(cn, NotNil) 16 | c.Assert(cn.State.Running, Equals, false) 17 | 18 | s.FromText(c, p, "start", SimpleTemplate) 19 | 20 | cn = s.GetContainerByName(c, name) 21 | c.Assert(cn, NotNil) 22 | c.Assert(cn.State.Running, Equals, true) 23 | } 24 | -------------------------------------------------------------------------------- /integration/stop_test.go: -------------------------------------------------------------------------------- 1 | package integration 2 | 3 | import ( 4 | "fmt" 5 | 6 | . "gopkg.in/check.v1" 7 | ) 8 | 9 | func (s *CliSuite) TestStop(c *C) { 10 | p := s.ProjectFromText(c, "up", SimpleTemplate) 11 | 12 | name := fmt.Sprintf("%s_%s_1", p, "hello") 13 | 14 | cn := s.GetContainerByName(c, name) 15 | c.Assert(cn, NotNil) 16 | c.Assert(cn.State.Running, Equals, true) 17 | 18 | s.FromText(c, p, "stop", SimpleTemplate) 19 | 20 | cn = s.GetContainerByName(c, name) 21 | c.Assert(cn, NotNil) 22 | c.Assert(cn.State.Running, Equals, false) 23 | } 24 | -------------------------------------------------------------------------------- /integration/volume_test.go: -------------------------------------------------------------------------------- 1 | package integration 2 | 3 | import ( 4 | "fmt" 5 | "os/exec" 6 | "path/filepath" 7 | 8 | . "gopkg.in/check.v1" 9 | ) 10 | 11 | func (s *CliSuite) TestVolumeFromService(c *C) { 12 | p := s.RandomProject() 13 | cmd := exec.Command(s.command, "-f", "./assets/regression/60-volume_from.yml", "-p", p, "create") 14 | err := cmd.Run() 15 | c.Assert(err, IsNil) 16 | 17 | volumeFromContainer := fmt.Sprintf("%s_%s_1", p, "first") 18 | secondContainerName := p + "_second_1" 19 | 20 | cn := s.GetContainerByName(c, secondContainerName) 21 | c.Assert(cn, NotNil) 22 | 23 | c.Assert(len(cn.HostConfig.VolumesFrom), Equals, 1) 24 | c.Assert(cn.HostConfig.VolumesFrom[0], Equals, volumeFromContainer) 25 | } 26 | 27 | func (s *CliSuite) TestVolumeFromServiceWithContainerName(c *C) { 28 | p := s.RandomProject() 29 | cmd := exec.Command(s.command, "-f", "./assets/regression/volume_from_container_name.yml", "-p", p, "create") 30 | err := cmd.Run() 31 | c.Assert(err, IsNil) 32 | 33 | volumeFromContainer := "first_container_name" 34 | secondContainerName := p + "_second_1" 35 | 36 | cn := s.GetContainerByName(c, secondContainerName) 37 | c.Assert(cn, NotNil) 38 | 39 | c.Assert(len(cn.HostConfig.VolumesFrom), Equals, 1) 40 | c.Assert(cn.HostConfig.VolumesFrom[0], Equals, volumeFromContainer) 41 | } 42 | 43 | func (s *CliSuite) TestRelativeVolume(c *C) { 44 | p := s.ProjectFromText(c, "up", ` 45 | server: 46 | image: busybox 47 | volumes: 48 | - .:/path 49 | `) 50 | 51 | absPath, err := filepath.Abs(".") 52 | c.Assert(err, IsNil) 53 | serverName := fmt.Sprintf("%s_%s_1", p, "server") 54 | cn := s.GetContainerByName(c, serverName) 55 | 56 | c.Assert(cn, NotNil) 57 | c.Assert(len(cn.Mounts), DeepEquals, 1) 58 | c.Assert(cn.Mounts[0].Source, DeepEquals, absPath) 59 | c.Assert(cn.Mounts[0].Destination, DeepEquals, "/path") 60 | } 61 | 62 | func (s *CliSuite) TestNamedVolume(c *C) { 63 | p := s.ProjectFromText(c, "up", ` 64 | server: 65 | image: busybox 66 | volumes: 67 | - vol:/path 68 | `) 69 | 70 | serverName := fmt.Sprintf("%s_%s_1", p, "server") 71 | cn := s.GetContainerByName(c, serverName) 72 | 73 | c.Assert(cn, NotNil) 74 | c.Assert(len(cn.Mounts), DeepEquals, 1) 75 | c.Assert(cn.Mounts[0].Name, DeepEquals, "vol") 76 | c.Assert(cn.Mounts[0].Destination, DeepEquals, "/path") 77 | } 78 | 79 | func (s *CliSuite) TestV2Volume(c *C) { 80 | testRequires(c, not(DaemonVersionIs("1.9"))) 81 | p := s.ProjectFromText(c, "up", `version: "2" 82 | services: 83 | with_volume: 84 | image: busybox 85 | volumes: 86 | - test:/test 87 | 88 | volumes: 89 | test: {} 90 | test2: {} 91 | `) 92 | 93 | v := s.GetVolumeByName(c, p+"_test") 94 | c.Assert(v, NotNil) 95 | 96 | v = s.GetVolumeByName(c, p+"_test2") 97 | c.Assert(v, NotNil) 98 | } 99 | -------------------------------------------------------------------------------- /labels/labels.go: -------------------------------------------------------------------------------- 1 | package labels 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/docker/libcompose/utils" 8 | ) 9 | 10 | // Label represents a docker label. 11 | type Label string 12 | 13 | // Libcompose default labels. 14 | const ( 15 | NUMBER = Label("com.docker.compose.container-number") 16 | ONEOFF = Label("com.docker.compose.oneoff") 17 | PROJECT = Label("com.docker.compose.project") 18 | SERVICE = Label("com.docker.compose.service") 19 | HASH = Label("com.docker.compose.config-hash") 20 | VERSION = Label("com.docker.compose.version") 21 | ) 22 | 23 | // EqString returns a label json string representation with the specified value. 24 | func (f Label) EqString(value string) string { 25 | return LabelFilterString(string(f), value) 26 | } 27 | 28 | // Eq returns a label map representation with the specified value. 29 | func (f Label) Eq(value string) map[string][]string { 30 | return LabelFilter(string(f), value) 31 | } 32 | 33 | // AndString returns a json list of labels by merging the two specified values (left and right) serialized as string. 34 | func AndString(left, right string) string { 35 | leftMap := map[string][]string{} 36 | rightMap := map[string][]string{} 37 | 38 | // Ignore errors 39 | json.Unmarshal([]byte(left), &leftMap) 40 | json.Unmarshal([]byte(right), &rightMap) 41 | 42 | for k, v := range rightMap { 43 | existing, ok := leftMap[k] 44 | if ok { 45 | leftMap[k] = append(existing, v...) 46 | } else { 47 | leftMap[k] = v 48 | } 49 | } 50 | 51 | result, _ := json.Marshal(leftMap) 52 | 53 | return string(result) 54 | } 55 | 56 | // And returns a map of labels by merging the two specified values (left and right). 57 | func And(left, right map[string][]string) map[string][]string { 58 | result := map[string][]string{} 59 | for k, v := range left { 60 | result[k] = v 61 | } 62 | 63 | for k, v := range right { 64 | existing, ok := result[k] 65 | if ok { 66 | result[k] = append(existing, v...) 67 | } else { 68 | result[k] = v 69 | } 70 | } 71 | 72 | return result 73 | } 74 | 75 | // Str returns the label name. 76 | func (f Label) Str() string { 77 | return string(f) 78 | } 79 | 80 | // LabelFilterString returns a label json string representation of the specifed couple (key,value) 81 | // that is used as filter for docker. 82 | func LabelFilterString(key, value string) string { 83 | return utils.FilterString(map[string][]string{ 84 | "label": {fmt.Sprintf("%s=%s", key, value)}, 85 | }) 86 | } 87 | 88 | // LabelFilter returns a label map representation of the specifed couple (key,value) 89 | // that is used as filter for docker. 90 | func LabelFilter(key, value string) map[string][]string { 91 | return map[string][]string{ 92 | "label": {fmt.Sprintf("%s=%s", key, value)}, 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /labels/labels_test.go: -------------------------------------------------------------------------------- 1 | package labels 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestLabelEq(t *testing.T) { 8 | label := Label("labelName") 9 | m := label.Eq("value") 10 | values, ok := m["label"] 11 | if !ok { 12 | t.Fatalf("expected a label key, got %v", m) 13 | } 14 | if len(values) != 1 { 15 | t.Fatalf("expected only one value, got %v", values) 16 | } 17 | if values[0] != "labelName=value" { 18 | t.Fatalf("expected 'labelName=value', got %s", values) 19 | } 20 | } 21 | 22 | func TestLabelEqString(t *testing.T) { 23 | label := Label("labelName") 24 | value := label.EqString("value") 25 | if value != `{"label":["labelName=value"]}` { 26 | t.Fatalf("expected '{labelName=value}', got %s", value) 27 | } 28 | } 29 | 30 | func TestLabelFilter(t *testing.T) { 31 | filters := []struct { 32 | key string 33 | value string 34 | expected string 35 | }{ 36 | { 37 | "key", "value", `{"label":["key=value"]}`, 38 | }, { 39 | "key", "", `{"label":["key="]}`, 40 | }, { 41 | "", "", `{"label":["="]}`, 42 | }, 43 | } 44 | for _, filter := range filters { 45 | actual := LabelFilterString(filter.key, filter.value) 46 | if actual != filter.expected { 47 | t.Fatalf("Expected '%s for key=%s and value=%s, got %s", filter.expected, filter.key, filter.value, actual) 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /logger/null.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "io" 5 | ) 6 | 7 | // NullLogger is a logger.Logger and logger.Factory implementation that does nothing. 8 | type NullLogger struct { 9 | } 10 | 11 | // Out is a no-op function. 12 | func (n *NullLogger) Out(_ []byte) { 13 | } 14 | 15 | // Err is a no-op function. 16 | func (n *NullLogger) Err(_ []byte) { 17 | } 18 | 19 | // CreateContainerLogger allows NullLogger to implement logger.Factory. 20 | func (n *NullLogger) CreateContainerLogger(_ string) Logger { 21 | return &NullLogger{} 22 | } 23 | 24 | // CreateBuildLogger allows NullLogger to implement logger.Factory. 25 | func (n *NullLogger) CreateBuildLogger(_ string) Logger { 26 | return &NullLogger{} 27 | } 28 | 29 | // CreatePullLogger allows NullLogger to implement logger.Factory. 30 | func (n *NullLogger) CreatePullLogger(_ string) Logger { 31 | return &NullLogger{} 32 | } 33 | 34 | // OutWriter returns the base writer 35 | func (n *NullLogger) OutWriter() io.Writer { 36 | return nil 37 | } 38 | 39 | // ErrWriter returns the base writer 40 | func (n *NullLogger) ErrWriter() io.Writer { 41 | return nil 42 | } 43 | -------------------------------------------------------------------------------- /logger/raw_logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | ) 8 | 9 | // RawLogger is a logger.Logger and logger.Factory implementation that prints raw data with no formatting. 10 | type RawLogger struct { 11 | } 12 | 13 | // Out is a no-op function. 14 | func (r *RawLogger) Out(message []byte) { 15 | fmt.Print(string(message)) 16 | 17 | } 18 | 19 | // Err is a no-op function. 20 | func (r *RawLogger) Err(message []byte) { 21 | fmt.Fprint(os.Stderr, string(message)) 22 | 23 | } 24 | 25 | // CreateContainerLogger allows RawLogger to implement logger.Factory. 26 | func (r *RawLogger) CreateContainerLogger(_ string) Logger { 27 | return &RawLogger{} 28 | } 29 | 30 | // CreateBuildLogger allows RawLogger to implement logger.Factory. 31 | func (r *RawLogger) CreateBuildLogger(_ string) Logger { 32 | return &RawLogger{} 33 | } 34 | 35 | // CreatePullLogger allows RawLogger to implement logger.Factory. 36 | func (r *RawLogger) CreatePullLogger(_ string) Logger { 37 | return &RawLogger{} 38 | } 39 | 40 | // OutWriter returns the base writer 41 | func (r *RawLogger) OutWriter() io.Writer { 42 | return os.Stdout 43 | } 44 | 45 | // ErrWriter returns the base writer 46 | func (r *RawLogger) ErrWriter() io.Writer { 47 | return os.Stderr 48 | } 49 | -------------------------------------------------------------------------------- /logger/types.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "io" 5 | ) 6 | 7 | // Factory defines methods a factory should implement, to create a Logger 8 | // based on the specified container, image or service name. 9 | type Factory interface { 10 | CreateContainerLogger(name string) Logger 11 | CreateBuildLogger(name string) Logger 12 | CreatePullLogger(name string) Logger 13 | } 14 | 15 | // Logger defines methods to implement for being a logger. 16 | type Logger interface { 17 | Out(bytes []byte) 18 | Err(bytes []byte) 19 | OutWriter() io.Writer 20 | ErrWriter() io.Writer 21 | } 22 | 23 | // Wrapper is a wrapper around Logger that implements the Writer interface, 24 | // mainly use by docker/pkg/stdcopy functions. 25 | type Wrapper struct { 26 | Err bool 27 | Logger Logger 28 | } 29 | 30 | func (l *Wrapper) Write(bytes []byte) (int, error) { 31 | if l.Err { 32 | l.Logger.Err(bytes) 33 | } else { 34 | l.Logger.Out(bytes) 35 | } 36 | return len(bytes), nil 37 | } 38 | -------------------------------------------------------------------------------- /lookup/composable.go: -------------------------------------------------------------------------------- 1 | package lookup 2 | 3 | import ( 4 | "github.com/docker/libcompose/config" 5 | ) 6 | 7 | // ComposableEnvLookup is a structure that implements the project.EnvironmentLookup interface. 8 | // It holds an ordered list of EnvironmentLookup to call to look for the environment value. 9 | type ComposableEnvLookup struct { 10 | Lookups []config.EnvironmentLookup 11 | } 12 | 13 | // Lookup creates a string slice of string containing a "docker-friendly" environment string 14 | // in the form of 'key=value'. It loop through the lookups and returns the latest value if 15 | // more than one lookup return a result. 16 | func (l *ComposableEnvLookup) Lookup(key string, config *config.ServiceConfig) []string { 17 | result := []string{} 18 | for _, lookup := range l.Lookups { 19 | env := lookup.Lookup(key, config) 20 | if len(env) == 1 { 21 | result = env 22 | } 23 | } 24 | return result 25 | } 26 | -------------------------------------------------------------------------------- /lookup/composable_test.go: -------------------------------------------------------------------------------- 1 | package lookup 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/docker/libcompose/config" 7 | ) 8 | 9 | type simpleEnvLookup struct { 10 | value []string 11 | } 12 | 13 | func (l *simpleEnvLookup) Lookup(key string, config *config.ServiceConfig) []string { 14 | return l.value 15 | } 16 | 17 | func TestComposableLookupWithoutAnyLookup(t *testing.T) { 18 | envLookup := &ComposableEnvLookup{} 19 | actuals := envLookup.Lookup("any", nil) 20 | if len(actuals) != 0 { 21 | t.Fatalf("expected an empty slice, got %v", actuals) 22 | } 23 | } 24 | 25 | func TestComposableLookupReturnsTheLastValue(t *testing.T) { 26 | envLookup1 := &simpleEnvLookup{ 27 | value: []string{"value=1"}, 28 | } 29 | envLookup2 := &simpleEnvLookup{ 30 | value: []string{"value=2"}, 31 | } 32 | envLookup := &ComposableEnvLookup{ 33 | []config.EnvironmentLookup{ 34 | envLookup1, 35 | envLookup2, 36 | }, 37 | } 38 | validateLookup(t, "value=2", envLookup.Lookup("value", nil)) 39 | 40 | envLookup = &ComposableEnvLookup{ 41 | []config.EnvironmentLookup{ 42 | envLookup2, 43 | envLookup1, 44 | }, 45 | } 46 | validateLookup(t, "value=1", envLookup.Lookup("value", nil)) 47 | } 48 | -------------------------------------------------------------------------------- /lookup/envfile.go: -------------------------------------------------------------------------------- 1 | package lookup 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/docker/cli/opts" 7 | "github.com/docker/libcompose/config" 8 | ) 9 | 10 | // EnvfileLookup is a structure that implements the project.EnvironmentLookup interface. 11 | // It holds the path of the file where to lookup environment values. 12 | type EnvfileLookup struct { 13 | Path string 14 | } 15 | 16 | // Lookup creates a string slice of string containing a "docker-friendly" environment string 17 | // in the form of 'key=value'. It gets environment values using a '.env' file in the specified 18 | // path. 19 | func (l *EnvfileLookup) Lookup(key string, config *config.ServiceConfig) []string { 20 | envs, err := opts.ParseEnvFile(l.Path) 21 | if err != nil { 22 | return []string{} 23 | } 24 | for _, env := range envs { 25 | e := strings.Split(env, "=") 26 | if e[0] == key { 27 | return []string{env} 28 | } 29 | } 30 | return []string{} 31 | } 32 | -------------------------------------------------------------------------------- /lookup/envfile_test.go: -------------------------------------------------------------------------------- 1 | package lookup 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "path/filepath" 7 | "testing" 8 | ) 9 | 10 | func TestEnvfileLookupReturnsEmptyIfError(t *testing.T) { 11 | envfileLookup := &EnvfileLookup{ 12 | Path: "anything/file.env", 13 | } 14 | actuals := envfileLookup.Lookup("any", nil) 15 | if len(actuals) != 0 { 16 | t.Fatalf("expected an empty slice, got %v", actuals) 17 | } 18 | } 19 | 20 | func TestEnvfileLookupWithGoodFile(t *testing.T) { 21 | content := `foo=bar 22 | baz=quux 23 | # comment 24 | 25 | _foobar=foobaz 26 | with.dots=working 27 | and_underscore=working too 28 | ` 29 | tmpFolder, err := ioutil.TempDir("", "test-envfile") 30 | if err != nil { 31 | t.Fatal(err) 32 | } 33 | envfile := filepath.Join(tmpFolder, ".env") 34 | if err := ioutil.WriteFile(envfile, []byte(content), 0700); err != nil { 35 | t.Fatal(err) 36 | } 37 | defer os.RemoveAll(tmpFolder) 38 | 39 | envfileLookup := &EnvfileLookup{ 40 | Path: envfile, 41 | } 42 | 43 | validateLookup(t, "baz=quux", envfileLookup.Lookup("baz", nil)) 44 | validateLookup(t, "foo=bar", envfileLookup.Lookup("foo", nil)) 45 | validateLookup(t, "_foobar=foobaz", envfileLookup.Lookup("_foobar", nil)) 46 | validateLookup(t, "with.dots=working", envfileLookup.Lookup("with.dots", nil)) 47 | validateLookup(t, "and_underscore=working too", envfileLookup.Lookup("and_underscore", nil)) 48 | } 49 | 50 | func validateLookup(t *testing.T, expected string, actuals []string) { 51 | if len(actuals) != 1 { 52 | t.Fatalf("expected 1 result, got %v", actuals) 53 | } 54 | if actuals[0] != expected { 55 | t.Fatalf("expected %s, got %s", expected, actuals[0]) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /lookup/file.go: -------------------------------------------------------------------------------- 1 | package lookup 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "path" 7 | "path/filepath" 8 | "strings" 9 | 10 | "github.com/sirupsen/logrus" 11 | ) 12 | 13 | // relativePath returns the proper relative path for the given file path. If 14 | // the relativeTo string equals "-", then it means that it's from the stdin, 15 | // and the returned path will be the current working directory. Otherwise, if 16 | // file is really an absolute path, then it will be returned without any 17 | // changes. Otherwise, the returned path will be a combination of relativeTo 18 | // and file. 19 | func relativePath(file, relativeTo string) string { 20 | // stdin: return the current working directory if possible. 21 | if relativeTo == "-" { 22 | if cwd, err := os.Getwd(); err == nil { 23 | return filepath.Join(cwd, file) 24 | } 25 | } 26 | 27 | // If the given file is already an absolute path, just return it. 28 | // Otherwise, the returned path will be relative to the given relativeTo 29 | // path. 30 | if filepath.IsAbs(file) { 31 | return file 32 | } 33 | 34 | abs, err := filepath.Abs(filepath.Join(path.Dir(relativeTo), file)) 35 | if err != nil { 36 | logrus.Errorf("Failed to get absolute directory: %s", err) 37 | return file 38 | } 39 | return abs 40 | } 41 | 42 | // FileResourceLookup is a "bare" structure that implements the project.ResourceLookup interface 43 | type FileResourceLookup struct { 44 | } 45 | 46 | // Lookup returns the content and the actual filename of the file that is "built" using the 47 | // specified file and relativeTo string. file and relativeTo are supposed to be file path. 48 | // If file starts with a slash ('/'), it tries to load it, otherwise it will build a 49 | // filename using the folder part of relativeTo joined with file. 50 | func (f *FileResourceLookup) Lookup(file, relativeTo string) ([]byte, string, error) { 51 | file = relativePath(file, relativeTo) 52 | logrus.Debugf("Reading file %s", file) 53 | bytes, err := ioutil.ReadFile(file) 54 | return bytes, file, err 55 | } 56 | 57 | // ResolvePath returns the path to be used for the given path volume. This 58 | // function already takes care of relative paths. 59 | func (f *FileResourceLookup) ResolvePath(path, relativeTo string) string { 60 | vs := strings.SplitN(path, ":", 2) 61 | if len(vs) != 2 || filepath.IsAbs(vs[0]) { 62 | return path 63 | } 64 | vs[0] = relativePath(vs[0], relativeTo) 65 | return strings.Join(vs, ":") 66 | } 67 | -------------------------------------------------------------------------------- /lookup/simple_env.go: -------------------------------------------------------------------------------- 1 | package lookup 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/docker/libcompose/config" 8 | ) 9 | 10 | // OsEnvLookup is a "bare" structure that implements the project.EnvironmentLookup interface 11 | type OsEnvLookup struct { 12 | } 13 | 14 | // Lookup creates a string slice of string containing a "docker-friendly" environment string 15 | // in the form of 'key=value'. It gets environment values using os.Getenv. 16 | // If the os environment variable does not exists, the slice is empty. serviceName and config 17 | // are not used at all in this implementation. 18 | func (o *OsEnvLookup) Lookup(key string, config *config.ServiceConfig) []string { 19 | ret := os.Getenv(key) 20 | if ret == "" { 21 | return []string{} 22 | } 23 | return []string{fmt.Sprintf("%s=%s", key, ret)} 24 | } 25 | -------------------------------------------------------------------------------- /lookup/simple_env_test.go: -------------------------------------------------------------------------------- 1 | package lookup 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestOsEnvLookup(t *testing.T) { 8 | osEnvLookup := &OsEnvLookup{} 9 | 10 | envs := osEnvLookup.Lookup("PATH", nil) 11 | if len(envs) != 1 { 12 | t.Fatalf("Expected envs to contains one element, but was %v", envs) 13 | } 14 | 15 | envs = osEnvLookup.Lookup("path", nil) 16 | if len(envs) != 0 { 17 | t.Fatalf("Expected envs to be empty, but was %v", envs) 18 | } 19 | 20 | envs = osEnvLookup.Lookup("DOES_NOT_EXIST", nil) 21 | if len(envs) != 0 { 22 | t.Fatalf("Expected envs to be empty, but was %v", envs) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /package.go: -------------------------------------------------------------------------------- 1 | package libcompose 2 | -------------------------------------------------------------------------------- /project/container.go: -------------------------------------------------------------------------------- 1 | package project 2 | 3 | import ( 4 | "golang.org/x/net/context" 5 | ) 6 | 7 | // Container defines what a libcompose container provides. 8 | type Container interface { 9 | ID() string 10 | Name() string 11 | Port(ctx context.Context, port string) (string, error) 12 | IsRunning(ctx context.Context) bool 13 | } 14 | -------------------------------------------------------------------------------- /project/context.go: -------------------------------------------------------------------------------- 1 | package project 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "path" 8 | "path/filepath" 9 | "regexp" 10 | "strings" 11 | 12 | "github.com/docker/libcompose/config" 13 | "github.com/docker/libcompose/logger" 14 | "github.com/sirupsen/logrus" 15 | ) 16 | 17 | var projectRegexp = regexp.MustCompile("[^a-zA-Z0-9_.-]") 18 | 19 | // Context holds context meta information about a libcompose project, like 20 | // the project name, the compose file, etc. 21 | type Context struct { 22 | ComposeFiles []string 23 | ComposeBytes [][]byte 24 | ProjectName string 25 | isOpen bool 26 | ServiceFactory ServiceFactory 27 | NetworksFactory NetworksFactory 28 | VolumesFactory VolumesFactory 29 | EnvironmentLookup config.EnvironmentLookup 30 | ResourceLookup config.ResourceLookup 31 | LoggerFactory logger.Factory 32 | IgnoreMissingConfig bool 33 | Project *Project 34 | } 35 | 36 | func (c *Context) readComposeFiles() error { 37 | if c.ComposeBytes != nil { 38 | return nil 39 | } 40 | 41 | logrus.Debugf("Opening compose files: %s", strings.Join(c.ComposeFiles, ",")) 42 | 43 | // Handle STDIN (`-f -`) 44 | if len(c.ComposeFiles) == 1 && c.ComposeFiles[0] == "-" { 45 | composeBytes, err := ioutil.ReadAll(os.Stdin) 46 | if err != nil { 47 | logrus.Errorf("Failed to read compose file from stdin: %v", err) 48 | return err 49 | } 50 | c.ComposeBytes = [][]byte{composeBytes} 51 | return nil 52 | } 53 | 54 | for _, composeFile := range c.ComposeFiles { 55 | composeBytes, err := ioutil.ReadFile(composeFile) 56 | if err != nil && !os.IsNotExist(err) { 57 | logrus.Errorf("Failed to open the compose file: %s", composeFile) 58 | return err 59 | } 60 | if err != nil && !c.IgnoreMissingConfig { 61 | logrus.Errorf("Failed to find the compose file: %s", composeFile) 62 | return err 63 | } 64 | c.ComposeBytes = append(c.ComposeBytes, composeBytes) 65 | } 66 | 67 | return nil 68 | } 69 | 70 | func (c *Context) determineProject() error { 71 | name, err := c.lookupProjectName() 72 | if err != nil { 73 | return err 74 | } 75 | 76 | c.ProjectName = normalizeName(name) 77 | 78 | if c.ProjectName == "" { 79 | return fmt.Errorf("Falied to determine project name") 80 | } 81 | 82 | return nil 83 | } 84 | 85 | func (c *Context) lookupProjectName() (string, error) { 86 | if c.ProjectName != "" { 87 | return c.ProjectName, nil 88 | } 89 | 90 | if envProject := os.Getenv("COMPOSE_PROJECT_NAME"); envProject != "" { 91 | return envProject, nil 92 | } 93 | 94 | file := "." 95 | if len(c.ComposeFiles) > 0 { 96 | file = c.ComposeFiles[0] 97 | } 98 | 99 | f, err := filepath.Abs(file) 100 | if err != nil { 101 | logrus.Errorf("Failed to get absolute directory for: %s", file) 102 | return "", err 103 | } 104 | 105 | f = toUnixPath(f) 106 | 107 | parent := path.Base(path.Dir(f)) 108 | if parent != "" && parent != "." { 109 | return parent, nil 110 | } else if wd, err := os.Getwd(); err != nil { 111 | return "", err 112 | } else { 113 | return path.Base(toUnixPath(wd)), nil 114 | } 115 | } 116 | 117 | func normalizeName(name string) string { 118 | r := regexp.MustCompile("[^a-z0-9]+") 119 | return r.ReplaceAllString(strings.ToLower(name), "") 120 | } 121 | 122 | func toUnixPath(p string) string { 123 | return strings.Replace(p, "\\", "/", -1) 124 | } 125 | 126 | func (c *Context) open() error { 127 | if c.isOpen { 128 | return nil 129 | } 130 | 131 | if err := c.readComposeFiles(); err != nil { 132 | return err 133 | } 134 | 135 | if err := c.determineProject(); err != nil { 136 | return err 137 | } 138 | 139 | c.isOpen = true 140 | return nil 141 | } 142 | -------------------------------------------------------------------------------- /project/events/events_test.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestEventEquality(t *testing.T) { 9 | if fmt.Sprintf("%s", ServiceStart) != "Started" || 10 | fmt.Sprintf("%v", ServiceStart) != "Started" { 11 | t.Fatalf("EventServiceStart String() doesn't work: %s %v", ServiceStart, ServiceStart) 12 | } 13 | 14 | if fmt.Sprintf("%s", ServiceStart) != fmt.Sprintf("%s", ServiceUp) { 15 | t.Fatal("Event messages do not match") 16 | } 17 | 18 | if ServiceStart == ServiceUp { 19 | t.Fatal("Events match") 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /project/info.go: -------------------------------------------------------------------------------- 1 | package project 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "text/tabwriter" 7 | ) 8 | 9 | // InfoSet holds a list of Info. 10 | type InfoSet []Info 11 | 12 | // Info holds a list of InfoPart. 13 | type Info map[string]string 14 | 15 | func (infos InfoSet) String(columns []string, titleFlag bool) string { 16 | //no error checking, none of this should fail 17 | buffer := bytes.NewBuffer(make([]byte, 0, 1024)) 18 | tabwriter := tabwriter.NewWriter(buffer, 4, 4, 2, ' ', 0) 19 | 20 | first := true 21 | for _, info := range infos { 22 | if first && titleFlag { 23 | writeLine(tabwriter, columns, true, info) 24 | } 25 | first = false 26 | writeLine(tabwriter, columns, false, info) 27 | } 28 | 29 | tabwriter.Flush() 30 | return buffer.String() 31 | } 32 | 33 | func writeLine(writer io.Writer, columns []string, key bool, info Info) { 34 | first := true 35 | for _, column := range columns { 36 | if !first { 37 | writer.Write([]byte{'\t'}) 38 | } 39 | first = false 40 | if key { 41 | writer.Write([]byte(column)) 42 | } else { 43 | writer.Write([]byte(info[column])) 44 | } 45 | } 46 | 47 | writer.Write([]byte{'\n'}) 48 | } 49 | -------------------------------------------------------------------------------- /project/interface.go: -------------------------------------------------------------------------------- 1 | package project 2 | 3 | import ( 4 | "golang.org/x/net/context" 5 | 6 | "github.com/docker/libcompose/config" 7 | "github.com/docker/libcompose/project/events" 8 | "github.com/docker/libcompose/project/options" 9 | ) 10 | 11 | // APIProject defines the methods a libcompose project should implement. 12 | type APIProject interface { 13 | events.Notifier 14 | events.Emitter 15 | 16 | Build(ctx context.Context, options options.Build, sevice ...string) error 17 | Config() (string, error) 18 | Create(ctx context.Context, options options.Create, services ...string) error 19 | Delete(ctx context.Context, options options.Delete, services ...string) error 20 | Down(ctx context.Context, options options.Down, services ...string) error 21 | Events(ctx context.Context, services ...string) (chan events.ContainerEvent, error) 22 | Kill(ctx context.Context, signal string, services ...string) error 23 | Log(ctx context.Context, follow bool, services ...string) error 24 | Pause(ctx context.Context, services ...string) error 25 | Ps(ctx context.Context, services ...string) (InfoSet, error) 26 | // FIXME(vdemeester) we could use nat.Port instead ? 27 | Port(ctx context.Context, index int, protocol, serviceName, privatePort string) (string, error) 28 | Pull(ctx context.Context, services ...string) error 29 | Restart(ctx context.Context, timeout int, services ...string) error 30 | Run(ctx context.Context, serviceName string, commandParts []string, options options.Run) (int, error) 31 | Scale(ctx context.Context, timeout int, servicesScale map[string]int) error 32 | Start(ctx context.Context, services ...string) error 33 | Stop(ctx context.Context, timeout int, services ...string) error 34 | Unpause(ctx context.Context, services ...string) error 35 | Up(ctx context.Context, options options.Up, services ...string) error 36 | 37 | Parse() error 38 | CreateService(name string) (Service, error) 39 | AddConfig(name string, config *config.ServiceConfig) error 40 | Load(bytes []byte) error 41 | Containers(ctx context.Context, filter Filter, services ...string) ([]string, error) 42 | 43 | GetServiceConfig(service string) (*config.ServiceConfig, bool) 44 | } 45 | 46 | // Filter holds filter element to filter containers 47 | type Filter struct { 48 | State State 49 | } 50 | 51 | // State defines the supported state you can filter on 52 | type State string 53 | 54 | // Definitions of filter states 55 | const ( 56 | AnyState = State("") 57 | Running = State("running") 58 | Stopped = State("stopped") 59 | ) 60 | 61 | // RuntimeProject defines runtime-specific methods for a libcompose implementation. 62 | type RuntimeProject interface { 63 | RemoveOrphans(ctx context.Context, projectName string, serviceConfigs *config.ServiceConfigs) error 64 | } 65 | -------------------------------------------------------------------------------- /project/listener.go: -------------------------------------------------------------------------------- 1 | package project 2 | 3 | import ( 4 | "bytes" 5 | 6 | "github.com/docker/libcompose/project/events" 7 | "github.com/sirupsen/logrus" 8 | ) 9 | 10 | var ( 11 | infoEvents = map[events.EventType]bool{ 12 | events.ServiceDeleteStart: true, 13 | events.ServiceDelete: true, 14 | events.ServiceDownStart: true, 15 | events.ServiceDown: true, 16 | events.ServiceStopStart: true, 17 | events.ServiceStop: true, 18 | events.ServiceKillStart: true, 19 | events.ServiceKill: true, 20 | events.ServiceCreateStart: true, 21 | events.ServiceCreate: true, 22 | events.ServiceStartStart: true, 23 | events.ServiceStart: true, 24 | events.ServiceRestartStart: true, 25 | events.ServiceRestart: true, 26 | events.ServiceUpStart: true, 27 | events.ServiceUp: true, 28 | events.ServicePauseStart: true, 29 | events.ServicePause: true, 30 | events.ServiceUnpauseStart: true, 31 | events.ServiceUnpause: true, 32 | } 33 | ) 34 | 35 | type defaultListener struct { 36 | project *Project 37 | listenChan chan events.Event 38 | upCount int 39 | } 40 | 41 | // NewDefaultListener create a default listener for the specified project. 42 | func NewDefaultListener(p *Project) chan<- events.Event { 43 | l := defaultListener{ 44 | listenChan: make(chan events.Event), 45 | project: p, 46 | } 47 | go l.start() 48 | return l.listenChan 49 | } 50 | 51 | func (d *defaultListener) start() { 52 | for event := range d.listenChan { 53 | buffer := bytes.NewBuffer(nil) 54 | if event.Data != nil { 55 | for k, v := range event.Data { 56 | if buffer.Len() > 0 { 57 | buffer.WriteString(", ") 58 | } 59 | buffer.WriteString(k) 60 | buffer.WriteString("=") 61 | buffer.WriteString(v) 62 | } 63 | } 64 | 65 | if event.EventType == events.ServiceUp { 66 | d.upCount++ 67 | } 68 | 69 | logf := logrus.Debugf 70 | 71 | if infoEvents[event.EventType] { 72 | logf = logrus.Infof 73 | } 74 | 75 | if event.ServiceName == "" { 76 | logf("Project [%s]: %s %s", d.project.Name, event.EventType, buffer.Bytes()) 77 | } else { 78 | logf("[%d/%d] [%s]: %s %s", d.upCount, d.project.ServiceConfigs.Len(), event.ServiceName, event.EventType, buffer.Bytes()) 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /project/network.go: -------------------------------------------------------------------------------- 1 | package project 2 | 3 | import ( 4 | "golang.org/x/net/context" 5 | 6 | "github.com/docker/libcompose/config" 7 | ) 8 | 9 | // Networks defines the methods a libcompose network aggregate should define. 10 | type Networks interface { 11 | Initialize(ctx context.Context) error 12 | Remove(ctx context.Context) error 13 | } 14 | 15 | // NetworksFactory is an interface factory to create Networks object for the specified 16 | // configurations (service, networks, …) 17 | type NetworksFactory interface { 18 | Create(projectName string, networkConfigs map[string]*config.NetworkConfig, serviceConfigs *config.ServiceConfigs, networkEnabled bool) (Networks, error) 19 | } 20 | -------------------------------------------------------------------------------- /project/options/types.go: -------------------------------------------------------------------------------- 1 | package options 2 | 3 | // Build holds options of compose build. 4 | type Build struct { 5 | NoCache bool 6 | ForceRemove bool 7 | Pull bool 8 | } 9 | 10 | // Delete holds options of compose rm. 11 | type Delete struct { 12 | RemoveVolume bool 13 | RemoveRunning bool 14 | } 15 | 16 | // Down holds options of compose down. 17 | type Down struct { 18 | RemoveVolume bool 19 | RemoveImages ImageType 20 | RemoveOrphans bool 21 | } 22 | 23 | // Create holds options of compose create. 24 | type Create struct { 25 | NoRecreate bool 26 | ForceRecreate bool 27 | NoBuild bool 28 | ForceBuild bool 29 | } 30 | 31 | // Run holds options of compose run. 32 | type Run struct { 33 | Detached bool 34 | DisableTty bool 35 | } 36 | 37 | // Up holds options of compose up. 38 | type Up struct { 39 | Create 40 | } 41 | 42 | // ImageType defines the type of image (local, all) 43 | type ImageType string 44 | 45 | // Valid indicates whether the image type is valid. 46 | func (i ImageType) Valid() bool { 47 | switch string(i) { 48 | case "", "local", "all": 49 | return true 50 | default: 51 | return false 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /project/options/types_test.go: -------------------------------------------------------------------------------- 1 | package options 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestImageType(t *testing.T) { 8 | cases := []struct { 9 | imageType string 10 | valid bool 11 | }{ 12 | { 13 | imageType: "", 14 | valid: true, 15 | }, 16 | { 17 | imageType: " ", 18 | valid: false, 19 | }, 20 | { 21 | imageType: "hello", 22 | valid: false, 23 | }, 24 | { 25 | imageType: "local", 26 | valid: true, 27 | }, 28 | { 29 | imageType: "all", 30 | valid: true, 31 | }, 32 | } 33 | for _, c := range cases { 34 | i := ImageType(c.imageType) 35 | if i.Valid() != c.valid { 36 | t.Errorf("expected %v, got %v, for %v", c.valid, i.Valid(), c) 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /project/project_build.go: -------------------------------------------------------------------------------- 1 | package project 2 | 3 | import ( 4 | "golang.org/x/net/context" 5 | 6 | "github.com/docker/libcompose/project/events" 7 | "github.com/docker/libcompose/project/options" 8 | ) 9 | 10 | // Build builds the specified services (like docker build). 11 | func (p *Project) Build(ctx context.Context, buildOptions options.Build, services ...string) error { 12 | return p.perform(events.ProjectBuildStart, events.ProjectBuildDone, services, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) { 13 | wrapper.Do(wrappers, events.ServiceBuildStart, events.ServiceBuild, func(service Service) error { 14 | return service.Build(ctx, buildOptions) 15 | }) 16 | }), nil) 17 | } 18 | -------------------------------------------------------------------------------- /project/project_config.go: -------------------------------------------------------------------------------- 1 | package project 2 | 3 | import ( 4 | "github.com/docker/libcompose/config" 5 | "gopkg.in/yaml.v2" 6 | ) 7 | 8 | // ExportedConfig holds config attribute that will be exported 9 | type ExportedConfig struct { 10 | Version string `yaml:"version,omitempty"` 11 | Services map[string]*config.ServiceConfig `yaml:"services"` 12 | Volumes map[string]*config.VolumeConfig `yaml:"volumes"` 13 | Networks map[string]*config.NetworkConfig `yaml:"networks"` 14 | } 15 | 16 | // Config validates and print the compose file. 17 | func (p *Project) Config() (string, error) { 18 | cfg := ExportedConfig{ 19 | Version: "2.0", 20 | Services: p.ServiceConfigs.All(), 21 | Volumes: p.VolumeConfigs, 22 | Networks: p.NetworkConfigs, 23 | } 24 | 25 | bytes, err := yaml.Marshal(cfg) 26 | return string(bytes), err 27 | } 28 | -------------------------------------------------------------------------------- /project/project_containers.go: -------------------------------------------------------------------------------- 1 | package project 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | 7 | "golang.org/x/net/context" 8 | 9 | "github.com/docker/libcompose/project/events" 10 | ) 11 | 12 | // Containers lists the containers for the specified services. Can be filter using 13 | // the Filter struct. 14 | func (p *Project) Containers(ctx context.Context, filter Filter, services ...string) ([]string, error) { 15 | containers := []string{} 16 | var lock sync.Mutex 17 | 18 | err := p.forEach(services, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) { 19 | wrapper.Do(nil, events.NoEvent, events.NoEvent, func(service Service) error { 20 | serviceContainers, innerErr := service.Containers(ctx) 21 | if innerErr != nil { 22 | return innerErr 23 | } 24 | 25 | for _, container := range serviceContainers { 26 | running := container.IsRunning(ctx) 27 | switch filter.State { 28 | case Running: 29 | if !running { 30 | continue 31 | } 32 | case Stopped: 33 | if running { 34 | continue 35 | } 36 | case AnyState: 37 | // Don't do a thing 38 | default: 39 | // Invalid state filter 40 | return fmt.Errorf("Invalid container filter: %s", filter.State) 41 | } 42 | containerID := container.ID() 43 | lock.Lock() 44 | containers = append(containers, containerID) 45 | lock.Unlock() 46 | } 47 | return nil 48 | }) 49 | }), nil) 50 | if err != nil { 51 | return nil, err 52 | } 53 | return containers, nil 54 | } 55 | -------------------------------------------------------------------------------- /project/project_create.go: -------------------------------------------------------------------------------- 1 | package project 2 | 3 | import ( 4 | "fmt" 5 | 6 | "golang.org/x/net/context" 7 | 8 | "github.com/docker/libcompose/project/events" 9 | "github.com/docker/libcompose/project/options" 10 | ) 11 | 12 | // Create creates the specified services (like docker create). 13 | func (p *Project) Create(ctx context.Context, options options.Create, services ...string) error { 14 | if options.NoRecreate && options.ForceRecreate { 15 | return fmt.Errorf("no-recreate and force-recreate cannot be combined") 16 | } 17 | if err := p.initialize(ctx); err != nil { 18 | return err 19 | } 20 | return p.perform(events.ProjectCreateStart, events.ProjectCreateDone, services, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) { 21 | wrapper.Do(wrappers, events.ServiceCreateStart, events.ServiceCreate, func(service Service) error { 22 | return service.Create(ctx, options) 23 | }) 24 | }), nil) 25 | } 26 | -------------------------------------------------------------------------------- /project/project_delete.go: -------------------------------------------------------------------------------- 1 | package project 2 | 3 | import ( 4 | "golang.org/x/net/context" 5 | 6 | "github.com/docker/libcompose/project/events" 7 | "github.com/docker/libcompose/project/options" 8 | ) 9 | 10 | // Delete removes the specified services (like docker rm). 11 | func (p *Project) Delete(ctx context.Context, options options.Delete, services ...string) error { 12 | return p.perform(events.ProjectDeleteStart, events.ProjectDeleteDone, services, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) { 13 | wrapper.Do(nil, events.ServiceDeleteStart, events.ServiceDelete, func(service Service) error { 14 | return service.Delete(ctx, options) 15 | }) 16 | }), nil) 17 | } 18 | -------------------------------------------------------------------------------- /project/project_down.go: -------------------------------------------------------------------------------- 1 | package project 2 | 3 | import ( 4 | "fmt" 5 | 6 | "golang.org/x/net/context" 7 | 8 | "github.com/docker/libcompose/project/events" 9 | "github.com/docker/libcompose/project/options" 10 | ) 11 | 12 | // Down stops the specified services and clean related containers (like docker stop + docker rm). 13 | func (p *Project) Down(ctx context.Context, opts options.Down, services ...string) error { 14 | if !opts.RemoveImages.Valid() { 15 | return fmt.Errorf("--rmi flag must be local, all or empty") 16 | } 17 | if err := p.Stop(ctx, 10, services...); err != nil { 18 | return err 19 | } 20 | if opts.RemoveOrphans { 21 | if err := p.runtime.RemoveOrphans(ctx, p.Name, p.ServiceConfigs); err != nil { 22 | return err 23 | } 24 | } 25 | if err := p.Delete(ctx, options.Delete{ 26 | RemoveVolume: opts.RemoveVolume, 27 | }, services...); err != nil { 28 | return err 29 | } 30 | 31 | networks, err := p.context.NetworksFactory.Create(p.Name, p.NetworkConfigs, p.ServiceConfigs, p.isNetworkEnabled()) 32 | if err != nil { 33 | return err 34 | } 35 | if err := networks.Remove(ctx); err != nil { 36 | return err 37 | } 38 | 39 | if opts.RemoveVolume { 40 | volumes, err := p.context.VolumesFactory.Create(p.Name, p.VolumeConfigs, p.ServiceConfigs, p.isVolumeEnabled()) 41 | if err != nil { 42 | return err 43 | } 44 | if err := volumes.Remove(ctx); err != nil { 45 | return err 46 | } 47 | } 48 | 49 | return p.forEach([]string{}, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) { 50 | wrapper.Do(wrappers, events.NoEvent, events.NoEvent, func(service Service) error { 51 | return service.RemoveImage(ctx, opts.RemoveImages) 52 | }) 53 | }), func(service Service) error { 54 | return service.Create(ctx, options.Create{}) 55 | }) 56 | } 57 | -------------------------------------------------------------------------------- /project/project_events.go: -------------------------------------------------------------------------------- 1 | package project 2 | 3 | import ( 4 | "golang.org/x/net/context" 5 | 6 | "github.com/docker/libcompose/project/events" 7 | ) 8 | 9 | // Events listen for real time events from containers (of the project). 10 | func (p *Project) Events(ctx context.Context, services ...string) (chan events.ContainerEvent, error) { 11 | events := make(chan events.ContainerEvent) 12 | if len(services) == 0 { 13 | services = p.ServiceConfigs.Keys() 14 | } 15 | // FIXME(vdemeester) handle errors (chan) here 16 | for _, service := range services { 17 | s, err := p.CreateService(service) 18 | if err != nil { 19 | return nil, err 20 | } 21 | go s.Events(ctx, events) 22 | } 23 | return events, nil 24 | } 25 | -------------------------------------------------------------------------------- /project/project_kill.go: -------------------------------------------------------------------------------- 1 | package project 2 | 3 | import ( 4 | "golang.org/x/net/context" 5 | 6 | "github.com/docker/libcompose/project/events" 7 | ) 8 | 9 | // Kill kills the specified services (like docker kill). 10 | func (p *Project) Kill(ctx context.Context, signal string, services ...string) error { 11 | return p.perform(events.ProjectKillStart, events.ProjectKillDone, services, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) { 12 | wrapper.Do(nil, events.ServiceKillStart, events.ServiceKill, func(service Service) error { 13 | return service.Kill(ctx, signal) 14 | }) 15 | }), nil) 16 | } 17 | -------------------------------------------------------------------------------- /project/project_log.go: -------------------------------------------------------------------------------- 1 | package project 2 | 3 | import ( 4 | "golang.org/x/net/context" 5 | 6 | "github.com/docker/libcompose/project/events" 7 | ) 8 | 9 | // Log aggregates and prints out the logs for the specified services. 10 | func (p *Project) Log(ctx context.Context, follow bool, services ...string) error { 11 | return p.forEach(services, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) { 12 | wrapper.Do(nil, events.NoEvent, events.NoEvent, func(service Service) error { 13 | return service.Log(ctx, follow) 14 | }) 15 | }), nil) 16 | } 17 | -------------------------------------------------------------------------------- /project/project_pause.go: -------------------------------------------------------------------------------- 1 | package project 2 | 3 | import ( 4 | "golang.org/x/net/context" 5 | 6 | "github.com/docker/libcompose/project/events" 7 | ) 8 | 9 | // Pause pauses the specified services containers (like docker pause). 10 | func (p *Project) Pause(ctx context.Context, services ...string) error { 11 | return p.perform(events.ProjectPauseStart, events.ProjectPauseDone, services, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) { 12 | wrapper.Do(nil, events.ServicePauseStart, events.ServicePause, func(service Service) error { 13 | return service.Pause(ctx) 14 | }) 15 | }), nil) 16 | } 17 | -------------------------------------------------------------------------------- /project/project_port.go: -------------------------------------------------------------------------------- 1 | package project 2 | 3 | import ( 4 | "fmt" 5 | 6 | "golang.org/x/net/context" 7 | ) 8 | 9 | // Port returns the public port for a port binding of the specified service. 10 | func (p *Project) Port(ctx context.Context, index int, protocol, serviceName, privatePort string) (string, error) { 11 | service, err := p.CreateService(serviceName) 12 | if err != nil { 13 | return "", err 14 | } 15 | 16 | containers, err := service.Containers(ctx) 17 | if err != nil { 18 | return "", err 19 | } 20 | 21 | if index < 1 || index > len(containers) { 22 | return "", fmt.Errorf("Invalid index %d", index) 23 | } 24 | 25 | return containers[index-1].Port(ctx, fmt.Sprintf("%s/%s", privatePort, protocol)) 26 | } 27 | -------------------------------------------------------------------------------- /project/project_ps.go: -------------------------------------------------------------------------------- 1 | package project 2 | 3 | import "golang.org/x/net/context" 4 | 5 | // Ps list containers for the specified services. 6 | func (p *Project) Ps(ctx context.Context, services ...string) (InfoSet, error) { 7 | allInfo := InfoSet{} 8 | 9 | if len(services) == 0 { 10 | services = p.ServiceConfigs.Keys() 11 | } 12 | 13 | for _, name := range services { 14 | 15 | service, err := p.CreateService(name) 16 | if err != nil { 17 | return nil, err 18 | } 19 | 20 | info, err := service.Info(ctx) 21 | if err != nil { 22 | return nil, err 23 | } 24 | 25 | allInfo = append(allInfo, info...) 26 | } 27 | return allInfo, nil 28 | } 29 | -------------------------------------------------------------------------------- /project/project_pull.go: -------------------------------------------------------------------------------- 1 | package project 2 | 3 | import ( 4 | "golang.org/x/net/context" 5 | 6 | "github.com/docker/libcompose/project/events" 7 | ) 8 | 9 | // Pull pulls the specified services (like docker pull). 10 | func (p *Project) Pull(ctx context.Context, services ...string) error { 11 | return p.forEach(services, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) { 12 | wrapper.Do(nil, events.ServicePullStart, events.ServicePull, func(service Service) error { 13 | return service.Pull(ctx) 14 | }) 15 | }), nil) 16 | } 17 | -------------------------------------------------------------------------------- /project/project_restart.go: -------------------------------------------------------------------------------- 1 | package project 2 | 3 | import ( 4 | "golang.org/x/net/context" 5 | 6 | "github.com/docker/libcompose/project/events" 7 | ) 8 | 9 | // Restart restarts the specified services (like docker restart). 10 | func (p *Project) Restart(ctx context.Context, timeout int, services ...string) error { 11 | return p.perform(events.ProjectRestartStart, events.ProjectRestartDone, services, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) { 12 | wrapper.Do(wrappers, events.ServiceRestartStart, events.ServiceRestart, func(service Service) error { 13 | return service.Restart(ctx, timeout) 14 | }) 15 | }), nil) 16 | } 17 | -------------------------------------------------------------------------------- /project/project_run.go: -------------------------------------------------------------------------------- 1 | package project 2 | 3 | import ( 4 | "fmt" 5 | 6 | "golang.org/x/net/context" 7 | 8 | "github.com/docker/libcompose/project/events" 9 | "github.com/docker/libcompose/project/options" 10 | ) 11 | 12 | // Run executes a one off command (like `docker run image command`). 13 | func (p *Project) Run(ctx context.Context, serviceName string, commandParts []string, opts options.Run) (int, error) { 14 | if !p.ServiceConfigs.Has(serviceName) { 15 | return 1, fmt.Errorf("%s is not defined in the template", serviceName) 16 | } 17 | 18 | if err := p.initialize(ctx); err != nil { 19 | return 1, err 20 | } 21 | var exitCode int 22 | err := p.forEach([]string{}, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) { 23 | wrapper.Do(wrappers, events.ServiceRunStart, events.ServiceRun, func(service Service) error { 24 | if service.Name() == serviceName { 25 | code, err := service.Run(ctx, commandParts, opts) 26 | exitCode = code 27 | return err 28 | } 29 | return nil 30 | }) 31 | }), func(service Service) error { 32 | return service.Create(ctx, options.Create{}) 33 | }) 34 | return exitCode, err 35 | } 36 | -------------------------------------------------------------------------------- /project/project_scale.go: -------------------------------------------------------------------------------- 1 | package project 2 | 3 | import ( 4 | "fmt" 5 | 6 | "golang.org/x/net/context" 7 | 8 | log "github.com/sirupsen/logrus" 9 | ) 10 | 11 | // Scale scales the specified services. 12 | func (p *Project) Scale(ctx context.Context, timeout int, servicesScale map[string]int) error { 13 | // This code is a bit verbose but I wanted to parse everything up front 14 | order := make([]string, 0, 0) 15 | services := make(map[string]Service) 16 | 17 | for name := range servicesScale { 18 | if !p.ServiceConfigs.Has(name) { 19 | return fmt.Errorf("%s is not defined in the template", name) 20 | } 21 | 22 | service, err := p.CreateService(name) 23 | if err != nil { 24 | return fmt.Errorf("Failed to lookup service: %s: %v", service, err) 25 | } 26 | 27 | order = append(order, name) 28 | services[name] = service 29 | } 30 | 31 | for _, name := range order { 32 | scale := servicesScale[name] 33 | log.Infof("Setting scale %s=%d...", name, scale) 34 | err := services[name].Scale(ctx, scale, timeout) 35 | if err != nil { 36 | return fmt.Errorf("Failed to set the scale %s=%d: %v", name, scale, err) 37 | } 38 | } 39 | return nil 40 | } 41 | -------------------------------------------------------------------------------- /project/project_start.go: -------------------------------------------------------------------------------- 1 | package project 2 | 3 | import ( 4 | "golang.org/x/net/context" 5 | 6 | "github.com/docker/libcompose/project/events" 7 | ) 8 | 9 | // Start starts the specified services (like docker start). 10 | func (p *Project) Start(ctx context.Context, services ...string) error { 11 | return p.perform(events.ProjectStartStart, events.ProjectStartDone, services, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) { 12 | wrapper.Do(wrappers, events.ServiceStartStart, events.ServiceStart, func(service Service) error { 13 | return service.Start(ctx) 14 | }) 15 | }), nil) 16 | } 17 | -------------------------------------------------------------------------------- /project/project_stop.go: -------------------------------------------------------------------------------- 1 | package project 2 | 3 | import ( 4 | "golang.org/x/net/context" 5 | 6 | "github.com/docker/libcompose/project/events" 7 | ) 8 | 9 | // Stop stops the specified services (like docker stop). 10 | func (p *Project) Stop(ctx context.Context, timeout int, services ...string) error { 11 | return p.perform(events.ProjectStopStart, events.ProjectStopDone, services, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) { 12 | wrapper.Do(nil, events.ServiceStopStart, events.ServiceStop, func(service Service) error { 13 | return service.Stop(ctx, timeout) 14 | }) 15 | }), nil) 16 | } 17 | -------------------------------------------------------------------------------- /project/project_unpause.go: -------------------------------------------------------------------------------- 1 | package project 2 | 3 | import ( 4 | "golang.org/x/net/context" 5 | 6 | "github.com/docker/libcompose/project/events" 7 | ) 8 | 9 | // Unpause pauses the specified services containers (like docker pause). 10 | func (p *Project) Unpause(ctx context.Context, services ...string) error { 11 | return p.perform(events.ProjectUnpauseStart, events.ProjectUnpauseDone, services, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) { 12 | wrapper.Do(nil, events.ServiceUnpauseStart, events.ServiceUnpause, func(service Service) error { 13 | return service.Unpause(ctx) 14 | }) 15 | }), nil) 16 | } 17 | -------------------------------------------------------------------------------- /project/project_up.go: -------------------------------------------------------------------------------- 1 | package project 2 | 3 | import ( 4 | "golang.org/x/net/context" 5 | 6 | "github.com/docker/libcompose/project/events" 7 | "github.com/docker/libcompose/project/options" 8 | ) 9 | 10 | // Up creates and starts the specified services (kinda like docker run). 11 | func (p *Project) Up(ctx context.Context, options options.Up, services ...string) error { 12 | if err := p.initialize(ctx); err != nil { 13 | return err 14 | } 15 | return p.perform(events.ProjectUpStart, events.ProjectUpDone, services, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) { 16 | wrapper.Do(wrappers, events.ServiceUpStart, events.ServiceUp, func(service Service) error { 17 | return service.Up(ctx, options) 18 | }) 19 | }), func(service Service) error { 20 | return service.Create(ctx, options.Create) 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /project/service-wrapper.go: -------------------------------------------------------------------------------- 1 | package project 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/docker/libcompose/project/events" 7 | log "github.com/sirupsen/logrus" 8 | ) 9 | 10 | type serviceWrapper struct { 11 | name string 12 | service Service 13 | done sync.WaitGroup 14 | state ServiceState 15 | err error 16 | project *Project 17 | noWait bool 18 | ignored map[string]bool 19 | } 20 | 21 | func newServiceWrapper(name string, p *Project) (*serviceWrapper, error) { 22 | wrapper := &serviceWrapper{ 23 | name: name, 24 | state: StateUnknown, 25 | project: p, 26 | ignored: map[string]bool{}, 27 | } 28 | 29 | return wrapper, wrapper.Reset() 30 | } 31 | 32 | func (s *serviceWrapper) IgnoreDep(name string) { 33 | s.ignored[name] = true 34 | } 35 | 36 | func (s *serviceWrapper) Reset() error { 37 | if s.state != StateExecuted { 38 | service, err := s.project.CreateService(s.name) 39 | if err != nil { 40 | log.Errorf("Failed to create service for %s : %v", s.name, err) 41 | return err 42 | } 43 | 44 | s.service = service 45 | } 46 | 47 | if s.err == ErrRestart { 48 | s.err = nil 49 | } 50 | s.done.Add(1) 51 | 52 | return nil 53 | } 54 | 55 | func (s *serviceWrapper) Ignore() { 56 | defer s.done.Done() 57 | 58 | s.state = StateExecuted 59 | s.project.Notify(events.ServiceUpIgnored, s.service.Name(), nil) 60 | } 61 | 62 | func (s *serviceWrapper) waitForDeps(wrappers map[string]*serviceWrapper) bool { 63 | if s.noWait { 64 | return true 65 | } 66 | 67 | for _, dep := range s.service.DependentServices() { 68 | if s.ignored[dep.Target] { 69 | continue 70 | } 71 | 72 | if wrapper, ok := wrappers[dep.Target]; ok { 73 | if wrapper.Wait() == ErrRestart { 74 | s.project.Notify(events.ProjectReload, wrapper.service.Name(), nil) 75 | s.err = ErrRestart 76 | return false 77 | } 78 | } else { 79 | log.Errorf("Failed to find %s", dep.Target) 80 | } 81 | } 82 | 83 | return true 84 | } 85 | 86 | func (s *serviceWrapper) Do(wrappers map[string]*serviceWrapper, start, done events.EventType, action func(service Service) error) { 87 | defer s.done.Done() 88 | 89 | if s.state == StateExecuted { 90 | return 91 | } 92 | 93 | if wrappers != nil && !s.waitForDeps(wrappers) { 94 | return 95 | } 96 | 97 | s.state = StateExecuted 98 | 99 | s.project.Notify(start, s.service.Name(), nil) 100 | 101 | s.err = action(s.service) 102 | if s.err == ErrRestart { 103 | s.project.Notify(done, s.service.Name(), nil) 104 | s.project.Notify(events.ProjectReloadTrigger, s.service.Name(), nil) 105 | } else if s.err != nil { 106 | log.Errorf("Failed %s %s : %v", start, s.name, s.err) 107 | } else { 108 | s.project.Notify(done, s.service.Name(), nil) 109 | } 110 | } 111 | 112 | func (s *serviceWrapper) Wait() error { 113 | s.done.Wait() 114 | return s.err 115 | } 116 | -------------------------------------------------------------------------------- /project/service.go: -------------------------------------------------------------------------------- 1 | package project 2 | 3 | import ( 4 | "errors" 5 | 6 | "golang.org/x/net/context" 7 | 8 | "github.com/docker/libcompose/config" 9 | "github.com/docker/libcompose/project/events" 10 | "github.com/docker/libcompose/project/options" 11 | ) 12 | 13 | // Service defines what a libcompose service provides. 14 | type Service interface { 15 | Build(ctx context.Context, buildOptions options.Build) error 16 | Create(ctx context.Context, options options.Create) error 17 | Delete(ctx context.Context, options options.Delete) error 18 | Events(ctx context.Context, messages chan events.ContainerEvent) error 19 | Info(ctx context.Context) (InfoSet, error) 20 | Log(ctx context.Context, follow bool) error 21 | Kill(ctx context.Context, signal string) error 22 | Pause(ctx context.Context) error 23 | Pull(ctx context.Context) error 24 | Restart(ctx context.Context, timeout int) error 25 | Run(ctx context.Context, commandParts []string, options options.Run) (int, error) 26 | Scale(ctx context.Context, count int, timeout int) error 27 | Start(ctx context.Context) error 28 | Stop(ctx context.Context, timeout int) error 29 | Unpause(ctx context.Context) error 30 | Up(ctx context.Context, options options.Up) error 31 | 32 | RemoveImage(ctx context.Context, imageType options.ImageType) error 33 | Containers(ctx context.Context) ([]Container, error) 34 | DependentServices() []ServiceRelationship 35 | Config() *config.ServiceConfig 36 | Name() string 37 | } 38 | 39 | // ServiceState holds the state of a service. 40 | type ServiceState string 41 | 42 | // State definitions 43 | var ( 44 | StateExecuted = ServiceState("executed") 45 | StateUnknown = ServiceState("unknown") 46 | ) 47 | 48 | // Error definitions 49 | var ( 50 | ErrRestart = errors.New("Restart execution") 51 | ErrUnsupported = errors.New("UnsupportedOperation") 52 | ) 53 | 54 | // ServiceFactory is an interface factory to create Service object for the specified 55 | // project, with the specified name and service configuration. 56 | type ServiceFactory interface { 57 | Create(project *Project, name string, serviceConfig *config.ServiceConfig) (Service, error) 58 | } 59 | 60 | // ServiceRelationshipType defines the type of service relationship. 61 | type ServiceRelationshipType string 62 | 63 | // RelTypeLink means the services are linked (docker links). 64 | const RelTypeLink = ServiceRelationshipType("") 65 | 66 | // RelTypeNetNamespace means the services share the same network namespace. 67 | const RelTypeNetNamespace = ServiceRelationshipType("netns") 68 | 69 | // RelTypeIpcNamespace means the service share the same ipc namespace. 70 | const RelTypeIpcNamespace = ServiceRelationshipType("ipc") 71 | 72 | // RelTypeVolumesFrom means the services share some volumes. 73 | const RelTypeVolumesFrom = ServiceRelationshipType("volumesFrom") 74 | 75 | // RelTypeDependsOn means the dependency was explicitly set using 'depends_on'. 76 | const RelTypeDependsOn = ServiceRelationshipType("dependsOn") 77 | 78 | // RelTypeNetworkMode means the services depends on another service on networkMode 79 | const RelTypeNetworkMode = ServiceRelationshipType("networkMode") 80 | 81 | // ServiceRelationship holds the relationship information between two services. 82 | type ServiceRelationship struct { 83 | Target, Alias string 84 | Type ServiceRelationshipType 85 | Optional bool 86 | } 87 | 88 | // NewServiceRelationship creates a new Relationship based on the specified alias 89 | // and relationship type. 90 | func NewServiceRelationship(nameAlias string, relType ServiceRelationshipType) ServiceRelationship { 91 | name, alias := NameAlias(nameAlias) 92 | return ServiceRelationship{ 93 | Target: name, 94 | Alias: alias, 95 | Type: relType, 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /project/utils.go: -------------------------------------------------------------------------------- 1 | package project 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | // DefaultDependentServices return the dependent services (as an array of ServiceRelationship) 8 | // for the specified project and service. It looks for : links, volumesFrom, net and ipc configuration. 9 | func DefaultDependentServices(p *Project, s Service) []ServiceRelationship { 10 | config := s.Config() 11 | if config == nil { 12 | return []ServiceRelationship{} 13 | } 14 | 15 | result := []ServiceRelationship{} 16 | for _, link := range config.Links { 17 | result = append(result, NewServiceRelationship(link, RelTypeLink)) 18 | } 19 | 20 | for _, volumesFrom := range config.VolumesFrom { 21 | result = append(result, NewServiceRelationship(volumesFrom, RelTypeVolumesFrom)) 22 | } 23 | 24 | for _, dependsOn := range config.DependsOn { 25 | result = append(result, NewServiceRelationship(dependsOn, RelTypeDependsOn)) 26 | } 27 | 28 | if config.NetworkMode != "" { 29 | if strings.HasPrefix(config.NetworkMode, "service:") { 30 | serviceName := config.NetworkMode[8:] 31 | result = append(result, NewServiceRelationship(serviceName, RelTypeNetworkMode)) 32 | } 33 | } 34 | 35 | return result 36 | } 37 | 38 | // NameAlias returns the name and alias based on the specified string. 39 | // If the name contains a colon (like name:alias) it will split it, otherwise 40 | // it will return the specified name as name and alias. 41 | func NameAlias(name string) (string, string) { 42 | parts := strings.SplitN(name, ":", 2) 43 | if len(parts) == 2 { 44 | return parts[0], parts[1] 45 | } 46 | return parts[0], parts[0] 47 | } 48 | -------------------------------------------------------------------------------- /project/volume.go: -------------------------------------------------------------------------------- 1 | package project 2 | 3 | import ( 4 | "golang.org/x/net/context" 5 | 6 | "github.com/docker/libcompose/config" 7 | ) 8 | 9 | // Volumes defines the methods a libcompose volume aggregate should define. 10 | type Volumes interface { 11 | Initialize(ctx context.Context) error 12 | Remove(ctx context.Context) error 13 | } 14 | 15 | // VolumesFactory is an interface factory to create Volumes object for the specified 16 | // configurations (service, volumes, …) 17 | type VolumesFactory interface { 18 | Create(projectName string, volumeConfigs map[string]*config.VolumeConfig, serviceConfigs *config.ServiceConfigs, volumeEnabled bool) (Volumes, error) 19 | } 20 | -------------------------------------------------------------------------------- /samples/compose.yml: -------------------------------------------------------------------------------- 1 | web: 2 | image: ubuntu 3 | links: 4 | - redis 5 | redis: 6 | image: redis -------------------------------------------------------------------------------- /utils/util_inparallel_test.go: -------------------------------------------------------------------------------- 1 | // +build !race 2 | 3 | package utils 4 | 5 | import ( 6 | "fmt" 7 | "sync" 8 | "testing" 9 | ) 10 | 11 | type safeMap struct { 12 | mu sync.RWMutex 13 | m map[int]bool 14 | } 15 | 16 | func (s *safeMap) Add(index int, ok bool) { 17 | s.mu.Lock() 18 | defer s.mu.Unlock() 19 | s.m[index] = ok 20 | } 21 | 22 | func (s *safeMap) Read() map[int]bool { 23 | s.mu.RLock() 24 | defer s.mu.RUnlock() 25 | return s.m 26 | } 27 | 28 | func TestInParallel(t *testing.T) { 29 | size := 5 30 | booleanMap := safeMap{ 31 | m: make(map[int]bool, size+1), 32 | } 33 | tasks := InParallel{} 34 | for i := 0; i < size; i++ { 35 | task := func(index int) func() error { 36 | return func() error { 37 | booleanMap.Add(index, true) 38 | return nil 39 | } 40 | }(i) 41 | tasks.Add(task) 42 | } 43 | err := tasks.Wait() 44 | if err != nil { 45 | t.Fatal(err) 46 | } 47 | // Make sure every value is true 48 | for _, value := range booleanMap.Read() { 49 | if !value { 50 | t.Fatalf("booleanMap expected to contain only true values, got at least one false") 51 | } 52 | } 53 | } 54 | 55 | func TestInParallelError(t *testing.T) { 56 | size := 5 57 | booleanMap := safeMap{ 58 | m: make(map[int]bool, size+1), 59 | } 60 | tasks := InParallel{} 61 | for i := 0; i < size; i++ { 62 | task := func(index int) func() error { 63 | return func() error { 64 | booleanMap.Add(index, false) 65 | t.Log("index", index) 66 | if index%2 == 0 { 67 | t.Log("return an error for", index) 68 | return fmt.Errorf("Error with %v", index) 69 | } 70 | booleanMap.Add(index, true) 71 | return nil 72 | } 73 | }(i) 74 | tasks.Add(task) 75 | } 76 | err := tasks.Wait() 77 | if err == nil { 78 | t.Fatalf("Expected an error on Wait, got nothing.") 79 | } 80 | for key, value := range booleanMap.Read() { 81 | if key%2 != 0 && !value { 82 | t.Fatalf("booleanMap expected to contain true values on odd number, got %v", booleanMap) 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | var ( 4 | // VERSION should be updated by hand at each release 5 | VERSION = "0.4.0" 6 | 7 | // GITCOMMIT will be overwritten automatically by the build system 8 | GITCOMMIT = "HEAD" 9 | 10 | // BUILDTIME will be overwritten automatically by the build system 11 | BUILDTIME = "" 12 | 13 | // SHOWWARNING might be overwritten by the build system to not show the warning 14 | SHOWWARNING = "true" 15 | ) 16 | 17 | // ShowWarning returns wether the warning should be printed or not 18 | func ShowWarning() bool { 19 | return SHOWWARNING != "false" 20 | } 21 | -------------------------------------------------------------------------------- /version/version_test.go: -------------------------------------------------------------------------------- 1 | package version 2 | -------------------------------------------------------------------------------- /yaml/build_test.go: -------------------------------------------------------------------------------- 1 | package yaml 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "gopkg.in/yaml.v2" 8 | ) 9 | 10 | var ( 11 | buildno = "1" 12 | user = "vincent" 13 | empty = "\x00" 14 | testCacheFrom = "someotherimage:latest" 15 | target = "intermediateimage" 16 | network = "buildnetwork" 17 | ) 18 | 19 | func TestMarshalBuild(t *testing.T) { 20 | builds := []struct { 21 | build Build 22 | expected string 23 | }{ 24 | { 25 | expected: `{} 26 | `, 27 | }, 28 | { 29 | build: Build{ 30 | Context: ".", 31 | }, 32 | expected: `context: . 33 | `, 34 | }, 35 | { 36 | build: Build{ 37 | Context: ".", 38 | Dockerfile: "alternate", 39 | }, 40 | expected: `context: . 41 | dockerfile: alternate 42 | `, 43 | }, 44 | { 45 | build: Build{ 46 | Context: ".", 47 | Dockerfile: "alternate", 48 | Args: map[string]*string{ 49 | "buildno": &buildno, 50 | "user": &user, 51 | }, 52 | CacheFrom: []*string{ 53 | &testCacheFrom, 54 | }, 55 | Labels: map[string]*string{ 56 | "buildno": &buildno, 57 | "user": &user, 58 | }, 59 | Target: target, 60 | Network: network, 61 | }, 62 | expected: `args: 63 | buildno: "1" 64 | user: vincent 65 | cache_from: 66 | - someotherimage:latest 67 | context: . 68 | dockerfile: alternate 69 | labels: 70 | buildno: "1" 71 | user: vincent 72 | network: buildnetwork 73 | target: intermediateimage 74 | `, 75 | }, 76 | } 77 | for _, build := range builds { 78 | bytes, err := yaml.Marshal(build.build) 79 | assert.Nil(t, err) 80 | assert.Equal(t, build.expected, string(bytes), "should be equal") 81 | } 82 | } 83 | 84 | func TestUnmarshalBuild(t *testing.T) { 85 | builds := []struct { 86 | yaml string 87 | expected *Build 88 | }{ 89 | { 90 | yaml: `.`, 91 | expected: &Build{ 92 | Context: ".", 93 | }, 94 | }, 95 | { 96 | yaml: `context: .`, 97 | expected: &Build{ 98 | Context: ".", 99 | }, 100 | }, 101 | { 102 | yaml: `context: . 103 | dockerfile: alternate`, 104 | expected: &Build{ 105 | Context: ".", 106 | Dockerfile: "alternate", 107 | }, 108 | }, 109 | { 110 | yaml: `context: . 111 | dockerfile: alternate 112 | args: 113 | buildno: 1 114 | user: vincent 115 | cache_from: 116 | - someotherimage:latest 117 | labels: 118 | buildno: "1" 119 | user: vincent 120 | target: intermediateimage 121 | network: buildnetwork 122 | `, 123 | expected: &Build{ 124 | Context: ".", 125 | Dockerfile: "alternate", 126 | Args: map[string]*string{ 127 | "buildno": &buildno, 128 | "user": &user, 129 | }, 130 | CacheFrom: []*string{ 131 | &testCacheFrom, 132 | }, 133 | Labels: map[string]*string{ 134 | "buildno": &buildno, 135 | "user": &user, 136 | }, 137 | Target: target, 138 | Network: network, 139 | }, 140 | }, 141 | { 142 | yaml: `context: . 143 | args: 144 | - buildno 145 | - user`, 146 | expected: &Build{ 147 | Context: ".", 148 | Args: map[string]*string{ 149 | "buildno": &empty, 150 | "user": &empty, 151 | }, 152 | }, 153 | }, 154 | { 155 | yaml: `context: . 156 | args: 157 | - buildno=1 158 | - user=vincent`, 159 | expected: &Build{ 160 | Context: ".", 161 | Args: map[string]*string{ 162 | "buildno": &buildno, 163 | "user": &user, 164 | }, 165 | }, 166 | }, 167 | } 168 | for _, build := range builds { 169 | actual := &Build{} 170 | err := yaml.Unmarshal([]byte(build.yaml), actual) 171 | assert.Nil(t, err) 172 | assert.Equal(t, build.expected, actual, "should be equal") 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /yaml/command.go: -------------------------------------------------------------------------------- 1 | package yaml 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/docker/docker/api/types/strslice" 8 | "github.com/flynn/go-shlex" 9 | ) 10 | 11 | // Command represents a docker command, can be a string or an array of strings. 12 | type Command strslice.StrSlice 13 | 14 | // UnmarshalYAML implements the Unmarshaller interface. 15 | func (s *Command) UnmarshalYAML(unmarshal func(interface{}) error) error { 16 | var stringType string 17 | if err := unmarshal(&stringType); err == nil { 18 | parts, err := shlex.Split(stringType) 19 | if err != nil { 20 | return err 21 | } 22 | *s = parts 23 | return nil 24 | } 25 | 26 | var sliceType []interface{} 27 | if err := unmarshal(&sliceType); err == nil { 28 | parts, err := toStrings(sliceType) 29 | if err != nil { 30 | return err 31 | } 32 | *s = parts 33 | return nil 34 | } 35 | 36 | var interfaceType interface{} 37 | if err := unmarshal(&interfaceType); err == nil { 38 | fmt.Println(interfaceType) 39 | } 40 | 41 | return errors.New("Failed to unmarshal Command") 42 | } 43 | -------------------------------------------------------------------------------- /yaml/command_test.go: -------------------------------------------------------------------------------- 1 | package yaml 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "gopkg.in/yaml.v2" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | type StructCommand struct { 13 | Entrypoint Command `yaml:"entrypoint,flow,omitempty"` 14 | Command Command `yaml:"command,flow,omitempty"` 15 | } 16 | 17 | var sampleStructCommand = `command: bash` 18 | 19 | func TestUnmarshalCommand(t *testing.T) { 20 | s := &StructCommand{} 21 | err := yaml.Unmarshal([]byte(sampleStructCommand), s) 22 | 23 | assert.Nil(t, err) 24 | assert.Equal(t, Command{"bash"}, s.Command) 25 | assert.Nil(t, s.Entrypoint) 26 | bytes, err := yaml.Marshal(s) 27 | assert.Nil(t, err) 28 | 29 | s2 := &StructCommand{} 30 | err = yaml.Unmarshal(bytes, s2) 31 | 32 | assert.Nil(t, err) 33 | assert.Equal(t, Command{"bash"}, s2.Command) 34 | assert.Nil(t, s2.Entrypoint) 35 | } 36 | 37 | var sampleEmptyCommand = `{}` 38 | 39 | func TestUnmarshalEmptyCommand(t *testing.T) { 40 | s := &StructCommand{} 41 | err := yaml.Unmarshal([]byte(sampleEmptyCommand), s) 42 | 43 | assert.Nil(t, err) 44 | assert.Nil(t, s.Command) 45 | 46 | bytes, err := yaml.Marshal(s) 47 | assert.Nil(t, err) 48 | assert.Equal(t, "{}", strings.TrimSpace(string(bytes))) 49 | 50 | s2 := &StructCommand{} 51 | err = yaml.Unmarshal(bytes, s2) 52 | 53 | assert.Nil(t, err) 54 | assert.Nil(t, s2.Command) 55 | } 56 | -------------------------------------------------------------------------------- /yaml/external.go: -------------------------------------------------------------------------------- 1 | package yaml 2 | 3 | // External represents an external network entry in compose file. 4 | // It can be a boolean (true|false) or have a name 5 | type External struct { 6 | External bool 7 | Name string 8 | } 9 | 10 | // MarshalYAML implements the Marshaller interface. 11 | func (n External) MarshalYAML() (interface{}, error) { 12 | if n.Name == "" { 13 | return n.External, nil 14 | } 15 | return map[string]interface{}{ 16 | "name": n.Name, 17 | }, nil 18 | } 19 | 20 | // UnmarshalYAML implements the Unmarshaller interface. 21 | func (n *External) UnmarshalYAML(unmarshal func(interface{}) error) error { 22 | if err := unmarshal(&n.External); err == nil { 23 | return nil 24 | } 25 | var dummyExternal struct { 26 | Name string 27 | } 28 | 29 | err := unmarshal(&dummyExternal) 30 | if err != nil { 31 | return err 32 | } 33 | n.Name = dummyExternal.Name 34 | n.External = true 35 | 36 | return nil 37 | } 38 | -------------------------------------------------------------------------------- /yaml/external_test.go: -------------------------------------------------------------------------------- 1 | package yaml 2 | 3 | import ( 4 | "testing" 5 | 6 | "gopkg.in/yaml.v2" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestMarshalExternal(t *testing.T) { 12 | externals := []struct { 13 | external External 14 | expected string 15 | }{ 16 | { 17 | external: External{}, 18 | expected: `false 19 | `, 20 | }, 21 | { 22 | external: External{ 23 | External: false, 24 | }, 25 | expected: `false 26 | `, 27 | }, 28 | { 29 | external: External{ 30 | External: true, 31 | }, 32 | expected: `true 33 | `, 34 | }, 35 | { 36 | external: External{ 37 | External: true, 38 | Name: "network-name", 39 | }, 40 | expected: `name: network-name 41 | `, 42 | }, 43 | } 44 | for _, e := range externals { 45 | bytes, err := yaml.Marshal(e.external) 46 | assert.Nil(t, err) 47 | assert.Equal(t, e.expected, string(bytes), "should be equal") 48 | } 49 | } 50 | 51 | func TestUnmarshalExternal(t *testing.T) { 52 | externals := []struct { 53 | yaml string 54 | expected *External 55 | }{ 56 | { 57 | yaml: `true`, 58 | expected: &External{ 59 | External: true, 60 | }, 61 | }, 62 | { 63 | yaml: `false`, 64 | expected: &External{ 65 | External: false, 66 | }, 67 | }, 68 | { 69 | yaml: ` 70 | name: name-of-network`, 71 | expected: &External{ 72 | External: true, 73 | Name: "name-of-network", 74 | }, 75 | }, 76 | } 77 | for _, e := range externals { 78 | actual := &External{} 79 | err := yaml.Unmarshal([]byte(e.yaml), actual) 80 | assert.Nil(t, err) 81 | assert.Equal(t, e.expected, actual, "should be equal") 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /yaml/network_test.go: -------------------------------------------------------------------------------- 1 | package yaml 2 | 3 | import ( 4 | "testing" 5 | 6 | "gopkg.in/yaml.v2" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestMarshalNetworks(t *testing.T) { 12 | networks := []struct { 13 | networks Networks 14 | expected string 15 | }{ 16 | { 17 | networks: Networks{}, 18 | expected: `{} 19 | `, 20 | }, 21 | { 22 | networks: Networks{ 23 | Networks: []*Network{ 24 | { 25 | Name: "network1", 26 | }, 27 | { 28 | Name: "network2", 29 | }, 30 | }, 31 | }, 32 | expected: `network1: {} 33 | network2: {} 34 | `, 35 | }, 36 | { 37 | networks: Networks{ 38 | Networks: []*Network{ 39 | { 40 | Name: "network1", 41 | Aliases: []string{"alias1", "alias2"}, 42 | }, 43 | { 44 | Name: "network2", 45 | }, 46 | }, 47 | }, 48 | expected: `network1: 49 | aliases: 50 | - alias1 51 | - alias2 52 | network2: {} 53 | `, 54 | }, 55 | { 56 | networks: Networks{ 57 | Networks: []*Network{ 58 | { 59 | Name: "network1", 60 | Aliases: []string{"alias1", "alias2"}, 61 | }, 62 | { 63 | Name: "network2", 64 | IPv4Address: "172.16.238.10", 65 | IPv6Address: "2001:3984:3989::10", 66 | }, 67 | }, 68 | }, 69 | expected: `network1: 70 | aliases: 71 | - alias1 72 | - alias2 73 | network2: 74 | ipv4_address: 172.16.238.10 75 | ipv6_address: 2001:3984:3989::10 76 | `, 77 | }, 78 | } 79 | for _, network := range networks { 80 | bytes, err := yaml.Marshal(network.networks) 81 | assert.Nil(t, err) 82 | assert.Equal(t, network.expected, string(bytes), "should be equal") 83 | } 84 | } 85 | 86 | func TestUnmarshalNetworks(t *testing.T) { 87 | networks := []struct { 88 | yaml string 89 | expected *Networks 90 | }{ 91 | { 92 | yaml: `- network1 93 | - network2`, 94 | expected: &Networks{ 95 | Networks: []*Network{ 96 | { 97 | Name: "network1", 98 | }, 99 | { 100 | Name: "network2", 101 | }, 102 | }, 103 | }, 104 | }, 105 | { 106 | yaml: `network1:`, 107 | expected: &Networks{ 108 | Networks: []*Network{ 109 | { 110 | Name: "network1", 111 | }, 112 | }, 113 | }, 114 | }, 115 | { 116 | yaml: `network1: {}`, 117 | expected: &Networks{ 118 | Networks: []*Network{ 119 | { 120 | Name: "network1", 121 | }, 122 | }, 123 | }, 124 | }, 125 | { 126 | yaml: `network1: 127 | aliases: 128 | - alias1 129 | - alias2`, 130 | expected: &Networks{ 131 | Networks: []*Network{ 132 | { 133 | Name: "network1", 134 | Aliases: []string{"alias1", "alias2"}, 135 | }, 136 | }, 137 | }, 138 | }, 139 | { 140 | yaml: `network1: 141 | aliases: 142 | - alias1 143 | - alias2 144 | ipv4_address: 172.16.238.10 145 | ipv6_address: 2001:3984:3989::10`, 146 | expected: &Networks{ 147 | Networks: []*Network{ 148 | { 149 | Name: "network1", 150 | Aliases: []string{"alias1", "alias2"}, 151 | IPv4Address: "172.16.238.10", 152 | IPv6Address: "2001:3984:3989::10", 153 | }, 154 | }, 155 | }, 156 | }, 157 | } 158 | for _, network := range networks { 159 | actual := &Networks{} 160 | err := yaml.Unmarshal([]byte(network.yaml), actual) 161 | assert.Nil(t, err) 162 | assert.Equal(t, network.expected, actual, "should be equal") 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /yaml/ulimit.go: -------------------------------------------------------------------------------- 1 | package yaml 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "sort" 7 | ) 8 | 9 | // Ulimits represents a list of Ulimit. 10 | // It is, however, represented in yaml as keys (and thus map in Go) 11 | type Ulimits struct { 12 | Elements []Ulimit 13 | } 14 | 15 | // MarshalYAML implements the Marshaller interface. 16 | func (u Ulimits) MarshalYAML() (interface{}, error) { 17 | ulimitMap := make(map[string]Ulimit) 18 | for _, ulimit := range u.Elements { 19 | ulimitMap[ulimit.Name] = ulimit 20 | } 21 | return ulimitMap, nil 22 | } 23 | 24 | // UnmarshalYAML implements the Unmarshaller interface. 25 | func (u *Ulimits) UnmarshalYAML(unmarshal func(interface{}) error) error { 26 | ulimits := make(map[string]Ulimit) 27 | 28 | var mapType map[interface{}]interface{} 29 | if err := unmarshal(&mapType); err == nil { 30 | for mapKey, mapValue := range mapType { 31 | name, ok := mapKey.(string) 32 | if !ok { 33 | return fmt.Errorf("Cannot unmarshal '%v' to type %T into a string value", name, name) 34 | } 35 | var soft, hard int64 36 | switch mv := mapValue.(type) { 37 | case int: 38 | soft = int64(mv) 39 | hard = int64(mv) 40 | case map[interface{}]interface{}: 41 | if len(mv) != 2 { 42 | return fmt.Errorf("Failed to unmarshal Ulimit: %#v", mapValue) 43 | } 44 | for mkey, mvalue := range mv { 45 | switch mkey { 46 | case "soft": 47 | soft = int64(mvalue.(int)) 48 | case "hard": 49 | hard = int64(mvalue.(int)) 50 | default: 51 | // FIXME(vdemeester) Should we ignore or fail ? 52 | continue 53 | } 54 | } 55 | default: 56 | return fmt.Errorf("Failed to unmarshal Ulimit: %v, %T", mapValue, mapValue) 57 | } 58 | ulimits[name] = Ulimit{ 59 | Name: name, 60 | ulimitValues: ulimitValues{ 61 | Soft: soft, 62 | Hard: hard, 63 | }, 64 | } 65 | } 66 | keys := make([]string, 0, len(ulimits)) 67 | for key := range ulimits { 68 | keys = append(keys, key) 69 | } 70 | sort.Strings(keys) 71 | for _, key := range keys { 72 | u.Elements = append(u.Elements, ulimits[key]) 73 | } 74 | return nil 75 | } 76 | 77 | return errors.New("Failed to unmarshal Ulimit") 78 | } 79 | 80 | // Ulimit represents ulimit information. 81 | type Ulimit struct { 82 | ulimitValues 83 | Name string 84 | } 85 | 86 | type ulimitValues struct { 87 | Soft int64 `yaml:"soft"` 88 | Hard int64 `yaml:"hard"` 89 | } 90 | 91 | // MarshalYAML implements the Marshaller interface. 92 | func (u Ulimit) MarshalYAML() (interface{}, error) { 93 | if u.Soft == u.Hard { 94 | return u.Soft, nil 95 | } 96 | return u.ulimitValues, nil 97 | } 98 | 99 | // NewUlimit creates a Ulimit based on the specified parts. 100 | func NewUlimit(name string, soft int64, hard int64) Ulimit { 101 | return Ulimit{ 102 | Name: name, 103 | ulimitValues: ulimitValues{ 104 | Soft: soft, 105 | Hard: hard, 106 | }, 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /yaml/ulimit_test.go: -------------------------------------------------------------------------------- 1 | package yaml 2 | 3 | import ( 4 | "testing" 5 | 6 | "gopkg.in/yaml.v2" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestMarshalUlimit(t *testing.T) { 12 | ulimits := []struct { 13 | ulimits *Ulimits 14 | expected string 15 | }{ 16 | { 17 | ulimits: &Ulimits{ 18 | Elements: []Ulimit{ 19 | { 20 | ulimitValues: ulimitValues{ 21 | Soft: 65535, 22 | Hard: 65535, 23 | }, 24 | Name: "nproc", 25 | }, 26 | }, 27 | }, 28 | expected: `nproc: 65535 29 | `, 30 | }, 31 | { 32 | ulimits: &Ulimits{ 33 | Elements: []Ulimit{ 34 | { 35 | Name: "nofile", 36 | ulimitValues: ulimitValues{ 37 | Soft: 20000, 38 | Hard: 40000, 39 | }, 40 | }, 41 | }, 42 | }, 43 | expected: `nofile: 44 | soft: 20000 45 | hard: 40000 46 | `, 47 | }, 48 | } 49 | 50 | for _, ulimit := range ulimits { 51 | 52 | bytes, err := yaml.Marshal(ulimit.ulimits) 53 | 54 | assert.Nil(t, err) 55 | assert.Equal(t, ulimit.expected, string(bytes), "should be equal") 56 | } 57 | } 58 | 59 | func TestUnmarshalUlimits(t *testing.T) { 60 | ulimits := []struct { 61 | yaml string 62 | expected *Ulimits 63 | }{ 64 | { 65 | yaml: "nproc: 65535", 66 | expected: &Ulimits{ 67 | Elements: []Ulimit{ 68 | { 69 | Name: "nproc", 70 | ulimitValues: ulimitValues{ 71 | Soft: 65535, 72 | Hard: 65535, 73 | }, 74 | }, 75 | }, 76 | }, 77 | }, 78 | { 79 | yaml: `nofile: 80 | soft: 20000 81 | hard: 40000`, 82 | expected: &Ulimits{ 83 | Elements: []Ulimit{ 84 | { 85 | Name: "nofile", 86 | ulimitValues: ulimitValues{ 87 | Soft: 20000, 88 | Hard: 40000, 89 | }, 90 | }, 91 | }, 92 | }, 93 | }, 94 | { 95 | yaml: `nproc: 65535 96 | nofile: 97 | soft: 20000 98 | hard: 40000`, 99 | expected: &Ulimits{ 100 | Elements: []Ulimit{ 101 | { 102 | Name: "nofile", 103 | ulimitValues: ulimitValues{ 104 | Soft: 20000, 105 | Hard: 40000, 106 | }, 107 | }, 108 | { 109 | Name: "nproc", 110 | ulimitValues: ulimitValues{ 111 | Soft: 65535, 112 | Hard: 65535, 113 | }, 114 | }, 115 | }, 116 | }, 117 | }, 118 | } 119 | 120 | for _, ulimit := range ulimits { 121 | actual := &Ulimits{} 122 | err := yaml.Unmarshal([]byte(ulimit.yaml), actual) 123 | 124 | assert.Nil(t, err) 125 | assert.Equal(t, ulimit.expected, actual, "should be equal") 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /yaml/volume.go: -------------------------------------------------------------------------------- 1 | package yaml 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "sort" 7 | "strings" 8 | ) 9 | 10 | // Volumes represents a list of service volumes in compose file. 11 | // It has several representation, hence this specific struct. 12 | type Volumes struct { 13 | Volumes []*Volume 14 | } 15 | 16 | // Volume represent a service volume 17 | type Volume struct { 18 | Source string `yaml:"-"` 19 | Destination string `yaml:"-"` 20 | AccessMode string `yaml:"-"` 21 | } 22 | 23 | // Generate a hash string to detect service volume config changes 24 | func (v *Volumes) HashString() string { 25 | if v == nil { 26 | return "" 27 | } 28 | result := []string{} 29 | for _, vol := range v.Volumes { 30 | result = append(result, vol.String()) 31 | } 32 | sort.Strings(result) 33 | return strings.Join(result, ",") 34 | } 35 | 36 | // String implements the Stringer interface. 37 | func (v *Volume) String() string { 38 | var paths []string 39 | if v.Source != "" { 40 | paths = []string{v.Source, v.Destination} 41 | } else { 42 | paths = []string{v.Destination} 43 | } 44 | if v.AccessMode != "" { 45 | paths = append(paths, v.AccessMode) 46 | } 47 | return strings.Join(paths, ":") 48 | } 49 | 50 | // MarshalYAML implements the Marshaller interface. 51 | func (v Volumes) MarshalYAML() (interface{}, error) { 52 | vs := []string{} 53 | for _, volume := range v.Volumes { 54 | vs = append(vs, volume.String()) 55 | } 56 | return vs, nil 57 | } 58 | 59 | // UnmarshalYAML implements the Unmarshaller interface. 60 | func (v *Volumes) UnmarshalYAML(unmarshal func(interface{}) error) error { 61 | var sliceType []interface{} 62 | if err := unmarshal(&sliceType); err == nil { 63 | v.Volumes = []*Volume{} 64 | for _, volume := range sliceType { 65 | name, ok := volume.(string) 66 | if !ok { 67 | return fmt.Errorf("Cannot unmarshal '%v' to type %T into a string value", name, name) 68 | } 69 | elts := strings.SplitN(name, ":", 3) 70 | var vol *Volume 71 | switch { 72 | case len(elts) == 1: 73 | vol = &Volume{ 74 | Destination: elts[0], 75 | } 76 | case len(elts) == 2: 77 | vol = &Volume{ 78 | Source: elts[0], 79 | Destination: elts[1], 80 | } 81 | case len(elts) == 3: 82 | vol = &Volume{ 83 | Source: elts[0], 84 | Destination: elts[1], 85 | AccessMode: elts[2], 86 | } 87 | default: 88 | // FIXME 89 | return fmt.Errorf("") 90 | } 91 | v.Volumes = append(v.Volumes, vol) 92 | } 93 | return nil 94 | } 95 | 96 | return errors.New("Failed to unmarshal Volumes") 97 | } 98 | -------------------------------------------------------------------------------- /yaml/volume_test.go: -------------------------------------------------------------------------------- 1 | package yaml 2 | 3 | import ( 4 | "testing" 5 | 6 | "gopkg.in/yaml.v2" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestMarshalVolumes(t *testing.T) { 12 | volumes := []struct { 13 | volumes Volumes 14 | expected string 15 | }{ 16 | { 17 | volumes: Volumes{}, 18 | expected: `[] 19 | `, 20 | }, 21 | { 22 | volumes: Volumes{ 23 | Volumes: []*Volume{ 24 | { 25 | Destination: "/in/the/container", 26 | }, 27 | }, 28 | }, 29 | expected: `- /in/the/container 30 | `, 31 | }, 32 | { 33 | volumes: Volumes{ 34 | Volumes: []*Volume{ 35 | { 36 | Source: "./a/path", 37 | Destination: "/in/the/container", 38 | AccessMode: "ro", 39 | }, 40 | }, 41 | }, 42 | expected: `- ./a/path:/in/the/container:ro 43 | `, 44 | }, 45 | { 46 | volumes: Volumes{ 47 | Volumes: []*Volume{ 48 | { 49 | Source: "./a/path", 50 | Destination: "/in/the/container", 51 | }, 52 | }, 53 | }, 54 | expected: `- ./a/path:/in/the/container 55 | `, 56 | }, 57 | { 58 | volumes: Volumes{ 59 | Volumes: []*Volume{ 60 | { 61 | Source: "./a/path", 62 | Destination: "/in/the/container", 63 | }, 64 | { 65 | Source: "named", 66 | Destination: "/in/the/container", 67 | }, 68 | }, 69 | }, 70 | expected: `- ./a/path:/in/the/container 71 | - named:/in/the/container 72 | `, 73 | }, 74 | } 75 | for _, volume := range volumes { 76 | bytes, err := yaml.Marshal(volume.volumes) 77 | assert.Nil(t, err) 78 | assert.Equal(t, volume.expected, string(bytes), "should be equal") 79 | } 80 | } 81 | 82 | func TestUnmarshalVolumes(t *testing.T) { 83 | volumes := []struct { 84 | yaml string 85 | expected *Volumes 86 | }{ 87 | { 88 | yaml: `- ./a/path:/in/the/container`, 89 | expected: &Volumes{ 90 | Volumes: []*Volume{ 91 | { 92 | Source: "./a/path", 93 | Destination: "/in/the/container", 94 | }, 95 | }, 96 | }, 97 | }, 98 | { 99 | yaml: `- /in/the/container`, 100 | expected: &Volumes{ 101 | Volumes: []*Volume{ 102 | { 103 | Destination: "/in/the/container", 104 | }, 105 | }, 106 | }, 107 | }, 108 | { 109 | yaml: `- /a/path:/in/the/container:ro`, 110 | expected: &Volumes{ 111 | Volumes: []*Volume{ 112 | { 113 | Source: "/a/path", 114 | Destination: "/in/the/container", 115 | AccessMode: "ro", 116 | }, 117 | }, 118 | }, 119 | }, 120 | { 121 | yaml: `- /a/path:/in/the/container 122 | - named:/somewhere/in/the/container`, 123 | expected: &Volumes{ 124 | Volumes: []*Volume{ 125 | { 126 | Source: "/a/path", 127 | Destination: "/in/the/container", 128 | }, 129 | { 130 | Source: "named", 131 | Destination: "/somewhere/in/the/container", 132 | }, 133 | }, 134 | }, 135 | }, 136 | } 137 | for _, volume := range volumes { 138 | actual := &Volumes{} 139 | err := yaml.Unmarshal([]byte(volume.yaml), actual) 140 | assert.Nil(t, err) 141 | assert.Equal(t, volume.expected, actual, "should be equal") 142 | } 143 | } 144 | --------------------------------------------------------------------------------