From c6118a68488693c24c056b9b1ce40e80ad5c5944 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 5 Feb 2025 15:33:53 -0500 Subject: [PATCH] less files --- container.go | 20 -- container_config.go | 13 -- container_runtime.go | 58 +++++- image.go => image_refs.go | 17 -- mount.go | 18 -- runtime/docker/build_image.go | 78 -------- runtime/docker/container.go | 262 +++++++++++++++++++++++- runtime/docker/container_exec.go | 121 ----------- runtime/docker/container_kill.go | 10 - runtime/docker/container_remove.go | 13 -- runtime/docker/container_restart.go | 19 -- runtime/docker/container_run.go | 100 --------- runtime/docker/container_runtime.go | 278 +++++++++++++++++++++++++- runtime/docker/container_start.go | 11 - runtime/docker/container_stop.go | 19 -- runtime/docker/copy_from_container.go | 12 -- runtime/docker/copy_to_container.go | 17 -- runtime/docker/create_container.go | 170 ---------------- runtime/docker/get_container.go | 15 -- runtime/docker/pull_image.go | 37 ---- 20 files changed, 593 insertions(+), 695 deletions(-) delete mode 100644 container.go delete mode 100644 container_config.go rename image.go => image_refs.go (59%) delete mode 100644 mount.go delete mode 100644 runtime/docker/build_image.go delete mode 100644 runtime/docker/container_exec.go delete mode 100644 runtime/docker/container_kill.go delete mode 100644 runtime/docker/container_remove.go delete mode 100644 runtime/docker/container_restart.go delete mode 100644 runtime/docker/container_run.go delete mode 100644 runtime/docker/container_start.go delete mode 100644 runtime/docker/container_stop.go delete mode 100644 runtime/docker/copy_from_container.go delete mode 100644 runtime/docker/copy_to_container.go delete mode 100644 runtime/docker/create_container.go delete mode 100644 runtime/docker/get_container.go delete mode 100644 runtime/docker/pull_image.go diff --git a/container.go b/container.go deleted file mode 100644 index ec28b71a..00000000 --- a/container.go +++ /dev/null @@ -1,20 +0,0 @@ -package forge - -import ( - "context" - "io" -) - -// Container represents a container created by a ContainerRuntime. -type Container interface { - GetID() string - CopyTo(context.Context, string, io.Reader) error - CopyFrom(context.Context, string) (io.ReadCloser, error) - Run(context.Context, *Streams) (int, error) - Start(context.Context) error - Restart(context.Context) error - Exec(context.Context, *ContainerConfig, *Streams) (int, error) - Stop(context.Context) error - Remove(context.Context) error - Kill(context.Context) error -} diff --git a/container_config.go b/container_config.go deleted file mode 100644 index d295ffca..00000000 --- a/container_config.go +++ /dev/null @@ -1,13 +0,0 @@ -package forge - -// ContainerConfig is the configuration that is used to -// create a container or an exec in a running container. -type ContainerConfig struct { - Entrypoint []string - Cmd []string - WorkingDir string - Env []string - User string - Privileged bool - Mounts []Mount -} diff --git a/container_runtime.go b/container_runtime.go index 679c56ab..a7e4b728 100644 --- a/container_runtime.go +++ b/container_runtime.go @@ -1,6 +1,62 @@ package forge -import "context" +import ( + "context" + "io" + + xslice "github.com/frantjc/x/slice" + "github.com/opencontainers/go-digest" + imagespecsv1 "github.com/opencontainers/image-spec/specs-go/v1" +) + +type Mount struct { + Source string `json:"source,omitempty"` + Destination string `json:"destination,omitempty"` +} + +func overrideMounts(oldMounts []Mount, newMounts ...Mount) []Mount { + return append(xslice.Filter(oldMounts, func(m Mount, _ int) bool { + return !xslice.Some(newMounts, func(n Mount, _ int) bool { + return m.Destination == n.Destination + }) + }), newMounts...) +} + +// ContainerConfig is the configuration that is used to +// create a container or an exec in a running container. +type ContainerConfig struct { + Entrypoint []string + Cmd []string + WorkingDir string + Env []string + User string + Privileged bool + Mounts []Mount +} + +// Container represents a container created by a ContainerRuntime. +type Container interface { + GetID() string + CopyTo(context.Context, string, io.Reader) error + CopyFrom(context.Context, string) (io.ReadCloser, error) + Run(context.Context, *Streams) (int, error) + Start(context.Context) error + Restart(context.Context) error + Exec(context.Context, *ContainerConfig, *Streams) (int, error) + Stop(context.Context) error + Remove(context.Context) error + Kill(context.Context) error +} + +// Image represents a image pulled by a ContainerRuntime. +// Used to create Containers from. +type Image interface { + Manifest() (*imagespecsv1.Manifest, error) + Config() (*imagespecsv1.ImageConfig, error) + Digest() (digest.Digest, error) + Blob() io.Reader + Name() string +} // ContainerRuntime represents the functionality needed by Runnables // to pull OCI images and run containers when being processed. diff --git a/image.go b/image_refs.go similarity index 59% rename from image.go rename to image_refs.go index 6cfaebd5..5fb5cb2e 100644 --- a/image.go +++ b/image_refs.go @@ -1,22 +1,5 @@ package forge -import ( - "io" - - "github.com/opencontainers/go-digest" - imagespecsv1 "github.com/opencontainers/image-spec/specs-go/v1" -) - -// Image represents a image pulled by a ContainerRuntime. -// Used to create Containers from. -type Image interface { - Manifest() (*imagespecsv1.Manifest, error) - Config() (*imagespecsv1.ImageConfig, error) - Digest() (digest.Digest, error) - Blob() io.Reader - Name() string -} - const ( DefaultNode10ImageReference = "docker.io/library/node:10" DefaultNode12ImageReference = "docker.io/library/node:12" diff --git a/mount.go b/mount.go deleted file mode 100644 index 9a527d8f..00000000 --- a/mount.go +++ /dev/null @@ -1,18 +0,0 @@ -package forge - -import ( - xslice "github.com/frantjc/x/slice" -) - -type Mount struct { - Source string `json:"source,omitempty"` - Destination string `json:"destination,omitempty"` -} - -func overrideMounts(oldMounts []Mount, newMounts ...Mount) []Mount { - return append(xslice.Filter(oldMounts, func(m Mount, _ int) bool { - return !xslice.Some(newMounts, func(n Mount, _ int) bool { - return m.Destination == n.Destination - }) - }), newMounts...) -} diff --git a/runtime/docker/build_image.go b/runtime/docker/build_image.go deleted file mode 100644 index 0df9cdea..00000000 --- a/runtime/docker/build_image.go +++ /dev/null @@ -1,78 +0,0 @@ -package docker - -import ( - "context" - "io" - "path/filepath" - - "github.com/docker/cli/cli/command/image/build" - "github.com/docker/docker/api/types" - "github.com/docker/docker/pkg/archive" - "github.com/docker/docker/pkg/idtools" - "github.com/docker/docker/pkg/jsonmessage" - "github.com/frantjc/forge" - "github.com/google/go-containerregistry/pkg/name" - "github.com/google/go-containerregistry/pkg/v1/daemon" -) - -func (d *ContainerRuntime) BuildDockerfile(ctx context.Context, dockerfile, reference string) (forge.Image, error) { - ref, err := name.ParseReference(reference) - if err != nil { - return nil, err - } - - dir := filepath.Dir(dockerfile) - - excludes, err := build.ReadDockerignore(dir) - if err != nil { - return nil, err - } - - buildCtx, err := archive.TarWithOptions(dir, &archive.TarOptions{ - ExcludePatterns: excludes, - ChownOpts: &idtools.Identity{UID: 0, GID: 0}, - }) - if err != nil { - return nil, err - } - - if bc, err := build.Compress(buildCtx); err == nil { - buildCtx = bc - } - - ibr, err := d.Client.ImageBuild(ctx, buildCtx, types.ImageBuildOptions{ - Tags: []string{ref.Name()}, - Dockerfile: filepath.Base(dockerfile), - PullParent: true, - Remove: true, - }) - if err != nil { - return nil, err - } - - if err := jsonmessage.DisplayJSONMessagesStream(ibr.Body, io.Discard, 0, false, nil); err != nil { - if jerr, ok := err.(*jsonmessage.JSONError); ok { - return nil, jerr - } - - return nil, err - } - - if _, err = io.Copy(io.Discard, ibr.Body); err != nil { - return nil, err - } - - if err = ibr.Body.Close(); err != nil { - return nil, err - } - - img, err := daemon.Image(ref, daemon.WithClient(d), daemon.WithContext(ctx)) - if err != nil { - return nil, err - } - - return &Image{ - Image: img, - Reference: ref, - }, nil -} diff --git a/runtime/docker/container.go b/runtime/docker/container.go index 0139e9d9..1bf42836 100644 --- a/runtime/docker/container.go +++ b/runtime/docker/container.go @@ -1,6 +1,20 @@ package docker -import "github.com/docker/docker/client" +import ( + "context" + "fmt" + "io" + "os" + "path/filepath" + "time" + + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/client" + "github.com/docker/docker/pkg/stdcopy" + "github.com/frantjc/forge" + "github.com/frantjc/forge/changroup" + "github.com/moby/term" +) type Container struct { ID string @@ -14,3 +28,249 @@ func (c *Container) GetID() string { func (c *Container) GoString() string { return "&Container{" + c.GetID() + "}" } + +func (c *Container) CopyTo(ctx context.Context, destination string, content io.Reader) error { + if rc, ok := content.(io.ReadCloser); ok { + defer rc.Close() + } + + return c.CopyToContainer(ctx, c.ID, filepath.Clean(destination), content, container.CopyToContainerOptions{}) +} + +func (c *Container) CopyFrom(ctx context.Context, source string) (io.ReadCloser, error) { + rc, _, err := c.CopyFromContainer(ctx, c.ID, filepath.Clean(source)) + return rc, err +} + +func (c *Container) Start(ctx context.Context) error { + return c.Client.ContainerStart(ctx, c.ID, container.StartOptions{}) +} + +func (c *Container) Run(ctx context.Context, streams *forge.Streams) (int, error) { + var ( + stdin io.Reader + stdout, stderr io.Writer + detachKeys string + tty bool + ) + if streams != nil { + stdin = streams.In + stdout = streams.Out + stderr = streams.Err + tty = streams.Tty + if tty { + stderr = stdout + } + detachKeys = streams.DetachKeys + } + + hjr, err := c.ContainerAttach(ctx, c.ID, container.AttachOptions{ + Stream: streams != nil, + Stdin: stdin != nil, + Stdout: stdout != nil, + Stderr: stderr != nil, + DetachKeys: detachKeys, + }) + if err != nil { + return -1, err + } + + errC := make(chan error, 1) + go func() { + if tty { + _, err = io.Copy(stdout, hjr.Reader) + } else { + _, err = stdcopy.StdCopy( + stdout, + stderr, + hjr.Reader, + ) + } + if err != nil { + errC <- err + } + }() + + if stdin != nil { + if detachKeys != "" { + detachKeysB, err := term.ToBytes(detachKeys) + if err != nil { + return -1, err + } + + stdin = term.NewEscapeProxy(stdin, detachKeysB) + } + + go func() { + if _, err = io.Copy(hjr.Conn, stdin); err != nil { + errC <- err + } + + if err = hjr.CloseWrite(); err != nil { + errC <- hjr.CloseWrite() + } + }() + } + + if err = c.ContainerStart(ctx, c.ID, container.StartOptions{}); err != nil { + return -1, err + } + + cwokbC, waitErrC := c.ContainerWait(ctx, c.ID, container.WaitConditionNotRunning) + + select { + case cwokb := <-cwokbC: + if cwokb.Error != nil { + err = fmt.Errorf("%s", cwokb.Error.Message) + } + + return int(cwokb.StatusCode), err + case err = <-errC: + return -1, err + case err = <-waitErrC: + return -1, err + case <-ctx.Done(): + return -1, ctx.Err() + } +} + +func (c *Container) Exec(ctx context.Context, containerConfig *forge.ContainerConfig, streams *forge.Streams) (int, error) { + var ( + stdin io.Reader + stdout, stderr io.Writer + tty bool + detachKeys string + ) + if streams != nil { + stdin = streams.In + stdout = streams.Out + stderr = streams.Err + tty = streams.Tty + if tty { + stderr = stdout + } + detachKeys = streams.DetachKeys + } + + idr, err := c.Client.ContainerExecCreate(ctx, c.ID, container.ExecOptions{ + User: containerConfig.User, + Privileged: containerConfig.Privileged, + Env: containerConfig.Env, + WorkingDir: containerConfig.WorkingDir, + Cmd: append(containerConfig.Entrypoint, containerConfig.Cmd...), + Tty: tty, + DetachKeys: detachKeys, + AttachStdin: stdin != nil, + AttachStdout: stdout != nil, + AttachStderr: stderr != nil, + }) + if err != nil { + return -1, err + } + + hjr, err := c.Client.ContainerExecAttach(ctx, idr.ID, container.ExecStartOptions{ + Tty: tty, + }) + if err != nil { + return -1, err + } + defer hjr.Close() + + errC := make(chan error, 1) + outC := make(chan any, 1) + go func() { + if tty { + _, err = io.Copy(stdout, hjr.Reader) + } else { + // "exit status 1" comes from here + _, err = stdcopy.StdCopy( + stdout, + stderr, + hjr.Reader, + ) + } + if err != nil { + errC <- err + } + + go close(outC) + }() + + inC := make(chan any, 1) + if stdin != nil { + if detachKeys != "" { + detachKeysB, err := term.ToBytes(detachKeys) + if err != nil { + return -1, err + } + + stdin = term.NewEscapeProxy(stdin, detachKeysB) + } + + go func() { + if _, err = io.Copy(hjr.Conn, stdin); err != nil { + errC <- err + } + + if err = hjr.CloseWrite(); err != nil { + errC <- err + } + + go close(inC) + }() + } else { + go close(inC) + } + + select { + case err = <-errC: + if _, ok := err.(term.EscapeError); ok { + err = nil + } + case <-ctx.Done(): + err = ctx.Err() + case <-changroup.AllSettled(inC, outC): + } + if err != nil { + return -1, err + } + + cei, inspectErr := c.ContainerExecInspect(ctx, idr.ID) + if inspectErr != nil { + return -1, inspectErr + } + + return cei.ExitCode, err +} + +func (c *Container) Restart(ctx context.Context) error { + seconds := -1 + if deadline, ok := ctx.Deadline(); ok { + seconds = int(time.Until(deadline).Seconds()) + } + + return c.Client.ContainerRestart(ctx, c.ID, container.StopOptions{ + Timeout: &seconds, + }) +} + +func (c *Container) Stop(ctx context.Context) error { + seconds := -1 + if deadline, ok := ctx.Deadline(); ok { + seconds = int(time.Until(deadline).Seconds()) + } + + return c.Client.ContainerStop(ctx, c.ID, container.StopOptions{ + Timeout: &seconds, + }) +} + +func (c *Container) Remove(ctx context.Context) error { + return c.Client.ContainerRemove(ctx, c.ID, container.RemoveOptions{ + Force: true, + }) +} + +func (c *Container) Kill(ctx context.Context) error { + return c.Client.ContainerKill(ctx, c.ID, os.Kill.String()) +} diff --git a/runtime/docker/container_exec.go b/runtime/docker/container_exec.go deleted file mode 100644 index 8daac590..00000000 --- a/runtime/docker/container_exec.go +++ /dev/null @@ -1,121 +0,0 @@ -package docker - -import ( - "context" - "io" - - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/pkg/stdcopy" - "github.com/frantjc/forge" - "github.com/frantjc/forge/changroup" - "github.com/moby/term" -) - -func (c *Container) Exec(ctx context.Context, containerConfig *forge.ContainerConfig, streams *forge.Streams) (int, error) { - var ( - stdin io.Reader - stdout, stderr io.Writer - tty bool - detachKeys string - ) - if streams != nil { - stdin = streams.In - stdout = streams.Out - stderr = streams.Err - tty = streams.Tty - if tty { - stderr = stdout - } - detachKeys = streams.DetachKeys - } - - idr, err := c.Client.ContainerExecCreate(ctx, c.ID, container.ExecOptions{ - User: containerConfig.User, - Privileged: containerConfig.Privileged, - Env: containerConfig.Env, - WorkingDir: containerConfig.WorkingDir, - Cmd: append(containerConfig.Entrypoint, containerConfig.Cmd...), - Tty: tty, - DetachKeys: detachKeys, - AttachStdin: stdin != nil, - AttachStdout: stdout != nil, - AttachStderr: stderr != nil, - }) - if err != nil { - return -1, err - } - - hjr, err := c.Client.ContainerExecAttach(ctx, idr.ID, container.ExecStartOptions{ - Tty: tty, - }) - if err != nil { - return -1, err - } - defer hjr.Close() - - errC := make(chan error, 1) - outC := make(chan any, 1) - go func() { - if tty { - _, err = io.Copy(stdout, hjr.Reader) - } else { - // "exit status 1" comes from here - _, err = stdcopy.StdCopy( - stdout, - stderr, - hjr.Reader, - ) - } - if err != nil { - errC <- err - } - - go close(outC) - }() - - inC := make(chan any, 1) - if stdin != nil { - if detachKeys != "" { - detachKeysB, err := term.ToBytes(detachKeys) - if err != nil { - return -1, err - } - - stdin = term.NewEscapeProxy(stdin, detachKeysB) - } - - go func() { - if _, err = io.Copy(hjr.Conn, stdin); err != nil { - errC <- err - } - - if err = hjr.CloseWrite(); err != nil { - errC <- err - } - - go close(inC) - }() - } else { - go close(inC) - } - - select { - case err = <-errC: - if _, ok := err.(term.EscapeError); ok { - err = nil - } - case <-ctx.Done(): - err = ctx.Err() - case <-changroup.AllSettled(inC, outC): - } - if err != nil { - return -1, err - } - - cei, inspectErr := c.ContainerExecInspect(ctx, idr.ID) - if inspectErr != nil { - return -1, inspectErr - } - - return cei.ExitCode, err -} diff --git a/runtime/docker/container_kill.go b/runtime/docker/container_kill.go deleted file mode 100644 index 30de9343..00000000 --- a/runtime/docker/container_kill.go +++ /dev/null @@ -1,10 +0,0 @@ -package docker - -import ( - "context" - "os" -) - -func (c *Container) Kill(ctx context.Context) error { - return c.Client.ContainerKill(ctx, c.ID, os.Kill.String()) -} diff --git a/runtime/docker/container_remove.go b/runtime/docker/container_remove.go deleted file mode 100644 index 12ebfaf6..00000000 --- a/runtime/docker/container_remove.go +++ /dev/null @@ -1,13 +0,0 @@ -package docker - -import ( - "context" - - "github.com/docker/docker/api/types/container" -) - -func (c *Container) Remove(ctx context.Context) error { - return c.Client.ContainerRemove(ctx, c.ID, container.RemoveOptions{ - Force: true, - }) -} diff --git a/runtime/docker/container_restart.go b/runtime/docker/container_restart.go deleted file mode 100644 index 2f19c1b8..00000000 --- a/runtime/docker/container_restart.go +++ /dev/null @@ -1,19 +0,0 @@ -package docker - -import ( - "context" - "time" - - "github.com/docker/docker/api/types/container" -) - -func (c *Container) Restart(ctx context.Context) error { - seconds := -1 - if deadline, ok := ctx.Deadline(); ok { - seconds = int(time.Until(deadline).Seconds()) - } - - return c.Client.ContainerRestart(ctx, c.ID, container.StopOptions{ - Timeout: &seconds, - }) -} diff --git a/runtime/docker/container_run.go b/runtime/docker/container_run.go deleted file mode 100644 index a98a7df7..00000000 --- a/runtime/docker/container_run.go +++ /dev/null @@ -1,100 +0,0 @@ -package docker - -import ( - "context" - "fmt" - "io" - - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/pkg/stdcopy" - "github.com/frantjc/forge" - "github.com/moby/term" -) - -func (c *Container) Run(ctx context.Context, streams *forge.Streams) (int, error) { - var ( - stdin io.Reader - stdout, stderr io.Writer - detachKeys string - tty bool - ) - if streams != nil { - stdin = streams.In - stdout = streams.Out - stderr = streams.Err - tty = streams.Tty - if tty { - stderr = stdout - } - detachKeys = streams.DetachKeys - } - - hjr, err := c.ContainerAttach(ctx, c.ID, container.AttachOptions{ - Stream: streams != nil, - Stdin: stdin != nil, - Stdout: stdout != nil, - Stderr: stderr != nil, - DetachKeys: detachKeys, - }) - if err != nil { - return -1, err - } - - errC := make(chan error, 1) - go func() { - if tty { - _, err = io.Copy(stdout, hjr.Reader) - } else { - _, err = stdcopy.StdCopy( - stdout, - stderr, - hjr.Reader, - ) - } - if err != nil { - errC <- err - } - }() - - if stdin != nil { - if detachKeys != "" { - detachKeysB, err := term.ToBytes(detachKeys) - if err != nil { - return -1, err - } - - stdin = term.NewEscapeProxy(stdin, detachKeysB) - } - - go func() { - if _, err = io.Copy(hjr.Conn, stdin); err != nil { - errC <- err - } - - if err = hjr.CloseWrite(); err != nil { - errC <- hjr.CloseWrite() - } - }() - } - - if err = c.ContainerStart(ctx, c.ID, container.StartOptions{}); err != nil { - return -1, err - } - - cwokbC, waitErrC := c.ContainerWait(ctx, c.ID, container.WaitConditionNotRunning) - - select { - case cwokb := <-cwokbC: - if cwokb.Error != nil { - err = fmt.Errorf("%s", cwokb.Error.Message) - } - - return int(cwokb.StatusCode), err - case err = <-errC: - return -1, err - case err = <-waitErrC: - return -1, err - case <-ctx.Done(): - return -1, ctx.Err() - } -} diff --git a/runtime/docker/container_runtime.go b/runtime/docker/container_runtime.go index b9253e32..bb1a85bf 100644 --- a/runtime/docker/container_runtime.go +++ b/runtime/docker/container_runtime.go @@ -1,6 +1,29 @@ package docker -import "github.com/docker/docker/client" +import ( + "context" + "errors" + "fmt" + "io" + "os/exec" + "path/filepath" + "runtime" + "strings" + + "github.com/docker/cli/cli/command/image/build" + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/image" + "github.com/docker/docker/api/types/mount" + "github.com/docker/docker/client" + "github.com/docker/docker/pkg/archive" + "github.com/docker/docker/pkg/idtools" + "github.com/docker/docker/pkg/jsonmessage" + "github.com/frantjc/forge" + xslice "github.com/frantjc/x/slice" + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/daemon" +) func New(c *client.Client, dindPath string) *ContainerRuntime { return &ContainerRuntime{c, dindPath} @@ -16,6 +39,255 @@ type ContainerRuntime struct { DockerInDockerPath string } -func (f *ContainerRuntime) GoString() string { - return "&ContainerRuntime{" + f.DaemonHost() + "}" +func (d *ContainerRuntime) GoString() string { + return "&ContainerRuntime{" + d.DaemonHost() + "}" +} + +func (d *ContainerRuntime) PullImage(ctx context.Context, reference string) (forge.Image, error) { + ref, err := name.ParseReference(reference) + if err != nil { + return nil, err + } + + r, err := d.Client.ImagePull(ctx, ref.Name(), image.PullOptions{}) + if err != nil { + return nil, err + } + + if _, err = io.Copy(io.Discard, r); err != nil { + return nil, err + } + + img, err := daemon.Image(ref, daemon.WithClient(d), daemon.WithContext(ctx)) + if err != nil { + return nil, err + } + + return &Image{ + Image: img, + Reference: ref, + }, nil +} + +func (d *ContainerRuntime) BuildDockerfile(ctx context.Context, dockerfile, reference string) (forge.Image, error) { + ref, err := name.ParseReference(reference) + if err != nil { + return nil, err + } + + dir := filepath.Dir(dockerfile) + + excludes, err := build.ReadDockerignore(dir) + if err != nil { + return nil, err + } + + buildCtx, err := archive.TarWithOptions(dir, &archive.TarOptions{ + ExcludePatterns: excludes, + ChownOpts: &idtools.Identity{UID: 0, GID: 0}, + }) + if err != nil { + return nil, err + } + + if bc, err := build.Compress(buildCtx); err == nil { + buildCtx = bc + } + + ibr, err := d.Client.ImageBuild(ctx, buildCtx, types.ImageBuildOptions{ + Tags: []string{ref.Name()}, + Dockerfile: filepath.Base(dockerfile), + PullParent: true, + Remove: true, + }) + if err != nil { + return nil, err + } + + if err := jsonmessage.DisplayJSONMessagesStream(ibr.Body, io.Discard, 0, false, nil); err != nil { + if jerr, ok := err.(*jsonmessage.JSONError); ok { + return nil, jerr + } + + return nil, err + } + + if _, err = io.Copy(io.Discard, ibr.Body); err != nil { + return nil, err + } + + if err = ibr.Body.Close(); err != nil { + return nil, err + } + + img, err := daemon.Image(ref, daemon.WithClient(d), daemon.WithContext(ctx)) + if err != nil { + return nil, err + } + + return &Image{ + Image: img, + Reference: ref, + }, nil +} + +func (d *ContainerRuntime) CreateContainer(ctx context.Context, image forge.Image, config *forge.ContainerConfig) (forge.Container, error) { + // If the Docker daemon already has the image, + // don't bother loading it in again. + ii, _, err := d.ImageInspectWithRaw(ctx, image.Name()) + if err != nil { + if ilr, err := d.ImageLoad(ctx, image.Blob(), true); err != nil { + return nil, err + } else if err = ilr.Body.Close(); err != nil { + return nil, err + } + } + + var ( + addr = d.Client.DaemonHost() + containerConfig = &container.Config{ + User: config.User, + Env: config.Env, + Cmd: config.Cmd, + WorkingDir: config.WorkingDir, + Entrypoint: config.Entrypoint, + Image: image.Name(), + AttachStdin: true, + OpenStdin: true, + StdinOnce: true, + AttachStdout: true, + AttachStderr: true, + } + hostConfig = &container.HostConfig{ + Privileged: config.Privileged, + } + ) + + if d.DockerInDockerPath != "" { + // Because this is the Docker runtime... + // Mount the Docker daemon into the container for use by the process inside the container. + if strings.HasPrefix(addr, "unix://") { + sock := filepath.Join(d.DockerInDockerPath, "/docker.sock") + hostConfig.Mounts = append(hostConfig.Mounts, mount.Mount{ + Source: strings.TrimPrefix(addr, "unix://"), + Target: sock, + Type: mount.TypeBind, + }) + containerConfig.Env = append(containerConfig.Env, fmt.Sprintf("%s=unix://%s", client.EnvOverrideHost, sock)) + } + + // Also because this is the Docker runtime... + // If we're on linux, mount the Docker CLI into the container since then executables + // on the host can also be used by the container because they have a common OS. + if runtime.GOOS == "linux" { + docker, err := exec.LookPath("docker") + if errors.Is(err, exec.ErrDot) { + docker, err = filepath.Abs(docker) + } + + if err == nil { + var ( + bin = filepath.Join(d.DockerInDockerPath, "bin") + addedPath = false + ) + + hostConfig.Mounts = append(hostConfig.Mounts, mount.Mount{ + Source: docker, + Target: filepath.Join(bin, "docker"), + Type: mount.TypeBind, + }) + + // If there already is a PATH, add to it. + for i, e := range containerConfig.Env { + if strings.HasPrefix(e, "PATH=") { + containerConfig.Env[i] = fmt.Sprintf("%s:%s", e, bin) + addedPath = true + break + } + } + + // findPATHAppendBinFn iterates an env array searching for PATH. + // If it finds it, it appends it to containerConfig.Env with bin + // appended to the end and marks addedPath as true so we know to + // stop this absurd PATH hunt. + // + // Note that we append to the very end so that we don't override + // a Docker CLI that already exists on the PATH and cause unexpected + // behavior with our arguably over the top helpfulness here. + findPATHAppendBinFn := func(env []string) { + for _, e := range env { + if strings.HasPrefix(e, "PATH=") { + containerConfig.Env = append(containerConfig.Env, fmt.Sprintf("%s:%s", e, bin)) + addedPath = true + break + } + } + } + + // If we didn't find the PATH on the containerConfig to modify, + // then we want to modify it as it appears on the image config. + // + // Unfortunately, image.Config() is unbearably slow, so we use this + // workaround to try and get the image config from elsewhere first. + if !addedPath { + // We may already have successfully called ImageInspectWithRaw + // previously to check if we needed to call ImageLoad. If we didn't, + // retry and it should work now that we've definitely loaded the image. + if ii.Config == nil || len(ii.Config.Env) == 0 { + ii, _, _ = d.ImageInspectWithRaw(ctx, image.Name()) + } + + if ii.Config != nil { + findPATHAppendBinFn(ii.Config.Env) + } + + if !addedPath { + if imageConfig, err := image.Config(); err == nil { + findPATHAppendBinFn(imageConfig.Env) + } + } + } + + // If we still didn't find the PATH on the imageConfig to modify, + // we just add PATH ourselves. + if !addedPath { + containerConfig.Env = append(containerConfig.Env, fmt.Sprintf("PATH=%s", bin)) + } + } + } + } + + hostConfig.Mounts = append(hostConfig.Mounts, xslice.Map( + config.Mounts, + func(m forge.Mount, _ int) mount.Mount { + mountType := mount.TypeVolume + switch { + case m.Source == "": + mountType = mount.TypeTmpfs + case filepath.IsAbs(m.Source): + mountType = mount.TypeBind + } + + return mount.Mount{ + Type: mountType, + Source: m.Source, + Target: m.Destination, + } + }, + )...) + + cccb, err := d.ContainerCreate(ctx, containerConfig, hostConfig, nil, nil, "") + if err != nil { + return nil, err + } + + return &Container{cccb.ID, d.Client}, nil +} + +func (d *ContainerRuntime) GetContainer(ctx context.Context, id string) (forge.Container, error) { + if _, err := d.ContainerInspect(ctx, id); err != nil { + return nil, err + } + + return &Container{id, d.Client}, nil } diff --git a/runtime/docker/container_start.go b/runtime/docker/container_start.go deleted file mode 100644 index 116281bf..00000000 --- a/runtime/docker/container_start.go +++ /dev/null @@ -1,11 +0,0 @@ -package docker - -import ( - "context" - - "github.com/docker/docker/api/types/container" -) - -func (c *Container) Start(ctx context.Context) error { - return c.Client.ContainerStart(ctx, c.ID, container.StartOptions{}) -} diff --git a/runtime/docker/container_stop.go b/runtime/docker/container_stop.go deleted file mode 100644 index b7fd6ccd..00000000 --- a/runtime/docker/container_stop.go +++ /dev/null @@ -1,19 +0,0 @@ -package docker - -import ( - "context" - "time" - - "github.com/docker/docker/api/types/container" -) - -func (c *Container) Stop(ctx context.Context) error { - seconds := -1 - if deadline, ok := ctx.Deadline(); ok { - seconds = int(time.Until(deadline).Seconds()) - } - - return c.Client.ContainerStop(ctx, c.ID, container.StopOptions{ - Timeout: &seconds, - }) -} diff --git a/runtime/docker/copy_from_container.go b/runtime/docker/copy_from_container.go deleted file mode 100644 index a6b3da5e..00000000 --- a/runtime/docker/copy_from_container.go +++ /dev/null @@ -1,12 +0,0 @@ -package docker - -import ( - "context" - "io" - "path/filepath" -) - -func (c *Container) CopyFrom(ctx context.Context, source string) (io.ReadCloser, error) { - rc, _, err := c.CopyFromContainer(ctx, c.ID, filepath.Clean(source)) - return rc, err -} diff --git a/runtime/docker/copy_to_container.go b/runtime/docker/copy_to_container.go deleted file mode 100644 index b7b65e71..00000000 --- a/runtime/docker/copy_to_container.go +++ /dev/null @@ -1,17 +0,0 @@ -package docker - -import ( - "context" - "io" - "path/filepath" - - "github.com/docker/docker/api/types/container" -) - -func (c *Container) CopyTo(ctx context.Context, destination string, content io.Reader) error { - if rc, ok := content.(io.ReadCloser); ok { - defer rc.Close() - } - - return c.CopyToContainer(ctx, c.ID, filepath.Clean(destination), content, container.CopyToContainerOptions{}) -} diff --git a/runtime/docker/create_container.go b/runtime/docker/create_container.go deleted file mode 100644 index 4083a337..00000000 --- a/runtime/docker/create_container.go +++ /dev/null @@ -1,170 +0,0 @@ -package docker - -import ( - "context" - "errors" - "fmt" - "os/exec" - "path/filepath" - "runtime" - "strings" - - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/mount" - "github.com/docker/docker/client" - "github.com/frantjc/forge" - xslice "github.com/frantjc/x/slice" -) - -func (d *ContainerRuntime) CreateContainer(ctx context.Context, image forge.Image, config *forge.ContainerConfig) (forge.Container, error) { - // If the Docker daemon already has the image, - // don't bother loading it in again. - ii, _, err := d.ImageInspectWithRaw(ctx, image.Name()) - if err != nil { - if ilr, err := d.ImageLoad(ctx, image.Blob(), true); err != nil { - return nil, err - } else if err = ilr.Body.Close(); err != nil { - return nil, err - } - } - - var ( - addr = d.Client.DaemonHost() - containerConfig = &container.Config{ - User: config.User, - Env: config.Env, - Cmd: config.Cmd, - WorkingDir: config.WorkingDir, - Entrypoint: config.Entrypoint, - Image: image.Name(), - AttachStdin: true, - OpenStdin: true, - StdinOnce: true, - AttachStdout: true, - AttachStderr: true, - } - hostConfig = &container.HostConfig{ - Privileged: config.Privileged, - } - ) - - if d.DockerInDockerPath != "" { - // Because this is the Docker runtime... - // Mount the Docker daemon into the container for use by the process inside the container. - if strings.HasPrefix(addr, "unix://") { - sock := filepath.Join(d.DockerInDockerPath, "/docker.sock") - hostConfig.Mounts = append(hostConfig.Mounts, mount.Mount{ - Source: strings.TrimPrefix(addr, "unix://"), - Target: sock, - Type: mount.TypeBind, - }) - containerConfig.Env = append(containerConfig.Env, fmt.Sprintf("%s=unix://%s", client.EnvOverrideHost, sock)) - } - - // Also because this is the Docker runtime... - // If we're on linux, mount the Docker CLI into the container since then executables - // on the host can also be used by the container because they have a common OS. - if runtime.GOOS == "linux" { - docker, err := exec.LookPath("docker") - if errors.Is(err, exec.ErrDot) { - docker, err = filepath.Abs(docker) - } - - if err == nil { - var ( - bin = filepath.Join(d.DockerInDockerPath, "bin") - addedPath = false - ) - - hostConfig.Mounts = append(hostConfig.Mounts, mount.Mount{ - Source: docker, - Target: filepath.Join(bin, "docker"), - Type: mount.TypeBind, - }) - - // If there already is a PATH, add to it. - for i, e := range containerConfig.Env { - if strings.HasPrefix(e, "PATH=") { - containerConfig.Env[i] = fmt.Sprintf("%s:%s", e, bin) - addedPath = true - break - } - } - - // findPATHAppendBinFn iterates an env array searching for PATH. - // If it finds it, it appends it to containerConfig.Env with bin - // appended to the end and marks addedPath as true so we know to - // stop this absurd PATH hunt. - // - // Note that we append to the very end so that we don't override - // a Docker CLI that already exists on the PATH and cause unexpected - // behavior with our arguably over the top helpfulness here. - findPATHAppendBinFn := func(env []string) { - for _, e := range env { - if strings.HasPrefix(e, "PATH=") { - containerConfig.Env = append(containerConfig.Env, fmt.Sprintf("%s:%s", e, bin)) - addedPath = true - break - } - } - } - - // If we didn't find the PATH on the containerConfig to modify, - // then we want to modify it as it appears on the image config. - // - // Unfortunately, image.Config() is unbearably slow, so we use this - // workaround to try and get the image config from elsewhere first. - if !addedPath { - // We may already have successfully called ImageInspectWithRaw - // previously to check if we needed to call ImageLoad. If we didn't, - // retry and it should work now that we've definitely loaded the image. - if ii.Config == nil || len(ii.Config.Env) == 0 { - ii, _, _ = d.ImageInspectWithRaw(ctx, image.Name()) - } - - if ii.Config != nil { - findPATHAppendBinFn(ii.Config.Env) - } - - if !addedPath { - if imageConfig, err := image.Config(); err == nil { - findPATHAppendBinFn(imageConfig.Env) - } - } - } - - // If we still didn't find the PATH on the imageConfig to modify, - // we just add PATH ourselves. - if !addedPath { - containerConfig.Env = append(containerConfig.Env, fmt.Sprintf("PATH=%s", bin)) - } - } - } - } - - hostConfig.Mounts = append(hostConfig.Mounts, xslice.Map( - config.Mounts, - func(m forge.Mount, _ int) mount.Mount { - mountType := mount.TypeVolume - switch { - case m.Source == "": - mountType = mount.TypeTmpfs - case filepath.IsAbs(m.Source): - mountType = mount.TypeBind - } - - return mount.Mount{ - Type: mountType, - Source: m.Source, - Target: m.Destination, - } - }, - )...) - - cccb, err := d.ContainerCreate(ctx, containerConfig, hostConfig, nil, nil, "") - if err != nil { - return nil, err - } - - return &Container{cccb.ID, d.Client}, nil -} diff --git a/runtime/docker/get_container.go b/runtime/docker/get_container.go deleted file mode 100644 index df05cc43..00000000 --- a/runtime/docker/get_container.go +++ /dev/null @@ -1,15 +0,0 @@ -package docker - -import ( - "context" - - "github.com/frantjc/forge" -) - -func (d *ContainerRuntime) GetContainer(ctx context.Context, id string) (forge.Container, error) { - if _, err := d.ContainerInspect(ctx, id); err != nil { - return nil, err - } - - return &Container{id, d.Client}, nil -} diff --git a/runtime/docker/pull_image.go b/runtime/docker/pull_image.go deleted file mode 100644 index 31519b1e..00000000 --- a/runtime/docker/pull_image.go +++ /dev/null @@ -1,37 +0,0 @@ -package docker - -import ( - "context" - "io" - - "github.com/docker/docker/api/types/image" - "github.com/frantjc/forge" - "github.com/google/go-containerregistry/pkg/name" - "github.com/google/go-containerregistry/pkg/v1/daemon" -) - -func (d *ContainerRuntime) PullImage(ctx context.Context, reference string) (forge.Image, error) { - ref, err := name.ParseReference(reference) - if err != nil { - return nil, err - } - - r, err := d.Client.ImagePull(ctx, ref.Name(), image.PullOptions{}) - if err != nil { - return nil, err - } - - if _, err = io.Copy(io.Discard, r); err != nil { - return nil, err - } - - img, err := daemon.Image(ref, daemon.WithClient(d), daemon.WithContext(ctx)) - if err != nil { - return nil, err - } - - return &Image{ - Image: img, - Reference: ref, - }, nil -}