diff --git a/.env b/.env index e5d9c36..9193a1d 100644 --- a/.env +++ b/.env @@ -13,10 +13,10 @@ DEFAULT_BASE_IMAGE_VERSION=${ALPINE_VERSION} DEFAULT_BASE=${DEFAULT_BASE_IMAGE}:${DEFAULT_BASE_IMAGE_VERSION} DOCTL_VERSION=1.70.0 DOLT_VERSION=0.37.9 -DOTNET_VERSION=6.0 +DOTNET_VERSION=6.0@sha256:a2a8f968b043349b8faa0625c5405ac33da70b3274ff9e17109430f16aa9a3ee GIT_VERSION=2.32.0 GH_VERSION=2.5.2 -GO_VERSION=1.17.8 +GO_VERSION=1.17.8-alpine@sha256:c7c94588b6445f5254fbc34df941afa10de04706deb330e62831740c9f0f2030 HASKELL_VERSION=9.2.2 GHCI_VERSION=${HASKELL_VERSION} HELM_VERSION=3.8.1 @@ -35,10 +35,10 @@ PROTOC_ARCH=${DEFAULT_ARCH} PROTOC_BASE=${DEFAULT_BASE} POSTGRES_VERSION=14.2 PSQL_VERSION=${POSTGRES_VERSION} -PYTHON_VERSION=3.10 +PYTHON_VERSION=3.10-alpine PYTHON2_VERSION=2.7.18 -RUBY_VERSION=3.1.1 -RUST_VERSION=1.59.0 +RUBY_VERSION=3.1.1-alpine3.14@sha256:e5a66799d2e82e1434ae4565842bcfd03bc0c2fb4394811ebedcae967fc77919 +RUST_VERSION=1.59.0-alpine3.14@sha256:86d063b20c2214fe5292683e9386f178462405880fb9507c0e765482eb126789 RUSTC_VERSION=${RUST_VERSION} S3CMD_BASE=python:${PYTHON_VERSION}-${DEFAULT_BASE_IMAGE} S3CMD_VERSION=2.2.0 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 351bb16..c6fb68a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,12 +9,18 @@ on: jobs: ShellTest: strategy: - fail-fast: true + fail-fast: false matrix: command: - - go - aws + - dotnet + - go + - java + - node + - perl + - php - python + - ruby - rustc - zip runs-on: ubuntu-latest @@ -23,11 +29,11 @@ jobs: uses: actions/checkout@v2 - name: Compile run: bin/dockerized --compile - - name: "Test: dockerized --shell ${{matrix.command}}" - env: - COMMAND: "${{matrix.command}}" + - name: "dockerized --shell ${{matrix.command}}" + run: bin/dockerized -v --shell ${{matrix.command}} -c env | tee ~/shell.log + - name: "Assert" run: | - bin/dockerized --shell $COMMAND -c 'echo $HOST_HOSTNAME' | tee ~/shell.log + echo "Test --shell" grep $(hostname) ~/shell.log IntegrationTest: runs-on: ubuntu-latest diff --git a/bin/dockerized b/bin/dockerized index aafa2b8..c0e31d6 100755 --- a/bin/dockerized +++ b/bin/dockerized @@ -87,12 +87,13 @@ if [ "$DOCKERIZED_COMPILE" ] || [ ! -f "$DOCKERIZED_BINARY" ]; then fi if [ $? -ne 0 ]; then - echo "Failed to compile dockerized" >&2 + echo "Failed to compile dockerized." >&2 exit 1 fi + echo "Compiled dockerized." >&2 + if [ $# -eq 0 ]; then - echo "Compiled dockerized" >&2 exit 0 fi fi diff --git a/docker-compose.yml b/docker-compose.yml index a5da840..ccba012 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -77,7 +77,7 @@ services: - "${HOME:-home}/.dockerized/apps/dolt:/root" entrypoint: [ "dolt" ] dotnet: - image: "mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION}-alpine" + image: "mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION}" entrypoint: [ "dotnet" ] gh: image: "gh:${GH_VERSION}" @@ -108,6 +108,8 @@ services: environment: GOOS: "${GOOS:-}" GOARCH: "${GOARCH:-}" + labels: + net.dockerized.shell: "/bin/bash" gofmt: <<: *go entrypoint: [ "gofmt" ] @@ -153,6 +155,8 @@ services: perl: image: perl:${PERL_VERSION} entrypoint: [ "perl" ] + labels: + net.dockerized.shell: "${PERL_SHELL:-/bin/bash}" php: image: "php:${PHP_VERSION}" psql: diff --git a/main.go b/main.go index 50d2562..948bb22 100644 --- a/main.go +++ b/main.go @@ -5,9 +5,10 @@ import ( "github.com/compose-spec/compose-go/types" dockerized "github.com/datastack-net/dockerized/pkg" "github.com/datastack-net/dockerized/pkg/help" + "github.com/datastack-net/dockerized/pkg/labels" util "github.com/datastack-net/dockerized/pkg/util" "github.com/docker/compose/v2/pkg/api" - "github.com/fatih/color" + "github.com/moby/term" "os" "path/filepath" "strings" @@ -33,8 +34,12 @@ func RunCli(args []string) (err error, exitCode int) { var optionVerbose = hasKey(dockerizedOptions, "--verbose") || hasKey(dockerizedOptions, "-v") var optionShell = hasKey(dockerizedOptions, "--shell") var optionBuild = hasKey(dockerizedOptions, "--build") + var optionPull = hasKey(dockerizedOptions, "--pull") var optionVersion = hasKey(dockerizedOptions, "--version") + var optionDigest = hasKey(dockerizedOptions, "--digest") var optionPort = hasKey(dockerizedOptions, "-p") + var optionEntrypoint = hasKey(dockerizedOptions, "--entrypoint") + var optionCommands = hasKey(dockerizedOptions, "--commands") dockerizedRoot := dockerized.GetDockerizedRoot() dockerized.NormalizeEnvironment(dockerizedRoot) @@ -61,7 +66,7 @@ func RunCli(args []string) (err error, exitCode int) { } if commandName == "" || optionHelp { - err := help.Help(composeFilePaths) + err := help.Help() if err != nil { return err, 1 } @@ -74,7 +79,7 @@ func RunCli(args []string) (err error, exitCode int) { if commandVersion != "" { if commandVersion == "?" { - err = dockerized.PrintCommandVersions(composeFilePaths, commandName, optionVerbose) + err = dockerized.PrintCommandVersions(commandName, optionVerbose) if err != nil { return err, 1 } else { @@ -85,7 +90,7 @@ func RunCli(args []string) (err error, exitCode int) { } } - project, err := dockerized.GetProject(composeFilePaths) + project, err := dockerized.GetProject() if err != nil { return err, 1 } @@ -97,6 +102,12 @@ func RunCli(args []string) (err error, exitCode int) { containerCwd += "/" + hostCwdDirName } + if optionCommands { + for _, service := range project.Services { + fmt.Printf("%s\n", service.Name) + } + return nil, 0 + } runOptions := api.RunOptions{ Service: commandName, Environment: []string{ @@ -104,7 +115,7 @@ func RunCli(args []string) (err error, exitCode int) { }, Command: commandArgs, AutoRemove: true, - Tty: true, + Tty: term.IsTerminal(os.Stdout.Fd()), WorkingDir: containerCwd, } @@ -142,29 +153,47 @@ func RunCli(args []string) (err error, exitCode int) { if optionVerbose { fmt.Printf("Building container image for %s...\n", commandName) } - err := dockerized.DockerComposeBuild(composeFilePaths, api.BuildOptions{ + err := dockerized.DockerComposeBuild(api.BuildOptions{ Services: []string{commandName}, }) if err != nil { return err, 1 } + } else if optionPull { + err = dockerized.Pull(commandName) + if err != nil { + return err, 1 + } + if !optionDigest { + return nil, 0 + } + } + + if optionDigest { + digest, err := dockerized.GetDigest(commandName) + + if err != nil { + return err, 0 + } + fmt.Println(digest) + return nil, 0 + } + + if optionShell && optionEntrypoint { + return fmt.Errorf("--shell and --entrypoint are mutually exclusive"), 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) - } + fmt.Printf("Setting up shell in container for %s...\n", commandName) } - var ps1 = fmt.Sprintf( - "%s %s:\\w \\$ ", - color.BlueString("dockerized %s", commandName), - color.New(color.FgHiBlue).Add(color.Bold).Sprintf("\\u@\\h"), - ) + //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" @@ -179,30 +208,26 @@ func RunCli(args []string) (err error, exitCode int) { } welcomeMessage = strings.ReplaceAll(welcomeMessage, "\\", "\\\\") - shells := []string{ - "bash", - "zsh", - "sh", - } - var shellDetectionCommands []string - for _, shell := range shells { - shellDetectionCommands = append(shellDetectionCommands, "command -v "+shell) + var shell = "sh" + if service.Labels[labels.Shell] != "" { + shell = service.Labels[labels.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"} + runOptions.Entrypoint = []string{shell} + runOptions.Command = commandArgs + } - 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 optionEntrypoint { + var entrypoint = dockerizedOptions["--entrypoint"] + if optionVerbose { + fmt.Printf("Setting entrypoint to %s\n", entrypoint) } + runOptions.Entrypoint = strings.Split(entrypoint, " ") + } + + if optionVerbose { + fmt.Printf("Entrypoint: %s\n", runOptions.Entrypoint) + fmt.Printf("Command: %s\n", runOptions.Command) } if !contains(project.ServiceNames(), commandName) { @@ -215,16 +240,20 @@ func RunCli(args []string) (err error, exitCode int) { return dockerized.DockerRun(image, runOptions, volumes) } - return dockerized.DockerComposeRun(project, runOptions, volumes, serviceOptions...) + return dockerized.DockerComposeRun(project, runOptions, volumes, optionVerbose, serviceOptions...) } func parseArguments(args []string) (map[string]string, string, string, []string) { var options = []string{ - "--shell", "--build", "-h", "--help", "-p", + "--pull", + "--digest", + "--commands", + "--shell", + "--entrypoint", "-v", "--verbose", "--version", @@ -232,6 +261,7 @@ func parseArguments(args []string) (map[string]string, string, string, []string) var optionsWithParameters = []string{ "-p", + "--entrypoint", } commandName := "" diff --git a/main_test.go b/main_test.go index ebd6c34..3ef95b3 100644 --- a/main_test.go +++ b/main_test.go @@ -23,6 +23,7 @@ type Context struct { } func TestHelp(t *testing.T) { + defer context().Restore() output := testDockerized(t, []string{"--help"}) assert.Contains(t, output, "Usage:") } @@ -245,6 +246,21 @@ func TestOverrideVersionWithGlobalEnvFile(t *testing.T) { assert.Contains(t, output, "3.8.0") } +func TestEnvironmentHostName(t *testing.T) { + defer context().Restore() + expectedHostName, err := os.Hostname() + assert.Nil(t, err) + assert.NotNil(t, expectedHostName) + var output = testDockerized(t, []string{"--shell", "go", "-c", "echo $HOST_HOSTNAME"}) + assert.Contains(t, output, expectedHostName) +} + +func TestShell(t *testing.T) { + defer context().Restore() + var output = testDockerized(t, []string{"--shell", "go", "--version"}) + assert.Contains(t, output, "GNU bash") +} + func capture(callback func()) string { read, write, _ := os.Pipe() os.Stdout = write diff --git a/pkg/dockerized.go b/pkg/dockerized.go index d93f240..c8ea7c3 100644 --- a/pkg/dockerized.go +++ b/pkg/dockerized.go @@ -4,6 +4,7 @@ import ( "encoding/json" "github.com/compose-spec/compose-go/dotenv" "github.com/datastack-net/dockerized/pkg/util" + "github.com/docker/docker/client" "github.com/hashicorp/go-version" "io" "net/http" @@ -30,7 +31,7 @@ import ( "syscall" ) -// Determine which docker-compose file to use. Assumes .env files are already loaded. +// GetComposeFilePaths 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") @@ -82,8 +83,8 @@ func getNpmPackageVersions(packageName string) ([]string, error) { return versionKeys, nil } -func PrintCommandVersions(composeFilePaths []string, commandName string, verbose bool) error { - project, err := GetProject(composeFilePaths) +func PrintCommandVersions(commandName string, verbose bool) error { + project, err := GetProject() if err != nil { return err } @@ -269,7 +270,7 @@ func LoadEnvFiles(hostCwd string, optionVerbose bool) error { var envFiles []string // Default - defaultEnvFile := GetDockerizedRoot() + "/.env" + defaultEnvFile := filepath.Join(GetDockerizedRoot(), ".env") envFiles = append(envFiles, defaultEnvFile) // Global overrides @@ -282,7 +283,10 @@ func LoadEnvFiles(hostCwd string, optionVerbose bool) error { // Project overrides if projectEnvFile, err := findProjectEnvFile(hostCwd); err == nil { envFiles = append(envFiles, projectEnvFile) - os.Setenv("DOCKERIZED_PROJECT_ROOT", filepath.Dir(projectEnvFile)) + err := os.Setenv("DOCKERIZED_PROJECT_ROOT", filepath.Dir(projectEnvFile)) + if err != nil { + return err + } } envFiles = unique(envFiles) @@ -354,7 +358,7 @@ func dockerComposeRunAdHocService(service types.ServiceConfig, runOptions api.Ru service, }, WorkingDir: GetDockerizedRoot(), - }, runOptions, []types.ServiceVolumeConfig{}) + }, runOptions, []types.ServiceVolumeConfig{}, false) } func DockerRun(image string, runOptions api.RunOptions, volumes []types.ServiceVolumeConfig) (error, int) { @@ -429,7 +433,7 @@ func getRawProject(composeFilePaths []string) (*types.Project, error) { return cli.ProjectFromOptions(options) } -func GetProject(composeFilePaths []string) (*types.Project, error) { +func GetProject() (*types.Project, error) { options, err := cli.NewProjectOptions([]string{}, cli.WithOsEnv, cli.WithConfigFileEnv, @@ -442,23 +446,59 @@ func GetProject(composeFilePaths []string) (*types.Project, error) { return cli.ProjectFromOptions(options) } -func dockerComposeUpNetworkOnly(backend *api.ServiceProxy, ctx context.Context, project *types.Project) error { +func dockerComposeUpPrepare(backend *api.ServiceProxy, ctx context.Context, project types.Project) error { project.Services = []types.ServiceConfig{} - upOptions := api.UpOptions{ - Create: api.CreateOptions{ - Services: []string{}, - RemoveOrphans: true, - Recreate: "always", - }, + return backend.Create(ctx, &project, api.CreateOptions{}) +} + +func GetDigest(serviceName string) (string, error) { + project, err := GetProject() + if err != nil { + return "", err } - err := backend.Up(ctx, project, upOptions) - // docker compose up will return error if there is no service to start, but the network will have been created. - expectedErrorMessage := "no container found for project \"" + project.Name + "\": not found" - if err == nil || api.IsNotFoundError(err) && err.Error() == expectedErrorMessage { - return nil + service, err := project.GetService(serviceName) + if err != nil { + return "", err + } + + dockerCli, err := getDockerCli() + if err != nil { + return "", err + } + + ctx, _ := newSigContext() + data, _, err := dockerCli.Client().ImageInspectWithRaw(ctx, service.Image) + + if err != nil { + if client.IsErrNotFound(err) { + return "", fmt.Errorf("%w\nTry --pull", err) + } + return "", err + } + + return data.RepoDigests[0], nil +} + +func Pull(serviceName string) error { + project, err := GetProject() + + service, err := project.GetService(serviceName) + if err != nil { + return err } - return err + project.Services = []types.ServiceConfig{service} + + backend, err := getBackend() + if err != nil { + return err + } + + ctx, _ := newSigContext() + return backend.Pull(ctx, project, api.PullOptions{ + Quiet: false, + IgnoreFailures: false, + }) } func getDockerCli() (*command.DockerCli, error) { @@ -486,8 +526,8 @@ func getBackend() (*api.ServiceProxy, error) { return backend, nil } -func DockerComposeBuild(composeFilePaths []string, buildOptions api.BuildOptions) error { - project, err := GetProject(composeFilePaths) +func DockerComposeBuild(buildOptions api.BuildOptions) error { + project, err := GetProject() if err != nil { return err } @@ -504,7 +544,7 @@ func DockerComposeBuild(composeFilePaths []string, buildOptions api.BuildOptions return backend.Build(ctx, project, buildOptions) } -func DockerComposeRun(project *types.Project, runOptions api.RunOptions, volumes []types.ServiceVolumeConfig, serviceOptions ...func(config *types.ServiceConfig) error) (error, int) { +func DockerComposeRun(project *types.Project, runOptions api.RunOptions, volumes []types.ServiceVolumeConfig, verbose bool, serviceOptions ...func(config *types.ServiceConfig) error) (error, int) { err := os.Chdir(project.WorkingDir) if err != nil { return err, 1 @@ -536,15 +576,34 @@ func DockerComposeRun(project *types.Project, runOptions api.RunOptions, volumes return err, 1 } - err = dockerComposeUpNetworkOnly(backend, ctx, project) + if verbose { + fmt.Println("Preparing compose environment...") + } + err = dockerComposeUpPrepare(backend, ctx, *project) if err != nil { return err, 1 } project.Services = []types.ServiceConfig{service} + if verbose { + fmt.Println("Running one-off container...") + } + exitCode, err := backend.RunOneOffContainer(ctx, project, runOptions) + + if verbose { + fmt.Printf("Container exited with code %d.\n", exitCode) + if err != nil { + fmt.Printf("Error: %s\n", err.Error()) + } + } + if err != nil { + if exitCode == 0 { + exitCode = 1 + } + return err, exitCode } if exitCode != 0 { diff --git a/pkg/help/help.go b/pkg/help/help.go index a1713e5..258c7b0 100644 --- a/pkg/help/help.go +++ b/pkg/help/help.go @@ -6,8 +6,8 @@ import ( "sort" ) -func Help(composeFilePaths []string) error { - project, err := dockerized.GetProject(composeFilePaths) +func Help() error { + project, err := dockerized.GetProject() if err != nil { return err } @@ -31,6 +31,9 @@ func Help(composeFilePaths []string) error { fmt.Println("Options:") fmt.Println(" --build Rebuild the container before running it.") + fmt.Println(" --commands List all available commands.") + fmt.Println(" --entrypoint ") + fmt.Println(" Override the default entrypoint of the command container.") fmt.Println(" --shell Start a shell inside the command container. Similar to `docker run --entrypoint=sh`.") fmt.Println(" -p Exposes given port to host, e.g. -p 8080") fmt.Println(" -p : Maps host port to container port, e.g. -p 80:8080") diff --git a/pkg/labels/labels.go b/pkg/labels/labels.go new file mode 100644 index 0000000..2cdbe4b --- /dev/null +++ b/pkg/labels/labels.go @@ -0,0 +1,3 @@ +package labels + +const Shell = "net.dockerized.shell"