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..d941058 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: + runs-on: 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/.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 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: diff --git a/README.md b/README.md index 09c44b0..f61ef93 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 @@ -9,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 @@ -154,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. @@ -163,7 +166,7 @@ dockerized node:15 ``` -**Listing versions** +### Listing versions To see which versions are available, run: @@ -173,7 +176,7 @@ dockerized node:? dockerized node: ``` -**Environment Variables** +### Environment Variables Each command has a `_VERSION` environment variable which you can override. @@ -203,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. @@ -229,6 +232,111 @@ 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 +# ~/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. + +```shell +# ./dockerized.env +COMPOSE_FILE="${COMPOSE_FILE};${DOCKERIZED_PROJECT_ROOT}/docker-compose.yml" +``` + +```yaml +# ./docker-compose.yml +``` + +### Adding custom commands + +After adding your custom Compose File to `COMPOSE_FILE`, you can add your own commands. + +```yaml +# docker-compose.yml +version: "3" +services: + du: + image: alpine + entrypoint: ["du"] +``` + +> 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: + +```yaml +# docker-compose.yml +version: "3" +services: + du: + image: alpine + entrypoint: ["du"] + volumes: + - "${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_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 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/bin/dockerized b/bin/dockerized index d3e63c9..aafa2b8 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 @@ -77,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/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/dockerized-banner.png b/dockerized-banner.png new file mode 100644 index 0000000..48e93db Binary files /dev/null and b/dockerized-banner.png differ 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..bd7f27a --- /dev/null +++ b/main_test.go @@ -0,0 +1,238 @@ +package main + +import ( + "fmt" + dockerized "github.com/datastack-net/dockerized/pkg" + "github.com/stretchr/testify/assert" + "io/ioutil" + "k8s.io/apimachinery/pkg/util/rand" + "os" + "strconv" + "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 TestLocalEnvFileOverridesGlobalEnvFile(t *testing.T) { + var projectPath = dockerized.GetDockerizedRoot() + "/test/project_override_global" + defer context(). + WithTempHome(). + 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 projectPath = dockerized.GetDockerizedRoot() + "/test/project_override_global" + defer context(). + WithTempHome(). + 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) { + defer context(). + WithTempHome(). + WithHomeEnvFile(`COMPOSE_FILE="${COMPOSE_FILE};${HOME}/docker-compose.yml"`). + WithHomeFile("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) { + defer context(). + WithTempHome(). + WithHomeEnvFile(`COMPOSE_FILE="${COMPOSE_FILE};${HOME}/docker-compose.yml"`). + WithHomeFile("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) { + projectPath := dockerized.GetDockerizedRoot() + "/test/project_with_customized_service" + projectSubPath := projectPath + "/sub" + + defer context(). + 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: + alpine: + environment: + CUSTOM: "CUSTOM_123456" +`). + Restore() + 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) + return c +} + +func (c *Context) WithHome(path string) *Context { + c.homePath = path + 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() + err := os.Chdir(path) + if err != nil { + panic(err) + } + 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) 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() { + _ = os.RemoveAll(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) + }) + println(output) + 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 57% rename from lib/dockerized.go rename to pkg/dockerized.go index d965e57..5639e74 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() { - normalizeEnvironment() - - 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 == "" { - dockerizedRoot := getDockerizedRoot() - 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,95 @@ 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 - if _, err := os.Stat(userGlobalDockerizedEnvFile); err == nil { - envFiles = append(envFiles, userGlobalDockerizedEnvFile) + // Default + defaultEnvFile := GetDockerizedRoot() + "/.env" + envFiles = append(envFiles, defaultEnvFile) + + // Global overrides + homeDir, _ := os.UserHomeDir() + globalUserEnvFile := filepath.Join(homeDir, dockerizedEnvFileName) + if _, err := os.Stat(globalUserEnvFile); err == nil { + envFiles = append(envFiles, globalUserEnvFile) } - if err == nil && !contains(envFiles, localDockerizedEnvFile) { - 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) + 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 + } + }, defaultEnvFile) + if err != nil { + return err + } + + var lookupEnvOrDefault = func(key string) (string, bool) { + var envValue = os.Getenv(key) + if envValue != "" { + return envValue, true + } + 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 + } + + 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 +361,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 +384,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,17 +395,7 @@ 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) { +func findProjectEnvFile(path string) (string, error) { envFilePath := "" for i := 0; i < 10; i++ { envFilePath = filepath.Join(path, dockerizedEnvFileName) @@ -528,7 +406,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) @@ -546,10 +425,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 @@ -565,17 +442,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) @@ -625,8 +499,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 } @@ -643,7 +517,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 @@ -684,80 +558,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 +}