Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

using docker-compose from a container when no docker-compose available #243

Merged
merged 11 commits into from
Jan 26, 2021
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
[![Maintainability](https://api.codeclimate.com/v1/badges/1511f826de92d2ab39cc/maintainability)](https://codeclimate.com/github/kool-dev/kool/maintainability)
[<img src="https://img.shields.io/badge/Join%20Slack-kool--dev-orange?logo=slack">](https://join.slack.com/t/kool-dev/shared_invite/zt-jeh36s5g-kVFHUsyLjFENUUH4ucrxPg)

**kool** is a CLI tool that helps bringing down to earth the complexities of modern software development environments - making them lightweight, fast and reproduceable. It takes off the complexity and learning curve of _Docker_ and _Docker Compose_ for local environments, as well as offers a highly simplified interface for leveraging Kubernetes cloud deployment for staging and production deployments.
**kool** is a CLI tool that helps bringing down to earth the complexities of modern software development environments - making them lightweight, fast and reproduceable. It takes off the complexity and learning curve of _Docker_ and _Docker Compose_ for local environments, as well as offers a highly simplified interface for leveraging _Kubernetes_ cloud deployment for staging and production deployments.

Get your local development environment up and running easy and quickly, put time and effort on making a great application, and then leverage the Kool cloud for deploying and sharing your work with the world! This tool is suitable for single developers or large teams, powering them with a simple start and still provide all flexibility the DevOps team needs to tailor up everything.

Expand All @@ -26,7 +26,7 @@ Get your local development environment up and running easy and quickly, put time

## Installation

Kool is powered by [Docker](https://docs.docker.com/get-docker/) and [Docker Compose](https://docs.docker.com/compose/install/), you need to have them already installed on your machine.
Requirements: Kool is powered by [Docker](https://docs.docker.com/get-docker/) so you need to have it already installed on your machine. If you haven't already, please [get Docker first](https://docs.docker.com/get-docker/).

#### For Linux or MacOS

Expand All @@ -42,7 +42,7 @@ Download and run the latest installer from our releases artifacts [here](https:/

## Getting started

It is easy to get started leveraging `kool`. Provided you have all requirements (Docker and Docker Compose).
It is easy to get started leveraging `kool`.

To create a new Laravel project you only need to:

Expand Down
3 changes: 2 additions & 1 deletion cmd/checker/checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package checker

import (
"kool-dev/kool/cmd/builder"
"kool-dev/kool/cmd/compose"
"kool-dev/kool/cmd/shell"
)

Expand All @@ -21,7 +22,7 @@ type DefaultChecker struct {
func NewChecker(s shell.Shell) *DefaultChecker {
return &DefaultChecker{
builder.NewCommand("docker", "info"),
builder.NewCommand("docker-compose", "ps"),
compose.NewDockerCompose("ps"),
s,
}
}
Expand Down
117 changes: 117 additions & 0 deletions cmd/compose/docker-compose.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package compose

import (
"fmt"
"kool-dev/kool/cmd/builder"
"kool-dev/kool/cmd/shell"
"kool-dev/kool/environment"
"os"
"strings"
)

// DockerComposeImage holds the Docker image:tag to use for Docker Compose
const DockerComposeImage = "docker/compose:1.28.0"

// DockerCompose holds data and logic to wrap docker-compose command
// within a container for flexibility
type DockerCompose struct {
builder.Command
localDockerCompose builder.Command
env environment.EnvStorage
sh shell.Shell
isTTY bool
}

// NewDockerCompose creates a new instance of DockerCompose
func NewDockerCompose(cmd string, args ...string) *DockerCompose {
return &DockerCompose{
Command: builder.NewCommand(cmd, args...),
env: environment.NewEnvStorage(),
sh: shell.NewShell(),
localDockerCompose: builder.NewCommand("docker-compose"),
}
}

// SetIsTTY sets whether we are under TTY
func (c *DockerCompose) SetIsTTY(tty bool) *DockerCompose {
c.isTTY = tty

return c
}

// SetShell sets the shell.Shell to be used
func (c *DockerCompose) SetShell(sh shell.Shell) *DockerCompose {
c.sh = sh

return c
}

// SetLocalDockerCompose sets the builder.Command to be used for checking
// docker-compose on PATH
func (c *DockerCompose) SetLocalDockerCompose(cmd builder.Command) *DockerCompose {
c.localDockerCompose = cmd

return c
}

// Args returns the command arguments
func (c *DockerCompose) Args() (args []string) {
if c.sh.LookPath(c.localDockerCompose) == nil {
return append([]string{c.Command.Cmd()}, c.Command.Args()...)
}

args = append(args, "run", "--rm", "-i")

if c.isTTY {
args = append(args, "-t")
}

dockerHost := c.env.Get("DOCKER_HOST")

if dockerHost == "" {
dockerHost = "unix:///var/run/docker.sock"
c.env.Set("DOCKER_HOST", dockerHost)
}

if strings.HasPrefix(dockerHost, "unix://") {
path := strings.TrimPrefix(dockerHost, "unix://")
args = append(args, "-v", fmt.Sprintf("%s:%s", path, path), "-e", "DOCKER_HOST")
} else {
args = append(args, "-e", "DOCKER_HOST", "-e", "DOCKER_TLS_VERIFY", "-e", "DOCKER_CERT_PATH")
}

cwd, _ := os.Getwd()
if cwd != "/" {
args = append(args, "-v", fmt.Sprintf("%s:%s", cwd, cwd))
}
args = append(args, "-w", cwd)
if home := c.env.Get("HOME"); home != "" {
args = append(args, "-v", fmt.Sprintf("%s:%s", home, home), "-e HOME")
}

for _, env := range c.env.All() {
key := strings.SplitN(env, "=", 2)[0]

if key == "PATH" {
continue
}

args = append(args, "-e", key)
}

args = append(args, DockerComposeImage, "-p", c.env.Get("KOOL_NAME"), c.Command.Cmd())
return append(args, c.Command.Args()...)
}

// Cmd returns the command executable
func (c *DockerCompose) Cmd() string {
if c.sh.LookPath(c.localDockerCompose) == nil {
return "docker-compose"
}

return "docker"
}

func (c *DockerCompose) String() string {
return strings.Trim(fmt.Sprintf("%s %s", c.Cmd(), strings.Join(c.Args(), " ")), " ")
}
102 changes: 102 additions & 0 deletions cmd/compose/docker-compose_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package compose

import (
"errors"
"kool-dev/kool/cmd/builder"
"kool-dev/kool/cmd/shell"
"kool-dev/kool/environment"
"os"
"strings"
"testing"
)

func TestNewDockerCompose(t *testing.T) {
dc := NewDockerCompose("cmd", "arg")

if dc.isTTY {
t.Error("unexpected default isTTY value for DockerCompose")
}

dc.SetIsTTY(true)

if !dc.isTTY {
t.Error("failed setting isTTY value for DockerCompose")
}

if _, ok := dc.env.(*environment.DefaultEnvStorage); !ok {
t.Error("unexpected default type for DockerCompose.env")
}

if !strings.HasSuffix(dc.String(), "cmd arg") {
t.Errorf("unexpected DockerCompose.String() suffix: %s", dc.String())
}

dc.SetShell(&shell.FakeShell{})
dc.SetLocalDockerCompose(&builder.FakeCommand{
MockLookPathError: errors.New("some error"),
})
if !strings.HasPrefix(dc.String(), "docker run --rm -i") {
t.Errorf("unexpected DockerCompose.String() prefix: %s", dc.String())
}

dc.SetIsTTY(false)
if strings.Contains(dc.String(), " -t ") {
t.Error("unexpected -t flag when not TTY")
}
dc.SetIsTTY(true)
if !strings.Contains(dc.String(), " -t ") {
t.Error("missing -t flag when on TTY")
}

dc.localDockerCompose = &builder.FakeCommand{
MockLookPathError: nil,
}
if !strings.HasPrefix(dc.String(), "docker-compose") {
t.Errorf("unexpected DockerCompose.String() prefix: %s", dc.String())
}
}

func TestDockerComposeArgsParsing(t *testing.T) {
dc := NewDockerCompose("cmd", "arg")
dc.sh = &shell.FakeShell{}
dc.localDockerCompose = &builder.FakeCommand{
MockLookPathError: errors.New("some error"),
}
dc.env = environment.NewFakeEnvStorage()

dc.env.Set("DOCKER_HOST", "")
if !strings.Contains(dc.String(), "-e DOCKER_HOST") || !strings.Contains(dc.String(), "/var/run/docker.sock") {
t.Error("failed parsing docker flags when DOCKER_HOST is not set")
}
dc.env.Set("DOCKER_HOST", "some-value")
if !strings.Contains(dc.String(), "-e DOCKER_HOST") || !strings.Contains(dc.String(), "-e DOCKER_TLS_VERIFY") {
t.Error("failed parsing docker flags when DOCKER_HOST is set to non-unix:// value")
}

cwd, _ := os.Getwd()
if !strings.Contains(dc.String(), cwd) {
t.Error("missing current working directory")
}

if strings.Contains(dc.String(), "-e HOME") {
t.Error("unexpected passing down HOME empty variable")
}
dc.env.Set("HOME", "/my/home/path")
if !strings.Contains(dc.String(), "-e HOME") || !strings.Contains(dc.String(), "/my/home/path") {
t.Error("missing HOME variable and/or mount")
}

dc.env.Set("PATH", "some-path")
dc.env.Set("SOME_VAR", "some-value")

if strings.Contains(dc.String(), "-e PATH") {
t.Error("unexpected passing down PATH variable")
}
if !strings.Contains(dc.String(), "-e SOME_VAR") {
t.Error("missing passing down SOME_VAR variable")
}

if dc.Cmd() != "docker" {
t.Error("unexpected Cmd() return")
}
}
4 changes: 1 addition & 3 deletions cmd/docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"kool-dev/kool/cmd/builder"
"kool-dev/kool/environment"
"os"
"strings"

"github.com/spf13/cobra"
)
Expand Down Expand Up @@ -47,14 +46,13 @@ func NewKoolDocker() *KoolDocker {

// Execute runs the docker logic with incoming arguments.
func (d *KoolDocker) Execute(args []string) (err error) {
image := args[0]
workDir, _ := os.Getwd()

if d.IsTerminal() {
d.dockerRun.AppendArgs("-t")
}

if asuser := d.envStorage.Get("KOOL_ASUSER"); asuser != "" && (strings.HasPrefix(image, "kooldev") || strings.HasPrefix(image, "fireworkweb")) {
if asuser := d.envStorage.Get("KOOL_ASUSER"); asuser != "" {
d.dockerRun.AppendArgs("--env", "ASUSER="+asuser)
}

Expand Down
23 changes: 19 additions & 4 deletions cmd/exec.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package cmd

import (
"fmt"
"kool-dev/kool/cmd/builder"
"kool-dev/kool/cmd/compose"
"kool-dev/kool/environment"
"strings"

"github.com/spf13/cobra"
)
Expand All @@ -19,7 +22,7 @@ type KoolExec struct {
DefaultKoolService
Flags *KoolExecFlags

envStorage environment.EnvStorage
env environment.EnvStorage
composeExec builder.Command
}

Expand All @@ -38,18 +41,30 @@ func NewKoolExec() *KoolExec {
*newDefaultKoolService(),
&KoolExecFlags{false, []string{}, false},
environment.NewEnvStorage(),
builder.NewCommand("docker-compose", "exec"),
compose.NewDockerCompose("exec"),
}
}

// Execute runs the exec logic with incoming arguments.
func (e *KoolExec) Execute(args []string) (err error) {
if asuser := e.env.Get("KOOL_ASUSER"); asuser != "" {
fabriciojs marked this conversation as resolved.
Show resolved Hide resolved
// we have a KOOL_ASUSER env; now we need to know whether
// the image of the target service have such user
passwd, _ := e.Exec(e.composeExec, args[0], "cat", "/etc/passwd")
// kool:x:UID
if strings.Contains(passwd, fmt.Sprintf("kool:x:%s", asuser)) {
// since user existing within the container, we use it
e.composeExec.AppendArgs("--user", asuser)
}
}

if !e.IsTerminal() {
e.composeExec.AppendArgs("-T")
}

if asuser := e.envStorage.Get("KOOL_ASUSER"); asuser != "" {
e.composeExec.AppendArgs("--user", asuser)
if _, assert := e.composeExec.(*compose.DockerCompose); assert {
// let DockerCompose know about wheter we are under TTY or not
e.composeExec.(*compose.DockerCompose).SetIsTTY(e.IsTerminal())
}

if len(e.Flags.EnvVariables) > 0 {
Expand Down