From b6dd77162f8fa16d3d2504a0142fe7e45d101965 Mon Sep 17 00:00:00 2001 From: David Sauer Date: Tue, 11 Jan 2022 10:29:55 +0100 Subject: [PATCH] add "d8s run" subcommand --- README.md | 15 ++-- main.go | 104 +++++++++++++++++++-------- pkg/down.go | 2 +- pkg/k8s-context.go | 2 +- pkg/run.go | 173 +++++++++++++++++++++++++++++++++++++++++++++ pkg/up.go | 132 +--------------------------------- version.go | 4 +- 7 files changed, 260 insertions(+), 172 deletions(-) create mode 100644 pkg/run.go diff --git a/README.md b/README.md index d2f441fb..06d75dc7 100644 --- a/README.md +++ b/README.md @@ -4,19 +4,16 @@ D8s is spoken dates. -## Example usage +## Example `d8s up tilt up` -`d8s -h` +This will +1. deploys a `Docker in Docker` pod into kubernetes +2. creates a port-forward to `dind` +3. execute the given command (`tilt up`) and sets `DOCKER_HOST=tcp://127.0.0.1:[random_port]` and `DOCKER_BUILDKIT=1` as environment variables -## How it works - -1. It deploys a `Docker in Docker` pod into kubernetes -2. It wait for `dind` to start and creates a port-forward -3. It executes the given command and sets `DOCKER_HOST=tcp://127.0.0.1:[random_port]` and `DOCKER_BUILDKIT=1` as environment variables - -### Pod +### Docker in docker Pod To keep `dind` healthy the pod runs docker and [Nurse](https://github.com/turbine-kreuzberg/dind-nurse). diff --git a/main.go b/main.go index f633bf16..08a271d3 100644 --- a/main.go +++ b/main.go @@ -15,43 +15,24 @@ var ( Usage: "Allowed Kubernetes context name.", EnvVars: []string{"TILT_ALLOW_CONTEXT"}, } -) - -func main() { - up := func(c *cli.Context) error { - ctx := c.Context - allowContext := c.String(allowContext.Name) - args := c.Args() - if !args.Present() { - return fmt.Errorf("command missing") - } - command := args.Slice() - - return d8s.Up(ctx, allowContext, command) - } - down := func(c *cli.Context) error { - ctx := c.Context - allowContext := c.String(allowContext.Name) - - return d8s.Down(ctx, allowContext) - } - version := func(c *cli.Context) error { - return Version() - } - - app := &cli.App{ - Name: "D8s (dates).", - Usage: "A wrapper for docker in docker doing port-forward.", + app = &cli.App{ + Name: "D8s (dates).", + Usage: "A wrapper for docker in docker doing port-forward.", + Description: "Example: d8s up docker run hello-world", Flags: []cli.Flag{ allowContext, }, - Action: up, Commands: []*cli.Command{ { Name: "up", - Usage: "Connect to docker in docker and set DOCKER_HOST for started process.", + Usage: "Deploy and connect to docker in docker and set DOCKER_HOST for started process.", Action: up, }, + { + Name: "run", + Usage: "Connect to docker in docker and set DOCKER_HOST for started process.", + Action: run, + }, { Name: "down", Usage: "Deletes docker in docker deployment.", @@ -60,13 +41,76 @@ func main() { { Name: "version", Usage: "Show the version", - Action: version, + Action: Version, }, }, } +) +func main() { err := app.Run(os.Args) if err != nil { log.Fatal(err) } } + +func verifyContext(c *cli.Context) error { + allowContext := c.String(allowContext.Name) + + allowed, err := d8s.ContextAllowed(allowContext) + if err != nil { + return fmt.Errorf("verify kubernetes context: %v", err) + } + + if !allowed { + return fmt.Errorf("kubernetes context not allowed") + } + + return nil +} + +func up(c *cli.Context) error { + err := verifyContext(c) + if err != nil { + return err + } + + ctx := c.Context + allowContext := c.String(allowContext.Name) + args := c.Args() + if !args.Present() { + return fmt.Errorf("command missing") + } + command := args.Slice() + + return d8s.Up(ctx, allowContext, command) +} + +func run(c *cli.Context) error { + err := verifyContext(c) + if err != nil { + return err + } + + ctx := c.Context + allowContext := c.String(allowContext.Name) + args := c.Args() + if !args.Present() { + return fmt.Errorf("command missing") + } + command := args.Slice() + + return d8s.Run(ctx, allowContext, command) +} + +func down(c *cli.Context) error { + err := verifyContext(c) + if err != nil { + return err + } + + ctx := c.Context + allowContext := c.String(allowContext.Name) + + return d8s.Down(ctx, allowContext) +} diff --git a/pkg/down.go b/pkg/down.go index 1de269e4..5387b975 100644 --- a/pkg/down.go +++ b/pkg/down.go @@ -11,7 +11,7 @@ import ( func Down(ctx context.Context, allowContext string) error { // verify kubernetes context in use - allowed, err := contextAllowed(allowContext) + allowed, err := ContextAllowed(allowContext) if err != nil { return fmt.Errorf("verify kubernetes context: %v", err) } diff --git a/pkg/k8s-context.go b/pkg/k8s-context.go index 08b2b7e5..fb96a25e 100644 --- a/pkg/k8s-context.go +++ b/pkg/k8s-context.go @@ -13,7 +13,7 @@ var ( manifest string ) -func contextAllowed(envVar string) (bool, error) { +func ContextAllowed(envVar string) (bool, error) { contextName, err := kubectlContext() if err != nil { return false, err diff --git a/pkg/run.go b/pkg/run.go new file mode 100644 index 00000000..d76c37c9 --- /dev/null +++ b/pkg/run.go @@ -0,0 +1,173 @@ +package d8s + +import ( + "context" + "fmt" + "log" + "net" + "os" + "os/exec" + "strconv" + "time" +) + +func Run(ctx context.Context, allowContext string, command []string) error { + // verify kubernetes context in use + allowed, err := ContextAllowed(allowContext) + if err != nil { + return fmt.Errorf("verify kubernetes context: %v", err) + } + if !allowed { + return fmt.Errorf("kubernetes context not allowed") + } + + // port forward + err = awaitDind() + if err != nil { + return fmt.Errorf("wait for dind to start: %v", err) + } + + localPort, err := freePort() + if err != nil { + return fmt.Errorf("select free local port: %v", err) + } + + go portForwardForever(ctx, localPort, dindPort) + + err = awaitPortOpen(ctx, localPort) + if err != nil { + return fmt.Errorf("wait for port forward to start: %v", err) + } + + // execute command + err = executeCommand(command, fmt.Sprintf("tcp://127.0.0.1:%d", localPort)) + if err != nil { + return fmt.Errorf("command failed with %s", err) + } + + return nil +} + +func awaitDind() error { + cmd := exec.Command( + "kubectl", + "wait", + "--for=condition=available", + "--timeout=600s", + "deployment/dind", + ) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Stdin = os.Stdin + cmd.Env = os.Environ() + + err := cmd.Run() + if err != nil { + return err + } + + return nil +} + +func freePort() (int, error) { + addr, err := net.ResolveTCPAddr("tcp", "localhost:0") + if err != nil { + return 0, err + } + + l, err := net.ListenTCP("tcp", addr) + if err != nil { + return 0, err + } + defer l.Close() + + return l.Addr().(*net.TCPAddr).Port, nil +} + +func portForwardForever(ctx context.Context, localPort, dindPort int) { + err := portForward(ctx, localPort, dindPort) + if err != nil { + log.Printf("port forward failed: %v", err) + } + + for { + select { + case <-ctx.Done(): + return + case <-time.After(100 * time.Millisecond): + err := portForward(ctx, localPort, dindPort) + if err != nil { + log.Printf("port forward failed: %v", err) + } + } + } +} + +func portForward(ctx context.Context, localPort, dinnerPort int) error { + cmd := exec.Command( + "kubectl", + "port-forward", + "deployment/dind", + fmt.Sprintf("%d:%d", localPort, dinnerPort), + ) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Stdin = os.Stdin + cmd.Env = os.Environ() + + err := cmd.Run() + if err != nil { + return err + } + + return nil +} + +func awaitPortOpen(ctx context.Context, localPort int) error { + for { + select { + case <-ctx.Done(): + return fmt.Errorf("port did not open: %v", ctx.Err()) + case <-time.After(1 * time.Second): + timeout, cancel := context.WithTimeout(ctx, time.Second) + defer cancel() + + open := portOpen(timeout, "127.0.0.1", strconv.Itoa(localPort)) + if open { + return nil + } + } + } +} + +func portOpen(ctx context.Context, host string, port string) bool { + + d := net.Dialer{Timeout: time.Second} + + conn, err := d.DialContext(ctx, "tcp", net.JoinHostPort(host, port)) + if err != nil { + return false + } + defer conn.Close() + + return true +} + +func executeCommand(command []string, dockerAddr string) error { + cmd := exec.Command(command[0], command[1:]...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Stdin = os.Stdin + cmd.Env = os.Environ() + cmd.Env = append(cmd.Env, "DOCKER_HOST="+dockerAddr) + cmd.Env = append(cmd.Env, "DOCKER_BUILDKIT=1") + + fmt.Printf("Execute command %s\n", cmd.String()) + + err := cmd.Run() + if err != nil { + return err + } + + return nil +} diff --git a/pkg/up.go b/pkg/up.go index 9f094162..f6332ee9 100644 --- a/pkg/up.go +++ b/pkg/up.go @@ -3,13 +3,9 @@ package d8s import ( "context" "fmt" - "log" - "net" "os" "os/exec" - "strconv" "strings" - "time" ) const ( @@ -18,7 +14,7 @@ const ( func Up(ctx context.Context, allowContext string, command []string) error { // verify kubernetes context in use - allowed, err := contextAllowed(allowContext) + allowed, err := ContextAllowed(allowContext) if err != nil { return fmt.Errorf("verify kubernetes context: %v", err) } @@ -32,12 +28,12 @@ func Up(ctx context.Context, allowContext string, command []string) error { return fmt.Errorf("deploy dind: %v", err) } + // port forward err = awaitDind() if err != nil { return fmt.Errorf("wait for dind to start: %v", err) } - // port forward localPort, err := freePort() if err != nil { return fmt.Errorf("select free local port: %v", err) @@ -77,127 +73,3 @@ func deployDind() error { return nil } - -func awaitDind() error { - cmd := exec.Command( - "kubectl", - "wait", - "--for=condition=available", - "--timeout=600s", - "deployment/dind", - ) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - cmd.Stdin = os.Stdin - cmd.Env = os.Environ() - - err := cmd.Run() - if err != nil { - return err - } - - return nil -} - -func freePort() (int, error) { - addr, err := net.ResolveTCPAddr("tcp", "localhost:0") - if err != nil { - return 0, err - } - - l, err := net.ListenTCP("tcp", addr) - if err != nil { - return 0, err - } - defer l.Close() - - return l.Addr().(*net.TCPAddr).Port, nil -} - -func portForwardForever(ctx context.Context, localPort, dindPort int) { - err := portForward(ctx, localPort, dindPort) - if err != nil { - log.Printf("port forward failed: %v", err) - } - - for { - select { - case <-ctx.Done(): - return - case <-time.After(100 * time.Millisecond): - err := portForward(ctx, localPort, dindPort) - if err != nil { - log.Printf("port forward failed: %v", err) - } - } - } -} - -func portForward(ctx context.Context, localPort, dinnerPort int) error { - cmd := exec.Command( - "kubectl", - "port-forward", - "deployment/dind", - fmt.Sprintf("%d:%d", localPort, dinnerPort), - ) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - cmd.Stdin = os.Stdin - cmd.Env = os.Environ() - - err := cmd.Run() - if err != nil { - return err - } - - return nil -} - -func awaitPortOpen(ctx context.Context, localPort int) error { - for { - select { - case <-ctx.Done(): - return fmt.Errorf("port did not open: %v", ctx.Err()) - case <-time.After(1 * time.Second): - timeout, cancel := context.WithTimeout(ctx, time.Second) - defer cancel() - - open := portOpen(timeout, "127.0.0.1", strconv.Itoa(localPort)) - if open { - return nil - } - } - } -} - -func portOpen(ctx context.Context, host string, port string) bool { - - d := net.Dialer{Timeout: time.Second} - - conn, err := d.DialContext(ctx, "tcp", net.JoinHostPort(host, port)) - if err != nil { - return false - } - defer conn.Close() - - return true -} - -func executeCommand(command []string, dockerAddr string) error { - cmd := exec.Command(command[0], command[1:]...) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - cmd.Stdin = os.Stdin - cmd.Env = os.Environ() - cmd.Env = append(cmd.Env, "DOCKER_HOST="+dockerAddr) - cmd.Env = append(cmd.Env, "DOCKER_BUILDKIT=1") - - fmt.Printf("Execute command %s\n", cmd.String()) - - err := cmd.Run() - if err != nil { - return err - } - - return nil -} diff --git a/version.go b/version.go index aaad9493..df8d52a4 100644 --- a/version.go +++ b/version.go @@ -2,6 +2,8 @@ package main import ( "fmt" + + "github.com/urfave/cli/v2" ) var ( @@ -10,7 +12,7 @@ var ( date string ) -func Version() error { +func Version(c *cli.Context) error { _, err := fmt.Printf("version: %s\ncommit: %s\nbuilt at: %s\n", version, commit, date) if err != nil { return err