From b6135ac64d4391d55a1665ad75621153cad0547f Mon Sep 17 00:00:00 2001 From: Bouke Versteegh Date: Fri, 25 Mar 2022 23:04:03 +0100 Subject: [PATCH 01/13] doc: Remove obsolete instruction --- DEV.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/DEV.md b/DEV.md index d0987d5..aa101c2 100644 --- a/DEV.md +++ b/DEV.md @@ -7,12 +7,10 @@ For example, this is how `go` is added: ```yaml services: go: - <<: *default image: golang:latest entrypoint: [ "go" ] ``` -- `<<: *default` is a YAML reference that copies some default settings for the service. Just include it. - `image: golang:latest` specifies the docker image to use. You can find these on [Docker Hub](https://hub.docker.com/). - `entrypoint` is the command to run when the service starts. @@ -33,7 +31,6 @@ Replace the version tag `latest` with `${GO_VERSION}`: ```yaml go: - <<: *default image: "golang:${GO_VERSION}" entrypoint: [ "go" ] ``` @@ -60,7 +57,7 @@ dockerized gh auth login ``` - Choose SSH, and Browser authentication -- See [gh](apps/gh/Readme.md) for more information. +- See [gh](apps/gh/README.md) for more information. ```bash # Create a PR: From 862edb496dbf3915b9e164d948861ed9634aad67 Mon Sep 17 00:00:00 2001 From: Bouke Versteegh Date: Sun, 27 Mar 2022 11:54:30 +0200 Subject: [PATCH 02/13] fix: compile and run the correct binary when sharing source between windows and wsl --- bin/dockerized | 1 + 1 file changed, 1 insertion(+) diff --git a/bin/dockerized b/bin/dockerized index d3e63c9..d335de8 100755 --- a/bin/dockerized +++ b/bin/dockerized @@ -26,6 +26,7 @@ fi case "$OSTYPE" in msys | cygwin) DOCKERIZED_COMPILE_GOOS=windows + DOCKERIZED_BINARY="${DOCKERIZED_BINARY}.exe" ;; darwin*) DOCKERIZED_COMPILE_GOOS=darwin From c352c29ee61a0404587118d289d64609f3f70563 Mon Sep 17 00:00:00 2001 From: Bouke Versteegh Date: Sun, 27 Mar 2022 12:08:38 +0200 Subject: [PATCH 03/13] chore: use absolute build contexts to prepare for custom compose files --- docker-compose.yml | 33 +++++++++++++++++---------------- lib/dockerized.go | 7 ++++--- 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 78c0ef0..273617d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,7 +5,7 @@ services: &alpine image: "alpine_${ALPINE_VERSION}" build: - context: apps/alpine + context: "${DOCKERIZED_ROOT:-.}/apps/alpine" args: ALPINE_VERSION: "${ALPINE_VERSION}" ALPINE_PACKAGES: "tree" @@ -17,8 +17,9 @@ services: entrypoint: [ "ansible-playbook" ] ab: <<: *alpine + image: "ab" build: - context: apps/alpine + context: "${DOCKERIZED_ROOT:-.}/apps/alpine" args: ALPINE_VERSION: "${ALPINE_VERSION}" ALPINE_PACKAGES: "apache2-ssl apache2-utils ca-certificates" @@ -37,9 +38,9 @@ services: - "${HOME:-home}/.ssh:/root/.ssh" - "${HOME:-home}/.dockerized/apps/az:/root/.azure" bash: - image: "bash" + image: "dockerized_bash" build: - context: apps/alpine + context: "${DOCKERIZED_ROOT:-.}/apps/alpine" args: ALPINE_VERSION: "${ALPINE_VERSION}" ALPINE_PACKAGES: "bash" @@ -47,8 +48,8 @@ services: doctl: image: "doctl:${DOCTL_VERSION}" build: - context: apps/doctl - dockerfile: ../alpine/Dockerfile + context: "${DOCKERIZED_ROOT:-.}/apps/doctl" + dockerfile: "${DOCKERIZED_ROOT:-.}/apps/alpine/Dockerfile" args: ALPINE_VERSION: "${ALPINE_VERSION}" BUILD_SCRIPT_ARGS: "${DOCTL_VERSION}" @@ -61,8 +62,8 @@ services: gh: image: "gh:${GH_VERSION}" build: - context: apps/gh - dockerfile: ../alpine/Dockerfile + context: "${DOCKERIZED_ROOT:-.}/apps/gh" + dockerfile: "${DOCKERIZED_ROOT:-.}/apps/alpine/Dockerfile" args: ALPINE_VERSION: "${ALPINE_VERSION}" ALPINE_PACKAGES: "git openssh-client" @@ -70,7 +71,7 @@ services: entrypoint: [ "/init.sh", "/gh/bin/gh" ] volumes: - "${HOME:-home}/.dockerized/apps/gh:/root" - - "./apps/gh/init.sh:/init.sh" + - "${DOCKERIZED_ROOT:-.}/apps/gh/init.sh:/init.sh" environment: BROWSER: "echo" git: @@ -100,8 +101,8 @@ services: pdflatex: image: "pdflatex" build: - context: apps/alpine - dockerfile: ../alpine/Dockerfile + context: "${DOCKERIZED_ROOT:-.}/apps/alpine" + dockerfile: "${DOCKERIZED_ROOT:-.}/apps/alpine/Dockerfile" args: ALPINE_VERSION: "${LATEX_ALPINE_VERSION}" ALPINE_PACKAGES: "texlive-full py-pygments gnuplot make git" @@ -133,7 +134,7 @@ services: protoc: image: "protoc:${PROTOC_VERSION}" build: - context: apps/protoc + context: "${DOCKERIZED_ROOT:-.}/apps/protoc" args: PROTOC_VERSION: "${PROTOC_VERSION}" PROTOC_BASE: "${PROTOC_BASE}" @@ -164,7 +165,7 @@ services: entrypoint: [ "rustc" ] s3cmd: build: - context: apps/s3cmd + context: "${DOCKERIZED_ROOT:-.}/apps/s3cmd" args: S3CMD_VERSION: "${S3CMD_VERSION}" S3CMD_BASE: "${S3CMD_BASE}" @@ -187,7 +188,7 @@ services: telnet: <<: *alpine build: - context: apps/alpine + context: "${DOCKERIZED_ROOT:-.}/apps/alpine" args: ALPINE_VERSION: "${ALPINE_VERSION}" ALPINE_PACKAGES: "busybox-extras" @@ -195,7 +196,7 @@ services: tree: <<: *alpine build: - context: apps/alpine + context: "${DOCKERIZED_ROOT:-.}/apps/alpine" args: ALPINE_VERSION: "${ALPINE_VERSION}" ALPINE_PACKAGES: "tree" @@ -217,7 +218,7 @@ services: zip: image: "zip" build: - context: apps/alpine + context: "${DOCKERIZED_ROOT:-.}/apps/alpine" args: ALPINE_VERSION: "${ALPINE_VERSION}" ALPINE_PACKAGES: "zip" diff --git a/lib/dockerized.go b/lib/dockerized.go index d965e57..1368880 100644 --- a/lib/dockerized.go +++ b/lib/dockerized.go @@ -43,7 +43,8 @@ var options = []string{ } func main() { - normalizeEnvironment() + dockerizedRoot := getDockerizedRoot() + normalizeEnvironment(dockerizedRoot) dockerizedOptions, commandName, commandVersion, commandArgs := parseArguments() @@ -60,7 +61,6 @@ func main() { dockerizedDockerComposeFilePath := os.Getenv("COMPOSE_FILE") if dockerizedDockerComposeFilePath == "" { - dockerizedRoot := getDockerizedRoot() dockerizedDockerComposeFilePath = filepath.Join(dockerizedRoot, "docker-compose.yml") } @@ -528,7 +528,8 @@ func findLocalEnvFile(path string) (string, error) { } return "", fmt.Errorf("no local %s found", dockerizedEnvFileName) } -func normalizeEnvironment() { +func normalizeEnvironment(dockerizedRoot string) { + _ = os.Setenv("DOCKERIZED_ROOT", dockerizedRoot) homeDir, _ := os.UserHomeDir() if os.Getenv("HOME") == "" { _ = os.Setenv("HOME", homeDir) From 15f2d8b24861c16f1cc60e0a197effd01c67aea9 Mon Sep 17 00:00:00 2001 From: Bouke Versteegh Date: Sun, 27 Mar 2022 22:24:00 +0200 Subject: [PATCH 04/13] feat: Ability to add or customize commands with additional compose files --- .env | 2 + .github/workflows/test.yml | 11 + bin/dockerized | 4 +- bin/dockerized.ps1 | 2 +- go.mod | 4 +- go.sum | 1 - main.go | 240 ++++++++++++++++++++++ main_test.go | 194 ++++++++++++++++++ {lib => pkg}/dockerized.go | 402 ++++++++----------------------------- pkg/help/help.go | 49 +++++ pkg/util/util.go | 11 + 11 files changed, 602 insertions(+), 318 deletions(-) create mode 100644 main.go create mode 100644 main_test.go rename {lib => pkg}/dockerized.go (58%) create mode 100644 pkg/help/help.go create mode 100644 pkg/util/util.go diff --git a/.env b/.env index 9414b58..5ffd34b 100644 --- a/.env +++ b/.env @@ -1,3 +1,5 @@ +COMPOSE_PATH_SEPARATOR=";" +COMPOSE_FILE="${DOCKERIZED_ROOT}/docker-compose.yml" DEFAULT_ARCH=x86_64 ALPINE_VERSION=3.14.2 ANSIBLE_VERSION=2.12 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4b9802f..18d6196 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -26,6 +26,17 @@ jobs: run: | bin/dockerized --shell $COMMAND -c 'echo $HOST_HOSTNAME' | tee ~/shell.log grep $(hostname) ~/shell.log + IntegrationTest: + runson: ubuntu-latest + steps: + - uses: actions/setup-go@v2 + with: + go-version: '1.17.8' + - name: Checkout Dockerized + uses: actions/checkout@v2 + - name: go test + run: | + DOCKERIZED_ROOT=$(pwd) go test -p 1 . CompileAndTest: runs-on: ${{ matrix.os }} strategy: diff --git a/bin/dockerized b/bin/dockerized index d335de8..aafa2b8 100755 --- a/bin/dockerized +++ b/bin/dockerized @@ -78,11 +78,11 @@ if [ "$DOCKERIZED_COMPILE" ] || [ ! -f "$DOCKERIZED_BINARY" ]; then -v "${DOCKERIZED_ROOT}/.cache:/go/pkg" \ -w //src \ "golang:1.17.8" \ - build -ldflags "$GO_LDFLAGS" -o //build/ lib/dockerized.go + build -ldflags "$GO_LDFLAGS" -o //build/ . else ( cd "$DOCKERIZED_ROOT" - go build -ldflags "$GO_LDFLAGS" -o build/ lib/dockerized.go + go build -ldflags "$GO_LDFLAGS" -o build/ . ) fi diff --git a/bin/dockerized.ps1 b/bin/dockerized.ps1 index 5dd7993..afd3c96 100644 --- a/bin/dockerized.ps1 +++ b/bin/dockerized.ps1 @@ -38,7 +38,7 @@ if (($DOCKERIZED_COMPILE -eq $true) -Or !(Test-Path "$DOCKERIZED_BINARY")) -v "${DOCKERIZED_ROOT}\.cache:/go/pkg" ` -w /src ` "golang:1.17.8" ` - build -o /build/ lib/dockerized.go + build -o /build/ . if ($LASTEXITCODE -ne 0) { Write-StdErr "Failed to compile dockerized." diff --git a/go.mod b/go.mod index 74ad4e9..a3e7c89 100644 --- a/go.mod +++ b/go.mod @@ -19,7 +19,7 @@ require ( github.com/docker/hub-tool v0.4.4 github.com/fatih/color v1.13.0 github.com/hashicorp/go-version v1.3.0 - github.com/joho/godotenv v1.3.0 + github.com/stretchr/testify v1.7.0 ) require ( @@ -88,6 +88,7 @@ require ( github.com/opencontainers/runc v1.1.0 // indirect github.com/pelletier/go-toml v1.9.4 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_golang v1.11.0 // indirect github.com/prometheus/client_model v0.2.0 // indirect github.com/prometheus/common v0.30.0 // indirect @@ -128,6 +129,7 @@ require ( google.golang.org/protobuf v1.27.1 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect k8s.io/apimachinery v0.22.5 // indirect k8s.io/client-go v0.22.5 // indirect k8s.io/klog/v2 v2.30.0 // indirect diff --git a/go.sum b/go.sum index 1e59d12..fd5eccd 100644 --- a/go.sum +++ b/go.sum @@ -986,7 +986,6 @@ github.com/jmhodges/clock v0.0.0-20160418191101-880ee4c33548/go.mod h1:hGT6jSUVz github.com/jmoiron/sqlx v0.0.0-20180124204410-05cef0741ade/go.mod h1:IiEW3SEiiErVyFdH8NTuWjSifiEQKUoyK3LNqr2kCHU= github.com/jmoiron/sqlx v1.2.1-0.20190826204134-d7d95172beb5/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= github.com/joefitzgerald/rainbow-reporter v0.1.0/go.mod h1:481CNgqmVHQZzdIbN52CupLJyoVwB10FQ/IQlF1pdL8= -github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= diff --git a/main.go b/main.go new file mode 100644 index 0000000..7505be6 --- /dev/null +++ b/main.go @@ -0,0 +1,240 @@ +package main + +import ( + "fmt" + "github.com/compose-spec/compose-go/types" + dockerized "github.com/datastack-net/dockerized/pkg" + "github.com/datastack-net/dockerized/pkg/help" + util "github.com/datastack-net/dockerized/pkg/util" + "github.com/docker/compose/v2/pkg/api" + "github.com/fatih/color" + "os" + "path/filepath" + "strings" +) + +var Version string + +var contains = util.Contains + +func main() { + RunCli(os.Args[1:]) +} + +func RunCli(args []string) (err error, exitCode int) { + dockerizedOptions, commandName, commandVersion, commandArgs := parseArguments(args) + + var optionHelp = contains(dockerizedOptions, "--help") || contains(dockerizedOptions, "-h") + var optionVerbose = contains(dockerizedOptions, "--verbose") || contains(dockerizedOptions, "-v") + var optionShell = contains(dockerizedOptions, "--shell") + var optionBuild = contains(dockerizedOptions, "--build") + var optionVersion = contains(dockerizedOptions, "--version") + + dockerizedRoot := dockerized.GetDockerizedRoot() + dockerized.NormalizeEnvironment(dockerizedRoot) + + if optionVerbose { + fmt.Printf("Dockerized root: %s\n", dockerizedRoot) + } + + if optionVersion { + fmt.Printf("dockerized %s\n", Version) + return nil, 0 + } + + hostCwd, _ := os.Getwd() + err = dockerized.LoadEnvFiles(hostCwd, optionVerbose) + if err != nil { + return err, 1 + } + + composeFilePaths := dockerized.GetComposeFilePaths(dockerizedRoot) + + if optionVerbose { + fmt.Printf("Compose files: %s\n", strings.Join(composeFilePaths, ", ")) + } + + if commandName == "" || optionHelp { + err := help.Help(composeFilePaths) + if err != nil { + return err, 1 + } + if optionHelp { + return nil, 0 + } else { + return nil, 1 + } + } + + if commandVersion != "" { + if commandVersion == "?" { + err = dockerized.PrintCommandVersions(composeFilePaths, commandName, optionVerbose) + if err != nil { + return err, 1 + } else { + return nil, 0 + } + } else { + dockerized.SetCommandVersion(composeFilePaths, commandName, optionVerbose, commandVersion) + } + } + + project, err := dockerized.GetProject(composeFilePaths) + if err != nil { + return err, 1 + } + + hostName, _ := os.Hostname() + hostCwdDirName := filepath.Base(hostCwd) + containerCwd := "/host" + if hostCwdDirName != "\\" { + containerCwd += "/" + hostCwdDirName + } + + runOptions := api.RunOptions{ + Service: commandName, + Environment: []string{ + "HOST_HOSTNAME=" + hostName, + }, + Command: commandArgs, + AutoRemove: true, + Tty: true, + WorkingDir: containerCwd, + } + + volumes := []types.ServiceVolumeConfig{ + { + Type: "bind", + Source: hostCwd, + Target: containerCwd, + }} + + if optionBuild { + if optionVerbose { + fmt.Printf("Building container image for %s...\n", commandName) + } + err := dockerized.DockerComposeBuild(composeFilePaths, api.BuildOptions{ + Services: []string{commandName}, + }) + + if err != nil { + return err, 1 + } + } + + if optionShell { + if optionVerbose { + fmt.Printf("Opening shell in container for %s...\n", commandName) + + if len(commandArgs) > 0 { + fmt.Printf("Passing arguments to shell: %s\n", commandArgs) + } + } + + var ps1 = fmt.Sprintf( + "%s %s:\\w \\$ ", + color.BlueString("dockerized %s", commandName), + color.New(color.FgHiBlue).Add(color.Bold).Sprintf("\\u@\\h"), + ) + var welcomeMessage = "Welcome to dockerized shell. Type 'exit' or press Ctrl+D to exit.\n" + welcomeMessage += "Mounted volumes:\n" + + for _, volume := range volumes { + welcomeMessage += fmt.Sprintf(" %s -> %s\n", volume.Source, volume.Target) + } + service, err := project.GetService(commandName) + if err == nil { + for _, volume := range service.Volumes { + welcomeMessage += fmt.Sprintf(" %s -> %s\n", volume.Source, volume.Target) + } + } + welcomeMessage = strings.ReplaceAll(welcomeMessage, "\\", "\\\\") + + shells := []string{ + "bash", + "zsh", + "sh", + } + var shellDetectionCommands []string + for _, shell := range shells { + shellDetectionCommands = append(shellDetectionCommands, "command -v "+shell) + } + for _, shell := range shells { + shellDetectionCommands = append(shellDetectionCommands, "which "+shell) + } + + var cmdPrintWelcome = fmt.Sprintf("echo '%s'", color.YellowString(welcomeMessage)) + var cmdLaunchShell = fmt.Sprintf("$(%s)", strings.Join(shellDetectionCommands, " || ")) + + runOptions.Environment = append(runOptions.Environment, "PS1="+ps1) + runOptions.Entrypoint = []string{"/bin/sh"} + + if len(commandArgs) > 0 { + runOptions.Command = []string{"-c", fmt.Sprintf("%s; %s \"%s\"", cmdPrintWelcome, cmdLaunchShell, strings.Join(commandArgs, "\" \""))} + } else { + runOptions.Command = []string{"-c", fmt.Sprintf("%s; %s", cmdPrintWelcome, cmdLaunchShell)} + } + } + + if !contains(project.ServiceNames(), commandName) { + image := "r.j3ss.co/" + commandName + if optionVerbose { + fmt.Printf("Service %s not found in compose file(s). Fallback to: %s.\n", commandName, image) + fmt.Printf(" This command, if it exists, will not support version switching.\n") + fmt.Printf(" See: https://github.com/jessfraz/dockerfiles\n") + } + err := dockerized.DockerRun(image, runOptions, volumes) + if err != nil { + return err, 1 + } + return nil, 0 + } + + err = dockerized.DockerComposeRun(project, runOptions, volumes) + if err != nil { + return err, 1 + } + return nil, 0 +} + +func parseArguments(args []string) ([]string, string, string, []string) { + var options = []string{ + "--shell", + "--build", + "-h", + "--help", + "-v", + "--verbose", + "--version", + } + + commandName := "" + var commandArgs []string + var dockerizedOptions []string + var commandVersion string + for _, arg := range args { + if arg[0] == '-' && commandName == "" { + if util.Contains(options, arg) { + dockerizedOptions = append(dockerizedOptions, arg) + } else { + fmt.Println("Unknown option:", arg) + os.Exit(1) + } + } else { + if commandName == "" { + commandName = arg + } else { + commandArgs = append(commandArgs, arg) + } + } + } + if strings.ContainsRune(commandName, ':') { + commandSplit := strings.Split(commandName, ":") + commandName = commandSplit[0] + commandVersion = commandSplit[1] + if commandVersion == "" { + commandVersion = "?" + } + } + return dockerizedOptions, commandName, commandVersion, commandArgs +} diff --git a/main_test.go b/main_test.go new file mode 100644 index 0000000..f33142d --- /dev/null +++ b/main_test.go @@ -0,0 +1,194 @@ +package main + +import ( + "fmt" + dockerized "github.com/datastack-net/dockerized/pkg" + "github.com/stretchr/testify/assert" + "io/ioutil" + "os" + "strings" + "testing" +) + +var initialEnv = os.Environ() + +type Context struct { + homePath string + before []func() + after []func() + envBefore []string + cwdBefore string +} + +func TestHelp(t *testing.T) { + output := testDockerized(t, []string{"--help"}) + assert.Contains(t, output, "Usage:") +} + +func TestOverrideVersionWithEnvVar(t *testing.T) { + defer context().WithEnv("PROTOC_VERSION", "3.6.0").Restore() + var output = testDockerized(t, []string{"protoc", "--version"}) + assert.Contains(t, output, "libprotoc 3.6.0") +} + +func TestCustomGlobalComposeFileAdditionalService(t *testing.T) { + homePath := dockerized.GetDockerizedRoot() + "/test/additional_service" + + println(strings.Join(os.Environ(), " / ")) + + defer context(). + WithHome(homePath). + WithHomeEnvFile(`COMPOSE_FILE="${COMPOSE_FILE};${HOME}/docker-compose.yml"`). + WithFile(homePath+"/docker-compose.yml", ` +version: "3" +services: + test: + image: alpine +`). + Restore() + var output = testDockerized(t, []string{"test", "uname"}) + assert.Contains(t, output, "Linux") +} + +func TestUserCanGloballyCustomizeDockerizedCommands(t *testing.T) { + homePath := dockerized.GetDockerizedRoot() + "/test/customized_service" + + defer context(). + WithHome(homePath). + WithHomeEnvFile(`COMPOSE_FILE="${COMPOSE_FILE};${HOME}/docker-compose.yml"`). + WithFile(homePath+"/docker-compose.yml", ` +version: "3" +services: + alpine: + environment: + CUSTOM: "CUSTOM_123456" +`). + Restore() + var output = testDockerized(t, []string{"alpine", "env"}) + assert.Contains(t, output, "CUSTOM_123456") +} + +func TestUserCanLocallyCustomizeDockerizedCommands(t *testing.T) { + return + projectPath := dockerized.GetDockerizedRoot() + "/test/project_with_customized_service" + + defer context(). + WithCwd(projectPath). + WithHomeEnvFile(`COMPOSE_FILE="${COMPOSE_FILE};docker-compose.yml"`). + WithFile(projectPath+"/docker-compose.yml", ` +version: "3" +services: + alpine: + environment: + CUSTOM: "CUSTOM_123456" +`). + Restore() + var output = testDockerized(t, []string{"alpine", "env"}) + assert.Contains(t, output, "CUSTOM_123456") +} + +func (c *Context) WithEnv(key string, value string) *Context { + _ = os.Setenv(key, value) + //c.after = append(c.after, func() { + // _ = os.Unsetenv(key) + //}) + return c +} + +func (c *Context) WithHome(path string) *Context { + c.homePath = path + _ = os.MkdirAll(path, os.ModePerm) + c.WithEnv("HOME", path) + c.WithEnv("USERPROFILE", path) + return c +} + +func (c *Context) WithCwd(path string) *Context { + c.cwdBefore, _ = os.Getwd() + os.Chdir(path) + c.after = append(c.after, func() { + os.Chdir(c.cwdBefore) + }) + return c +} + +func (c *Context) WithHomeEnvFile(content string) *Context { + return c.WithFile(c.homePath+"/dockerized.env", content) +} + +func (c *Context) WithFile(path string, content string) *Context { + _ = os.WriteFile(path, []byte(content), 0644) + c.after = append(c.after, func() { + _ = os.Remove(path) + }) + return c +} + +func (c *Context) Execute(callback func()) { + for _, before := range c.before { + before() + } + defer func() { + for _, after := range c.after { + after() + } + }() + callback() +} + +func restoreInitialEnv() { + os.Clearenv() + for _, envEntry := range initialEnv { + keyValue := strings.Split(envEntry, "=") + os.Setenv(keyValue[0], keyValue[1]) + } +} + +func (c *Context) Restore() { + defer restoreInitialEnv() + for _, after := range c.after { + //goland:noinspection GoDeferInLoop + defer after() + } +} + +func context() *Context { + restoreInitialEnv() + return &Context{ + envBefore: os.Environ(), + } +} + +func TestOverrideVersionWithGlobalEnvFile(t *testing.T) { + defer context(). + WithHome(dockerized.GetDockerizedRoot() + "/test/home"). + WithHomeEnvFile("PROTOC_VERSION=3.8.0"). + Restore() + + var output = testDockerized(t, []string{"protoc", "--version"}) + + assert.Contains(t, output, "3.8.0") +} + +func capture(callback func()) string { + read, write, _ := os.Pipe() + os.Stdout = write + + callback() + + os.Stdout.Close() + bytes, _ := ioutil.ReadAll(read) + var output = string(bytes) + return output +} + +func testDockerized(t *testing.T, args []string) string { + var err error + var exitCode int + var output = capture(func() { + err, exitCode = RunCli(args) + }) + assert.Nil(t, err, fmt.Sprintf("error: %s", err)) + assert.Equal(t, 0, exitCode) + return output +} diff --git a/lib/dockerized.go b/pkg/dockerized.go similarity index 58% rename from lib/dockerized.go rename to pkg/dockerized.go index 1368880..9cf7788 100644 --- a/lib/dockerized.go +++ b/pkg/dockerized.go @@ -1,7 +1,9 @@ -package main +package dockerized import ( "encoding/json" + "github.com/compose-spec/compose-go/dotenv" + "github.com/datastack-net/dockerized/pkg/util" "github.com/hashicorp/go-version" "io" "net/http" @@ -19,8 +21,6 @@ import ( "github.com/docker/compose/v2/pkg/compose" "github.com/docker/distribution/reference" "github.com/docker/hub-tool/pkg/hub" - "github.com/fatih/color" - "github.com/joho/godotenv" "os" "os/signal" "path/filepath" @@ -30,197 +30,20 @@ import ( "syscall" ) -var Version string - -var options = []string{ - "--shell", - "--build", - "-h", - "--help", - "-v", - "--verbose", - "--version", -} - -func main() { - dockerizedRoot := getDockerizedRoot() - normalizeEnvironment(dockerizedRoot) - - dockerizedOptions, commandName, commandVersion, commandArgs := parseArguments() - - var optionHelp = contains(dockerizedOptions, "--help") || contains(dockerizedOptions, "-h") - var optionVerbose = contains(dockerizedOptions, "--verbose") || contains(dockerizedOptions, "-v") - var optionShell = contains(dockerizedOptions, "--shell") - var optionBuild = contains(dockerizedOptions, "--build") - var optionVersion = contains(dockerizedOptions, "--version") - - if optionVersion { - fmt.Printf("dockerized %s\n", Version) - os.Exit(0) - } - - dockerizedDockerComposeFilePath := os.Getenv("COMPOSE_FILE") - if dockerizedDockerComposeFilePath == "" { - dockerizedDockerComposeFilePath = filepath.Join(dockerizedRoot, "docker-compose.yml") - } - - if optionVerbose { - fmt.Println("Dockerized docker-compose file: ", dockerizedDockerComposeFilePath) - } - - if commandName == "" || optionHelp { - err := help(dockerizedDockerComposeFilePath) - if err != nil { - fmt.Println(err) - os.Exit(1) - } - if optionHelp { - os.Exit(0) - } else { - os.Exit(1) - } - } - - hostCwd, _ := os.Getwd() - var err = loadEnvFiles(hostCwd, optionVerbose) - if err != nil { - panic(err) - } - - if commandVersion != "" { - if commandVersion == "?" { - err = PrintCommandVersions(dockerizedDockerComposeFilePath, commandName, optionVerbose) - if err != nil { - fmt.Println(err) - os.Exit(1) - } else { - os.Exit(0) - } - } else { - setCommandVersion(dockerizedDockerComposeFilePath, commandName, optionVerbose, commandVersion) - } - } - - project, err := getProject(dockerizedDockerComposeFilePath) - if err != nil { - panic(err) - } - - hostName, _ := os.Hostname() - hostCwdDirName := filepath.Base(hostCwd) - containerCwd := "/host" - if hostCwdDirName != "\\" { - containerCwd += "/" + hostCwdDirName - } - - runOptions := api.RunOptions{ - Service: commandName, - Environment: []string{ - "HOST_HOSTNAME=" + hostName, - }, - Command: commandArgs, - AutoRemove: true, - Tty: true, - WorkingDir: containerCwd, - } - - volumes := []types.ServiceVolumeConfig{ - { - Type: "bind", - Source: hostCwd, - Target: containerCwd, - }} - - if optionBuild { - if optionVerbose { - fmt.Printf("Building container image for %s...\n", commandName) - } - err := dockerComposeBuild(dockerizedDockerComposeFilePath, api.BuildOptions{ - Services: []string{commandName}, - }) - - if err != nil { - fmt.Println(err) - os.Exit(1) - } - } - - if optionShell { - if optionVerbose { - fmt.Printf("Opening shell in container for %s...\n", commandName) - - if len(commandArgs) > 0 { - fmt.Printf("Passing arguments to shell: %s\n", commandArgs) - } - } - - var ps1 = fmt.Sprintf( - "%s %s:\\w \\$ ", - color.BlueString("dockerized %s", commandName), - color.New(color.FgHiBlue).Add(color.Bold).Sprintf("\\u@\\h"), - ) - var welcomeMessage = "Welcome to dockerized shell. Type 'exit' or press Ctrl+D to exit.\n" - welcomeMessage += "Mounted volumes:\n" - - for _, volume := range volumes { - welcomeMessage += fmt.Sprintf(" %s -> %s\n", volume.Source, volume.Target) - } - service, err := project.GetService(commandName) - if err == nil { - for _, volume := range service.Volumes { - welcomeMessage += fmt.Sprintf(" %s -> %s\n", volume.Source, volume.Target) - } - } - welcomeMessage = strings.ReplaceAll(welcomeMessage, "\\", "\\\\") - - shells := []string{ - "bash", - "zsh", - "sh", - } - var shellDetectionCommands []string - for _, shell := range shells { - shellDetectionCommands = append(shellDetectionCommands, "command -v "+shell) - } - for _, shell := range shells { - shellDetectionCommands = append(shellDetectionCommands, "which "+shell) - } - - var cmdPrintWelcome = fmt.Sprintf("echo '%s'", color.YellowString(welcomeMessage)) - var cmdLaunchShell = fmt.Sprintf("$(%s)", strings.Join(shellDetectionCommands, " || ")) - - runOptions.Environment = append(runOptions.Environment, "PS1="+ps1) - runOptions.Entrypoint = []string{"/bin/sh"} - - if len(commandArgs) > 0 { - runOptions.Command = []string{"-c", fmt.Sprintf("%s; %s \"%s\"", cmdPrintWelcome, cmdLaunchShell, strings.Join(commandArgs, "\" \""))} - } else { - runOptions.Command = []string{"-c", fmt.Sprintf("%s; %s", cmdPrintWelcome, cmdLaunchShell)} - } - } - - if !contains(project.ServiceNames(), commandName) { - image := "r.j3ss.co/" + commandName - if optionVerbose { - fmt.Printf("Service %s not found in %s. Fallback to: %s.\n", commandName, dockerizedDockerComposeFilePath, image) - fmt.Printf(" This command, if it exists, will not support version switching.\n") - fmt.Printf(" See: https://github.com/jessfraz/dockerfiles\n") - } - err := dockerRun(image, runOptions, volumes) - if err != nil { - fmt.Println(err) - os.Exit(1) - } - os.Exit(0) - } - - err = dockerComposeRun(project, runOptions, volumes) - if err != nil { - if optionVerbose { - fmt.Println(err) +// Determine which docker-compose file to use. Assumes .env files are already loaded. +func GetComposeFilePaths(dockerizedRoot string) []string { + var composeFilePaths []string + composeFilePath := os.Getenv("COMPOSE_FILE") + if composeFilePath == "" { + composeFilePaths = append(composeFilePaths, filepath.Join(dockerizedRoot, "docker-compose.yml")) + } else { + composePathSeparator := os.Getenv("COMPOSE_PATH_SEPARATOR") + if composePathSeparator == "" { + composePathSeparator = ";" } - os.Exit(1) + composeFilePaths = strings.Split(composeFilePath, composePathSeparator) } + return composeFilePaths } func getNpmPackageVersions(packageName string) ([]string, error) { @@ -259,8 +82,8 @@ func getNpmPackageVersions(packageName string) ([]string, error) { return versionKeys, nil } -func PrintCommandVersions(dockerizedDockerComposeFilePath string, commandName string, verbose bool) error { - project, err := getProject(dockerizedDockerComposeFilePath) +func PrintCommandVersions(composeFilePaths []string, commandName string, verbose bool) error { + project, err := GetProject(composeFilePaths) if err != nil { return err } @@ -390,8 +213,8 @@ func sortVersions(versions []string) { }) } -func setCommandVersion(dockerizedDockerComposeFilePath string, commandName string, optionVerbose bool, commandVersion string) { - rawProject, err := getRawProject(dockerizedDockerComposeFilePath) +func SetCommandVersion(composeFilePaths []string, commandName string, optionVerbose bool, commandVersion string) { + rawProject, err := getRawProject(composeFilePaths) if err != nil { panic(err) } @@ -417,7 +240,7 @@ func setCommandVersion(dockerizedDockerComposeFilePath string, commandName strin } versionKey := versionVariableExpected - if !contains(variablesUsed, versionVariableExpected) { + if !util.Contains(variablesUsed, versionVariableExpected) { if len(versionVariablesUsed) == 1 { fmt.Printf("Error: To specify the version of %s, please set %s.\n", commandName, @@ -442,33 +265,72 @@ func setCommandVersion(dockerizedDockerComposeFilePath string, commandName strin } } -func loadEnvFiles(hostCwd string, optionVerbose bool) error { - homeDir, _ := os.UserHomeDir() - userGlobalDockerizedEnvFile := filepath.Join(homeDir, dockerizedEnvFileName) - localDockerizedEnvFile, err := findLocalEnvFile(hostCwd) - +func LoadEnvFiles(hostCwd string, optionVerbose bool) error { var envFiles []string + // Default + dockerizedEnvFile := GetDockerizedRoot() + "/.env" + envFiles = append(envFiles, dockerizedEnvFile) + + // Global overrides + homeDir, _ := os.UserHomeDir() + userGlobalDockerizedEnvFile := filepath.Join(homeDir, dockerizedEnvFileName) if _, err := os.Stat(userGlobalDockerizedEnvFile); err == nil { envFiles = append(envFiles, userGlobalDockerizedEnvFile) } - if err == nil && !contains(envFiles, localDockerizedEnvFile) { + + // Local overrides + if localDockerizedEnvFile, err := findLocalEnvFile(hostCwd); err == nil { envFiles = append(envFiles, localDockerizedEnvFile) } + envFiles = unique(envFiles) + if optionVerbose { - // Print it in order of priority (lowest to highest) for _, envFile := range envFiles { - fmt.Println("Loading: ", envFile) + fmt.Printf("Loading: '%s'\n", envFile) } } - // Load in reverse. GoDotEnv does not override vars, this allows runtime env-vars to override the env files. - for i := len(envFiles) - 1; i >= 0; i-- { - err := godotenv.Load(envFiles[i]) - if err != nil { - return err + + dockerizedEnvMap, err := dotenv.ReadWithLookup(func(key string) (string, bool) { + if os.Getenv(key) != "" { + return os.Getenv(key), true + } else { + return "", false + } + }, dockerizedEnvFile) + if err != nil { + return err + } + + envMap, err := dotenv.ReadWithLookup(func(key string) (string, bool) { + if dockerizedEnvMap[key] != "" { + return dockerizedEnvMap[key], true + } + var envValue = os.Getenv(key) + if envValue != "" { + return envValue, true + } else { + return "", false + } + }, envFiles...) + if err != nil { + return err + } + + currentEnv := map[string]bool{} + rawEnv := os.Environ() + for _, rawEnvLine := range rawEnv { + key := strings.Split(rawEnvLine, "=")[0] + currentEnv[key] = true + } + + for key, value := range envMap { + if !currentEnv[key] { + _ = os.Setenv(key, value) } } + return nil } @@ -476,16 +338,16 @@ func dockerComposeRunAdHocService(service types.ServiceConfig, runOptions api.Ru if service.Environment == nil { service.Environment = map[string]*string{} } - return dockerComposeRun(&types.Project{ + return DockerComposeRun(&types.Project{ Name: "dockerized", Services: []types.ServiceConfig{ service, }, - WorkingDir: getDockerizedRoot(), + WorkingDir: GetDockerizedRoot(), }, runOptions, []types.ServiceVolumeConfig{}) } -func dockerRun(image string, runOptions api.RunOptions, volumes []types.ServiceVolumeConfig) error { +func DockerRun(image string, runOptions api.RunOptions, volumes []types.ServiceVolumeConfig) error { // Couldn't get 'docker run' to work, so instead define a Docker Compose Service and run that. // This coincidentally allows re-using the same code for both 'docker run' and 'docker-compose run' // - ContainerCreate is simple, but the logic to attach to it is very complex, and not exposed by the Docker SDK. @@ -499,7 +361,10 @@ func dockerRun(image string, runOptions api.RunOptions, volumes []types.ServiceV var dockerizedEnvFileName = "dockerized.env" -func getDockerizedRoot() string { +func GetDockerizedRoot() string { + if os.Getenv("DOCKERIZED_ROOT") != "" { + return os.Getenv("DOCKERIZED_ROOT") + } executable, err := os.Executable() if err != nil { panic("Cannot detect dockerized root directory: " + err.Error()) @@ -507,16 +372,6 @@ func getDockerizedRoot() string { return filepath.Dir(filepath.Dir(executable)) } -func contains(s []string, str string) bool { - for _, v := range s { - if v == str { - return true - } - } - - return false -} - func findLocalEnvFile(path string) (string, error) { envFilePath := "" for i := 0; i < 10; i++ { @@ -528,7 +383,7 @@ func findLocalEnvFile(path string) (string, error) { } return "", fmt.Errorf("no local %s found", dockerizedEnvFileName) } -func normalizeEnvironment(dockerizedRoot string) { +func NormalizeEnvironment(dockerizedRoot string) { _ = os.Setenv("DOCKERIZED_ROOT", dockerizedRoot) homeDir, _ := os.UserHomeDir() if os.Getenv("HOME") == "" { @@ -547,10 +402,8 @@ func newSigContext() (context.Context, func()) { return ctx, cancel } -func getRawProject(dockerComposeFilePath string) (*types.Project, error) { - options, err := cli.NewProjectOptions([]string{ - dockerComposeFilePath, - }, +func getRawProject(composeFilePaths []string) (*types.Project, error) { + options, err := cli.NewProjectOptions(composeFilePaths, cli.WithInterpolation(false), cli.WithLoadOptions(func(l *loader.Options) { l.SkipValidation = true @@ -566,17 +419,14 @@ func getRawProject(dockerComposeFilePath string) (*types.Project, error) { return cli.ProjectFromOptions(options) } -func getProject(dockerComposeFilePath string) (*types.Project, error) { - options, err := cli.NewProjectOptions([]string{ - dockerComposeFilePath, - }, - cli.WithDotEnv, +func GetProject(composeFilePaths []string) (*types.Project, error) { + options, err := cli.NewProjectOptions([]string{}, cli.WithOsEnv, cli.WithConfigFileEnv, - ) //, cli.WithDefaultConfigPath + ) if err != nil { - return nil, nil + return nil, err } return cli.ProjectFromOptions(options) @@ -626,8 +476,8 @@ func getBackend() (*api.ServiceProxy, error) { return backend, nil } -func dockerComposeBuild(dockerComposeFilePath string, buildOptions api.BuildOptions) error { - project, err := getProject(dockerComposeFilePath) +func DockerComposeBuild(composeFilePaths []string, buildOptions api.BuildOptions) error { + project, err := GetProject(composeFilePaths) if err != nil { return err } @@ -644,7 +494,7 @@ func dockerComposeBuild(dockerComposeFilePath string, buildOptions api.BuildOpti return backend.Build(ctx, project, buildOptions) } -func dockerComposeRun(project *types.Project, runOptions api.RunOptions, volumes []types.ServiceVolumeConfig) error { +func DockerComposeRun(project *types.Project, runOptions api.RunOptions, volumes []types.ServiceVolumeConfig) error { err := os.Chdir(project.WorkingDir) if err != nil { return err @@ -685,80 +535,6 @@ func dockerComposeRun(project *types.Project, runOptions api.RunOptions, volumes return nil } -func help(dockerComposeFilePath string) error { - project, err := getProject(dockerComposeFilePath) - if err != nil { - return err - } - - fmt.Println("Usage: dockerized [options] [:version] [arguments]") - fmt.Println("") - fmt.Println("Examples:") - fmt.Println(" dockerized go") - fmt.Println(" dockerized go:1.8 build") - fmt.Println(" dockerized --shell go") - fmt.Println(" dockerized go:?") - fmt.Println("") - - fmt.Println("Commands:") - services := project.ServiceNames() - sort.Strings(services) - for _, service := range services { - fmt.Printf(" %s\n", service) - } - fmt.Println() - - fmt.Println("Options:") - fmt.Println(" --build Rebuild the container before running it.") - fmt.Println(" --shell Start a shell inside the command container. Similar to `docker run --entrypoint=sh`.") - fmt.Println(" -v, --verbose Log what dockerized is doing.") - fmt.Println(" -h, --help Show this help.") - fmt.Println() - - fmt.Println("Version:") - fmt.Println(" : The version of the command to run, e.g. 1, 1.8, 1.8.1.") - fmt.Println(" :? List all available versions. E.g. `dockerized go:?`") - fmt.Println(" : Same as ':?' .") - fmt.Println() - - fmt.Println("Arguments:") - fmt.Println(" All arguments after are passed to the command itself.") - - return nil -} - -func parseArguments() ([]string, string, string, []string) { - commandName := "" - var commandArgs []string - var dockerizedOptions []string - var commandVersion string - for _, arg := range os.Args[1:] { - if arg[0] == '-' && commandName == "" { - if contains(options, arg) { - dockerizedOptions = append(dockerizedOptions, arg) - } else { - fmt.Println("Unknown option:", arg) - os.Exit(1) - } - } else { - if commandName == "" { - commandName = arg - } else { - commandArgs = append(commandArgs, arg) - } - } - } - if strings.ContainsRune(commandName, ':') { - commandSplit := strings.Split(commandName, ":") - commandName = commandSplit[0] - commandVersion = commandSplit[1] - if commandVersion == "" { - commandVersion = "?" - } - } - return dockerizedOptions, commandName, commandVersion, commandArgs -} - func unique(s []string) []string { keys := make(map[string]bool) var list []string diff --git a/pkg/help/help.go b/pkg/help/help.go new file mode 100644 index 0000000..bcee6f7 --- /dev/null +++ b/pkg/help/help.go @@ -0,0 +1,49 @@ +package help + +import ( + "fmt" + dockerized "github.com/datastack-net/dockerized/pkg" + "sort" +) + +func Help(composeFilePaths []string) error { + project, err := dockerized.GetProject(composeFilePaths) + if err != nil { + return err + } + + fmt.Println("Usage: dockerized [options] [:version] [arguments]") + fmt.Println("") + fmt.Println("Examples:") + fmt.Println(" dockerized go") + fmt.Println(" dockerized go:1.8 build") + fmt.Println(" dockerized --shell go") + fmt.Println(" dockerized go:?") + fmt.Println("") + + fmt.Println("Commands:") + services := project.ServiceNames() + sort.Strings(services) + for _, service := range services { + fmt.Printf(" %s\n", service) + } + fmt.Println() + + fmt.Println("Options:") + fmt.Println(" --build Rebuild the container before running it.") + fmt.Println(" --shell Start a shell inside the command container. Similar to `docker run --entrypoint=sh`.") + fmt.Println(" -v, --verbose Log what dockerized is doing.") + fmt.Println(" -h, --help Show this help.") + fmt.Println() + + fmt.Println("Version:") + fmt.Println(" : The version of the command to run, e.g. 1, 1.8, 1.8.1.") + fmt.Println(" :? List all available versions. E.g. `dockerized go:?`") + fmt.Println(" : Same as ':?' .") + fmt.Println() + + fmt.Println("Arguments:") + fmt.Println(" All arguments after are passed to the command itself.") + + return nil +} diff --git a/pkg/util/util.go b/pkg/util/util.go new file mode 100644 index 0000000..1e8813e --- /dev/null +++ b/pkg/util/util.go @@ -0,0 +1,11 @@ +package util + +func Contains(s []string, str string) bool { + for _, v := range s { + if v == str { + return true + } + } + + return false +} From e07291f888b2fde13069e5a1297c0aa88975b669 Mon Sep 17 00:00:00 2001 From: Bouke Versteegh Date: Sun, 27 Mar 2022 22:32:52 +0200 Subject: [PATCH 05/13] fix integration test job --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 18d6196..d941058 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,7 +27,7 @@ jobs: bin/dockerized --shell $COMMAND -c 'echo $HOST_HOSTNAME' | tee ~/shell.log grep $(hostname) ~/shell.log IntegrationTest: - runson: ubuntu-latest + runs-on: ubuntu-latest steps: - uses: actions/setup-go@v2 with: From 16d0643e5755572848f84078c8405615a9f38968 Mon Sep 17 00:00:00 2001 From: Bouke Versteegh Date: Sun, 27 Mar 2022 22:41:23 +0200 Subject: [PATCH 06/13] test allowing local custom compose file --- main_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/main_test.go b/main_test.go index f33142d..acde949 100644 --- a/main_test.go +++ b/main_test.go @@ -69,7 +69,6 @@ services: } func TestUserCanLocallyCustomizeDockerizedCommands(t *testing.T) { - return projectPath := dockerized.GetDockerizedRoot() + "/test/project_with_customized_service" defer context(). From 46a26495892368ed6c68ba184c967e181068c132 Mon Sep 17 00:00:00 2001 From: Bouke Versteegh Date: Sun, 27 Mar 2022 23:13:10 +0200 Subject: [PATCH 07/13] test overriding of env vars --- main_test.go | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/main_test.go b/main_test.go index acde949..f0f6931 100644 --- a/main_test.go +++ b/main_test.go @@ -31,6 +31,34 @@ func TestOverrideVersionWithEnvVar(t *testing.T) { assert.Contains(t, output, "libprotoc 3.6.0") } +func TestLocalEnvFileOverridesGlobalEnvFile(t *testing.T) { + var homePath = dockerized.GetDockerizedRoot() + "/test/home" + var projectPath = dockerized.GetDockerizedRoot() + "/test/project_override_global" + defer context(). + WithHome(homePath). + WithHomeEnvFile("PROTOC_VERSION=3.6.0"). + WithDir(projectPath). + WithCwd(projectPath). + WithFile(projectPath+"/dockerized.env", "PROTOC_VERSION=3.8.0"). + Restore() + var output = testDockerized(t, []string{"-v", "protoc", "--version"}) + assert.Contains(t, output, "libprotoc 3.8.0") +} + +func TestRuntimeEnvOverridesLocalEnvFile(t *testing.T) { + var homePath = dockerized.GetDockerizedRoot() + "/test/home" + var projectPath = dockerized.GetDockerizedRoot() + "/test/project_override_global" + defer context(). + WithHome(homePath). + WithDir(projectPath). + WithCwd(projectPath). + WithFile(projectPath+"/dockerized.env", "PROTOC_VERSION=3.8.0"). + WithEnv("PROTOC_VERSION", "3.16.1"). + Restore() + var output = testDockerized(t, []string{"protoc", "--version"}) + assert.Contains(t, output, "libprotoc 3.16.1") +} + func TestCustomGlobalComposeFileAdditionalService(t *testing.T) { homePath := dockerized.GetDockerizedRoot() + "/test/additional_service" @@ -123,6 +151,14 @@ func (c *Context) WithFile(path string, content string) *Context { return c } +func (c *Context) WithDir(path string) *Context { + _ = os.MkdirAll(path, os.ModePerm) + c.after = append(c.after, func() { + _ = os.RemoveAll(path) + }) + return c +} + func (c *Context) Execute(callback func()) { for _, before := range c.before { before() @@ -187,6 +223,7 @@ func testDockerized(t *testing.T, args []string) string { var output = capture(func() { err, exitCode = RunCli(args) }) + println(output) assert.Nil(t, err, fmt.Sprintf("error: %s", err)) assert.Equal(t, 0, exitCode) return output From 6895b7154d2dede95b078e075b694b86cc8e0099 Mon Sep 17 00:00:00 2001 From: Bouke Versteegh Date: Mon, 28 Mar 2022 12:05:48 +0200 Subject: [PATCH 08/13] feat: Logo for Dockerized --- README.md | 5 ++++- dockerized-banner.png | Bin 0 -> 33421 bytes 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 dockerized-banner.png diff --git a/README.md b/README.md index 09c44b0..5b81953 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,7 @@ -# Dockerized [![Compile and Test](https://github.com/datastack-net/dockerized/actions/workflows/test.yml/badge.svg)](https://github.com/datastack-net/dockerized/actions/workflows/test.yml) +![Dockerized](dockerized-banner.png) + +# Dockerized [![Compile and Test](https://github.com/datastack-net/dockerized/actions/workflows/test.yml/badge.svg)](https://github.com/datastack-net/dockerized/actions/workflows/test.yml) + Run popular commandline tools without installing them. ```shell diff --git a/dockerized-banner.png b/dockerized-banner.png new file mode 100644 index 0000000000000000000000000000000000000000..48e93dbe23912838698e546d79bd7ac7e2a199e9 GIT binary patch literal 33421 zcmbUJbySqw`#%ov8HN^75ETqiDQU?gHDl4KAOaF9sf3b(!VF@eNQZ#HC@Ba?OX$Ec zNTnSqL8Xxp27#&jw}F+;NDB2x4S>B z$F|k+qJK|0`-!ndb$yF#Z2C?TQ18NfZ@by5;@5VdQB3$^P=wf?1m^T_A)hwx-2Kjq zr_Jto<*xGH%5{00k({#MQ&lB>&j2H9PR6V}zo;SHdSG;Ig#qqOQE0}2<~m(VA>r7a zkkJ2se-*i$1_0-IJVxcJnTXrIRkN=2&sg972D?)hn1zRZ``m`rhqJO`qN9Iv0}yr+ z4FDlw`A!%gr7NT4XzZPEs|OieO2cYP0g1T)h$)IgI;52ZDH@;K?rQT;EwSMzoj;4S zsj~w@k^%fT3$X}Grr|7xnS<7TF#s}p4LM$M0kC`P-#|osI3Jyx8h=-k?Q3VvNmsZd zw*dTVTa?DoQY=M--Js07CS|ZR%>`!9;s?eBG*R-@bi``>XQ$4l9`-24IKY)=q6l zmdA#N6RWG$G{#9m3+ekt=0ghayHy^Y?#fQ(-J{U@@$sJZM2@5Kyir^1-n*ZV{#o6< z#-XKmaj~?ry!?lWxw)3)-T=ztx4Ft$cU?8L$ycSN^|X^l{%+UcH31;vO~%z+YYwy| za4uU+f#thU22ymT~3 z%FgcYE#+0eEcpIgbX~545m-eU^h%KduCib9X=aa#2***Zf6V!VwLKl)FmFf%pbwC&I@^YJeALT@jx zPPZ6>L_wvA1Z#m<=WG&vJ^FRPW=&dJ+FABkuYo~1N#1^0HI{9G<%ir!pPpeP5WB@P ziNUiK#P|}sj5GF4F^j{HzmUy*OsQLEETgHFLO1%Zf>8}YXGZ`Ei8~>|QO7eU`|L(b zE8o7APgQhE32Km!^xJ6VJ|4Ov9Mb;m;PF4v+95Nq_v?oKtFxxDt?NDK`gX7N#gPPB zuy5O_J@y9j?ur>_UXMrXr%(594V}8?;^G1~Pd>qt?A-(m^7!%N-mBFvzP{$v*$*?T ze)Our&X*ia%*JHCA<7heyMlHqxS@Le)NJj&d+cdhS&M;c2~b}Tu{7F1%y1d2r8UoC z%~-z6y&Rhs&*;f!COo6kh0)AMeRrGW6S+2KBG)ITV#gA@I!UxkZf<&|j&FzNuZp7p z@i0rfyHAykJL%J^C-DCM=T`EMFr-YIkZ5kG?(_3f_&)^|?ls|79MMmn)XAfAavW?> z;?UrPwp3gplpPtG#?SWVjnEk883cr~>XaZ@kiDlVcxh37v*%0A?{pVjmcSQY9JCgr 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 literal 0 HcmV?d00001 From 14e22c50958234fff437e16cc60151a42da0a92d Mon Sep 17 00:00:00 2001 From: Bouke Versteegh Date: Mon, 28 Mar 2022 12:06:07 +0200 Subject: [PATCH 09/13] ignore test files --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index ac4e202..ec6efa9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ build/* .cache/* -.go/* \ No newline at end of file +.go/* +test/* \ No newline at end of file From e1d73e2cd810abe20ebc7c5ced59d345e2d66943 Mon Sep 17 00:00:00 2001 From: Bouke Versteegh Date: Mon, 28 Mar 2022 12:12:02 +0200 Subject: [PATCH 10/13] feat: Add your own commands, or customize the defaults --- README.md | 75 ++++++++++++++++++++++++++++++++++++++++++----- main_test.go | 52 ++++++++++++++++++-------------- pkg/dockerized.go | 57 ++++++++++++++++++++++++----------- 3 files changed, 138 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index 5b81953..982f4dd 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ dockerized ## Supported commands -> If your favorite command is not included, it can be added very easily. See [Add a command](DEV.md). +> If your favorite command is not included, it can be added very easily. See [Customization](#customization). > Dockerized will also fall back to over 150 commands defined in [jessfraz/dockerfiles](https://github.com/jessfraz/dockerfiles). - Cloud @@ -157,7 +157,7 @@ dockerized npm install # install packages.json ## Switching command versions -**Ad-hoc** +### Ad-hoc Add `:` to the end of the command to override the version. @@ -166,7 +166,7 @@ dockerized node:15 ``` -**Listing versions** +### Listing versions To see which versions are available, run: @@ -176,7 +176,7 @@ dockerized node:? dockerized node: ``` -**Environment Variables** +### Environment Variables Each command has a `_VERSION` environment variable which you can override. @@ -206,12 +206,12 @@ Notes: - [.env](.env) -**Per directory** +**Per project (directory)** -You can also specify version and other settings per directory. +You can also specify version and other settings per directory and its subdirectory. This allows you to "lock" your tools to specific versions for your project. -- Create a `dockerized.env` file in your project directory. +- Create a `dockerized.env` file in the root of your project directory. - All commands executed within this directory will use the settings specified in this file. @@ -232,6 +232,67 @@ This allows you to "lock" your tools to specific versions for your project. dockerized node ``` +## Customization + +Dockerized uses [Docker Compose](https://docs.docker.com/compose/overview/) to run commands, which are defined in a Compose File. +The default commands are listed in [docker-compose.yml](docker-compose.yml). You can add your own commands or customize the defaults, by loading a custom Compose File. + +The `COMPOSE_FILE` environment variable defines which files to load. This variable is set by the included [.env](.env) file, and your own `dockerized.env` files, as explained in [Environment Variables](#environment-variables). To load an additional Compose File, add the path to the file to the `COMPOSE_FILE` environment variable. + + +### Including an additional Compose File + +**Globally** + +To change global settings, create a file `dockerized.env` in your home directory, which loads an extra Compose File. +In this example, the Compose File is also in the home directory, referenced relative to the `${HOME}` directory. The original variable `${COMPOSE_FILE}` is included to preserve the default commands. Omit it if you want to completely replace the default commands. + +```bash +COMPOSE_FILE="${COMPOSE_FILE};${HOME}/docker-compose.yml" +``` + +**Per Project** + +To change settings within a specific directory, create a file `dockerized.env` in the root of that directory, which loads the extra Compose File. `${DOCKERIZED_PROJECT_ROOT}` refers to the absolute path to the root of the project. + +```shell +COMPOSE_FILE="${COMPOSE_FILE};${DOCKERIZED_PROJECT_ROOT}/docker-compose.yml" +``` +`${DOCKERIZED_PROJECT_ROOT}` contains absolute path to the project directory. In this example it is `myproject`. + +### Adding custom commands + +Your custom compose file can be called whatever you want. Just make sure it is in the `${COMPOSE_FILE}` variable. + +Adding a new command to the Compose File: +```yaml +version: "3" +services: + du: + image: alpine + entrypoint: ["du"] +``` + +>Now you can run `dockerized du` to see the size of the current directory. + +You can also mount a directory to the container: + +```yaml +version: "3" +services: + du: + image: alpine + entrypoint: ["du"] + volumes: + - "${DOCKERIZED_PROJECT_ROOT}/foobar:/foobar" + - "${HOME}/.config:/root/.config" +``` +> Make sure host volumes are **absolute paths**. For paths relative to home and the project root, you can use `${HOME}` and `${DOCKERIZED_PROJECT_ROOT}`. + +> It is possible to use **relative paths** in the service definitions, but then your Compose File must be loaded **before** the default: `COMPOSE_FILE=${DOCKERIZED_PROJECT_ROOT}/docker-compose.yml;${COMPOSE_FILE}`. All paths are relative to the first Compose File. Compose Files are also merged in the order they are specified, with the last Compose File overriding earlier ones. Because of this, if you want to override the default Compose File, you must load it before _your_ file, and you can't use relative paths. + +> To keep **compatibility** with docker-compose, you can specify a default value for `DOCKERIZED_ROOT` with the following syntax`${DOCKERIZED_PROJECT_ROOT:-someDefaultValue}` instead. For example, `${DOCKERIZED_PROJECT_ROOT:-.}` sets the default to `.`, the compose file directory, allowing you to run your command with docker-compose: `docker-compose --rm du -sh /foobar`. + ## Localhost Dockerized applications run within an isolated network. To access services running on your machine, you need to use `host.docker.internal` instead of `localhost`. diff --git a/main_test.go b/main_test.go index f0f6931..bd7f27a 100644 --- a/main_test.go +++ b/main_test.go @@ -5,7 +5,9 @@ import ( dockerized "github.com/datastack-net/dockerized/pkg" "github.com/stretchr/testify/assert" "io/ioutil" + "k8s.io/apimachinery/pkg/util/rand" "os" + "strconv" "strings" "testing" ) @@ -32,10 +34,9 @@ func TestOverrideVersionWithEnvVar(t *testing.T) { } func TestLocalEnvFileOverridesGlobalEnvFile(t *testing.T) { - var homePath = dockerized.GetDockerizedRoot() + "/test/home" var projectPath = dockerized.GetDockerizedRoot() + "/test/project_override_global" defer context(). - WithHome(homePath). + WithTempHome(). WithHomeEnvFile("PROTOC_VERSION=3.6.0"). WithDir(projectPath). WithCwd(projectPath). @@ -46,10 +47,9 @@ func TestLocalEnvFileOverridesGlobalEnvFile(t *testing.T) { } func TestRuntimeEnvOverridesLocalEnvFile(t *testing.T) { - var homePath = dockerized.GetDockerizedRoot() + "/test/home" var projectPath = dockerized.GetDockerizedRoot() + "/test/project_override_global" defer context(). - WithHome(homePath). + WithTempHome(). WithDir(projectPath). WithCwd(projectPath). WithFile(projectPath+"/dockerized.env", "PROTOC_VERSION=3.8.0"). @@ -60,14 +60,10 @@ func TestRuntimeEnvOverridesLocalEnvFile(t *testing.T) { } func TestCustomGlobalComposeFileAdditionalService(t *testing.T) { - homePath := dockerized.GetDockerizedRoot() + "/test/additional_service" - - println(strings.Join(os.Environ(), " / ")) - defer context(). - WithHome(homePath). + WithTempHome(). WithHomeEnvFile(`COMPOSE_FILE="${COMPOSE_FILE};${HOME}/docker-compose.yml"`). - WithFile(homePath+"/docker-compose.yml", ` + WithHomeFile("docker-compose.yml", ` version: "3" services: test: @@ -79,12 +75,10 @@ services: } func TestUserCanGloballyCustomizeDockerizedCommands(t *testing.T) { - homePath := dockerized.GetDockerizedRoot() + "/test/customized_service" - defer context(). - WithHome(homePath). + WithTempHome(). WithHomeEnvFile(`COMPOSE_FILE="${COMPOSE_FILE};${HOME}/docker-compose.yml"`). - WithFile(homePath+"/docker-compose.yml", ` + WithHomeFile("docker-compose.yml", ` version: "3" services: alpine: @@ -98,10 +92,14 @@ services: func TestUserCanLocallyCustomizeDockerizedCommands(t *testing.T) { projectPath := dockerized.GetDockerizedRoot() + "/test/project_with_customized_service" + projectSubPath := projectPath + "/sub" defer context(). - WithCwd(projectPath). - WithHomeEnvFile(`COMPOSE_FILE="${COMPOSE_FILE};docker-compose.yml"`). + WithTempHome(). + WithDir(projectPath). + WithDir(projectSubPath). + WithCwd(projectSubPath). + WithFile(projectPath+"/dockerized.env", `COMPOSE_FILE="${COMPOSE_FILE};${DOCKERIZED_PROJECT_ROOT}/docker-compose.yml"`). WithFile(projectPath+"/docker-compose.yml", ` version: "3" services: @@ -110,29 +108,35 @@ services: CUSTOM: "CUSTOM_123456" `). Restore() - var output = testDockerized(t, []string{"alpine", "env"}) + var output = testDockerized(t, []string{"-v", "alpine", "env"}) assert.Contains(t, output, "CUSTOM_123456") } func (c *Context) WithEnv(key string, value string) *Context { _ = os.Setenv(key, value) - //c.after = append(c.after, func() { - // _ = os.Unsetenv(key) - //}) return c } func (c *Context) WithHome(path string) *Context { c.homePath = path - _ = os.MkdirAll(path, os.ModePerm) + c.WithDir(path) c.WithEnv("HOME", path) c.WithEnv("USERPROFILE", path) return c } +func (c *Context) WithTempHome() *Context { + var homePath = dockerized.GetDockerizedRoot() + "/test/home" + strconv.Itoa(rand.Int()) + c.WithHome(homePath) + return c +} + func (c *Context) WithCwd(path string) *Context { c.cwdBefore, _ = os.Getwd() - os.Chdir(path) + err := os.Chdir(path) + if err != nil { + panic(err) + } c.after = append(c.after, func() { os.Chdir(c.cwdBefore) }) @@ -151,6 +155,10 @@ func (c *Context) WithFile(path string, content string) *Context { return c } +func (c *Context) WithHomeFile(path string, content string) *Context { + return c.WithFile(c.homePath+"/"+path, content) +} + func (c *Context) WithDir(path string) *Context { _ = os.MkdirAll(path, os.ModePerm) c.after = append(c.after, func() { diff --git a/pkg/dockerized.go b/pkg/dockerized.go index 9cf7788..5639e74 100644 --- a/pkg/dockerized.go +++ b/pkg/dockerized.go @@ -269,19 +269,20 @@ func LoadEnvFiles(hostCwd string, optionVerbose bool) error { var envFiles []string // Default - dockerizedEnvFile := GetDockerizedRoot() + "/.env" - envFiles = append(envFiles, dockerizedEnvFile) + defaultEnvFile := GetDockerizedRoot() + "/.env" + envFiles = append(envFiles, defaultEnvFile) // Global overrides homeDir, _ := os.UserHomeDir() - userGlobalDockerizedEnvFile := filepath.Join(homeDir, dockerizedEnvFileName) - if _, err := os.Stat(userGlobalDockerizedEnvFile); err == nil { - envFiles = append(envFiles, userGlobalDockerizedEnvFile) + globalUserEnvFile := filepath.Join(homeDir, dockerizedEnvFileName) + if _, err := os.Stat(globalUserEnvFile); err == nil { + envFiles = append(envFiles, globalUserEnvFile) } - // Local overrides - if localDockerizedEnvFile, err := findLocalEnvFile(hostCwd); err == nil { - envFiles = append(envFiles, localDockerizedEnvFile) + // Project overrides + if projectEnvFile, err := findProjectEnvFile(hostCwd); err == nil { + envFiles = append(envFiles, projectEnvFile) + os.Setenv("DOCKERIZED_PROJECT_ROOT", filepath.Dir(projectEnvFile)) } envFiles = unique(envFiles) @@ -298,22 +299,44 @@ func LoadEnvFiles(hostCwd string, optionVerbose bool) error { } else { return "", false } - }, dockerizedEnvFile) + }, defaultEnvFile) if err != nil { return err } - envMap, err := dotenv.ReadWithLookup(func(key string) (string, bool) { - if dockerizedEnvMap[key] != "" { - return dockerizedEnvMap[key], true - } + var lookupEnvOrDefault = func(key string) (string, bool) { var envValue = os.Getenv(key) if envValue != "" { return envValue, true - } else { - return "", false } - }, envFiles...) + if dockerizedEnvMap[key] != "" { + return dockerizedEnvMap[key], true + } + return "", false + } + + var envMap = make(map[string]string) + err = func() error { + for _, envFilePath := range envFiles { + file, err := os.Open(envFilePath) + if err != nil { + return err + } + defer file.Close() + + envFileMap, err := dotenv.ParseWithLookup(file, func(key string) (string, bool) { + return lookupEnvOrDefault(key) + }) + if err != nil { + return err + } + for key, value := range envFileMap { + envMap[key] = value + } + } + return nil + }() + if err != nil { return err } @@ -372,7 +395,7 @@ func GetDockerizedRoot() string { return filepath.Dir(filepath.Dir(executable)) } -func findLocalEnvFile(path string) (string, error) { +func findProjectEnvFile(path string) (string, error) { envFilePath := "" for i := 0; i < 10; i++ { envFilePath = filepath.Join(path, dockerizedEnvFileName) From 10ec28a1424bd24f032d6a7225db89d1a7ff67e0 Mon Sep 17 00:00:00 2001 From: Bouke Versteegh Date: Mon, 28 Mar 2022 12:27:48 +0200 Subject: [PATCH 11/13] improve documentation --- README.md | 38 +++++++++++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 982f4dd..3077ff7 100644 --- a/README.md +++ b/README.md @@ -258,7 +258,6 @@ To change settings within a specific directory, create a file `dockerized.env` i ```shell COMPOSE_FILE="${COMPOSE_FILE};${DOCKERIZED_PROJECT_ROOT}/docker-compose.yml" ``` -`${DOCKERIZED_PROJECT_ROOT}` contains absolute path to the project directory. In this example it is `myproject`. ### Adding custom commands @@ -266,6 +265,7 @@ Your custom compose file can be called whatever you want. Just make sure it is i Adding a new command to the Compose File: ```yaml +# docker-compose.yml version: "3" services: du: @@ -278,20 +278,52 @@ services: You can also mount a directory to the container: ```yaml +# docker-compose.yml version: "3" services: du: image: alpine entrypoint: ["du"] volumes: - - "${DOCKERIZED_PROJECT_ROOT}/foobar:/foobar" - "${HOME}/.config:/root/.config" + - "${DOCKERIZED_PROJECT_ROOT}/foobar:/foobar" ``` > Make sure host volumes are **absolute paths**. For paths relative to home and the project root, you can use `${HOME}` and `${DOCKERIZED_PROJECT_ROOT}`. > It is possible to use **relative paths** in the service definitions, but then your Compose File must be loaded **before** the default: `COMPOSE_FILE=${DOCKERIZED_PROJECT_ROOT}/docker-compose.yml;${COMPOSE_FILE}`. All paths are relative to the first Compose File. Compose Files are also merged in the order they are specified, with the last Compose File overriding earlier ones. Because of this, if you want to override the default Compose File, you must load it before _your_ file, and you can't use relative paths. -> To keep **compatibility** with docker-compose, you can specify a default value for `DOCKERIZED_ROOT` with the following syntax`${DOCKERIZED_PROJECT_ROOT:-someDefaultValue}` instead. For example, `${DOCKERIZED_PROJECT_ROOT:-.}` sets the default to `.`, the compose file directory, allowing you to run your command with docker-compose: `docker-compose --rm du -sh /foobar`. +> To keep **compatibility** with docker-compose, you can specify a default value for `DOCKERIZED_PROJECT_ROOT`, for example: `${DOCKERIZED_PROJECT_ROOT:-.}` sets the default to `.`, allowing you to run like this as well: `docker-compose --rm du -sh /foobar`. + +To customize existing commands, you can override or add properties to the `services` section of the Compose File, for the command you want to customize. For example, this is how to set an extra environment variable for `dockerized aws`: + +```yaml +# docker-compose.yml +version: "3" +services: + aws: + environment: + AWS_DEFAULT_REGION: "us-east-1" +``` + +If you'd like to pass environment variables directly from your `dockerized.env` file, you can expose the variable as follows: + +```bash +# dockerized.env +AWS_DEFAULT_REGION="us-east-1" +``` + +```yaml +# docker-compose.yml +version: "3" +services: + aws: + environment: + AWS_DEFAULT_REGION: "${AWS_DEFAULT_REGION}" +``` + + + +For more information on extending Compose Files, see the Docker Compose documentation: [Multiple Compose Files](https://docs.docker.com/compose/extends/#multiple-compose-files). Note that the `extends` keyword is not supported in the Docker Compose version used by Dockerized. ## Localhost From 412acdaeb4b40b02a1a417772c02a3d64c3ffb59 Mon Sep 17 00:00:00 2001 From: Bouke Versteegh Date: Mon, 28 Mar 2022 12:29:58 +0200 Subject: [PATCH 12/13] add file names to examples --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 3077ff7..1320e49 100644 --- a/README.md +++ b/README.md @@ -248,6 +248,7 @@ To change global settings, create a file `dockerized.env` in your home directory In this example, the Compose File is also in the home directory, referenced relative to the `${HOME}` directory. The original variable `${COMPOSE_FILE}` is included to preserve the default commands. Omit it if you want to completely replace the default commands. ```bash +# ~/dockerized.env COMPOSE_FILE="${COMPOSE_FILE};${HOME}/docker-compose.yml" ``` @@ -256,6 +257,7 @@ COMPOSE_FILE="${COMPOSE_FILE};${HOME}/docker-compose.yml" To change settings within a specific directory, create a file `dockerized.env` in the root of that directory, which loads the extra Compose File. `${DOCKERIZED_PROJECT_ROOT}` refers to the absolute path to the root of the project. ```shell +# ./dockerized.env COMPOSE_FILE="${COMPOSE_FILE};${DOCKERIZED_PROJECT_ROOT}/docker-compose.yml" ``` @@ -288,6 +290,7 @@ services: - "${HOME}/.config:/root/.config" - "${DOCKERIZED_PROJECT_ROOT}/foobar:/foobar" ``` + > Make sure host volumes are **absolute paths**. For paths relative to home and the project root, you can use `${HOME}` and `${DOCKERIZED_PROJECT_ROOT}`. > It is possible to use **relative paths** in the service definitions, but then your Compose File must be loaded **before** the default: `COMPOSE_FILE=${DOCKERIZED_PROJECT_ROOT}/docker-compose.yml;${COMPOSE_FILE}`. All paths are relative to the first Compose File. Compose Files are also merged in the order they are specified, with the last Compose File overriding earlier ones. Because of this, if you want to override the default Compose File, you must load it before _your_ file, and you can't use relative paths. From ac235910c687f5cb06868c54b17b7ca28975aff9 Mon Sep 17 00:00:00 2001 From: Bouke Versteegh Date: Mon, 28 Mar 2022 12:37:09 +0200 Subject: [PATCH 13/13] documentation --- README.md | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 1320e49..f61ef93 100644 --- a/README.md +++ b/README.md @@ -251,7 +251,11 @@ In this example, the Compose File is also in the home directory, referenced rela # ~/dockerized.env COMPOSE_FILE="${COMPOSE_FILE};${HOME}/docker-compose.yml" ``` - + +```yaml +# ~/docker-compose.yml +``` + **Per Project** To change settings within a specific directory, create a file `dockerized.env` in the root of that directory, which loads the extra Compose File. `${DOCKERIZED_PROJECT_ROOT}` refers to the absolute path to the root of the project. @@ -261,11 +265,14 @@ To change settings within a specific directory, create a file `dockerized.env` i COMPOSE_FILE="${COMPOSE_FILE};${DOCKERIZED_PROJECT_ROOT}/docker-compose.yml" ``` +```yaml +# ./docker-compose.yml +``` + ### Adding custom commands -Your custom compose file can be called whatever you want. Just make sure it is in the `${COMPOSE_FILE}` variable. +After adding your custom Compose File to `COMPOSE_FILE`, you can add your own commands. -Adding a new command to the Compose File: ```yaml # docker-compose.yml version: "3" @@ -275,7 +282,9 @@ services: entrypoint: ["du"] ``` ->Now you can run `dockerized du` to see the size of the current directory. +> Now you can run `dockerized du` to see the size of the current directory. + +> To learn how to support versioning, see [Development Guide: Configurable Version](DEV.md#configurable-version). You can also mount a directory to the container: