diff --git a/pkg/container/container.go b/pkg/container/container.go new file mode 100644 index 0000000..6ea8f98 --- /dev/null +++ b/pkg/container/container.go @@ -0,0 +1,45 @@ +package container + +import ( + "errors" + + "github.com/docker/docker/client" + "golang.org/x/net/context" +) + +// Options represents the configuration for running a docker container to +// be used as backdrop. +type Options struct { + Client *client.Client + Image string + Name string + Interactive bool + Interpreter []string + Entrypoint string + Script string + Command []string + Environment []string + Volumes []string + VolumesFrom []string + User string + WorkingDir string +} + +// Run runs a docker container as backdrop. +func Run(ctx context.Context, options Options) error { + if options.Client == nil { + return errors.New("client may not be nil") + } + + containerID, err := createContainer(ctx, options) + if err != nil { + return err + } + + err = uploadEntrypoint(ctx, containerID, options) + if err != nil { + return err + } + + return runContainer(ctx, containerID, options) +} diff --git a/pkg/container/create.go b/pkg/container/create.go new file mode 100644 index 0000000..67fc2a9 --- /dev/null +++ b/pkg/container/create.go @@ -0,0 +1,49 @@ +package container + +import ( + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/network" + "golang.org/x/net/context" +) + +func createContainer(ctx context.Context, options Options) (string, error) { + response, err := options.Client.ContainerCreate( + ctx, + &container.Config{ + User: options.User, + AttachStdin: true, + AttachStdout: true, + AttachStderr: true, + Tty: true, + OpenStdin: true, + StdinOnce: true, + Env: options.Environment, + Cmd: options.Command, + Image: options.Image, + WorkingDir: options.WorkingDir, + Entrypoint: getEntrypoint(options), + }, + &container.HostConfig{ + AutoRemove: true, + Binds: options.Volumes, + VolumesFrom: options.VolumesFrom, + }, + &network.NetworkingConfig{}, + options.Name, + ) + if err != nil { + return "", err + } + return response.ID, nil +} + +func getEntrypoint(options Options) []string { + entrypoint := []string{"/bin/sh"} + if len(options.Interpreter) > 0 { + entrypoint = options.Interpreter + } + if !options.Interactive { + entrypoint = append(entrypoint, options.Entrypoint) + } + return entrypoint +} diff --git a/pkg/state/entrypoint.go b/pkg/container/entrypoint.go similarity index 51% rename from pkg/state/entrypoint.go rename to pkg/container/entrypoint.go index 522aaf9..08cef7f 100644 --- a/pkg/state/entrypoint.go +++ b/pkg/container/entrypoint.go @@ -1,4 +1,4 @@ -package state +package container import ( "archive/tar" @@ -9,19 +9,7 @@ import ( "golang.org/x/net/context" ) -// EnsureEntrypoint makes sure the entrypoint script ist uploaded to the -// container. -func (state *State) EnsureEntrypoint(ctx context.Context) error { - config := state.Config - client, err := state.EnsureClient() - if err != nil { - return err - } - container, err := state.EnsureContainer(ctx) - if err != nil { - return err - } - +func uploadEntrypoint(ctx context.Context, containerID string, options Options) error { reader, writer := io.Pipe() defer func() { if err := reader.Close(); err != nil { @@ -30,9 +18,9 @@ func (state *State) EnsureEntrypoint(ctx context.Context) error { }() go func() { - err := client.CopyToContainer( + err := options.Client.CopyToContainer( ctx, - container, + containerID, "/", reader, types.CopyToContainerOptions{}, @@ -43,15 +31,15 @@ func (state *State) EnsureEntrypoint(ctx context.Context) error { }() tarWriter := tar.NewWriter(writer) - err = tarWriter.WriteHeader(&tar.Header{ - Name: state.Entrypoint, + err := tarWriter.WriteHeader(&tar.Header{ + Name: options.Entrypoint, Mode: 0600, - Size: int64(len(config.Script)), + Size: int64(len(options.Script)), }) if err != nil { return err } - _, err = tarWriter.Write([]byte(state.Config.Script)) + _, err = tarWriter.Write([]byte(options.Script)) if err != nil { return err } diff --git a/pkg/state/container_run.go b/pkg/container/run.go similarity index 79% rename from pkg/state/container_run.go rename to pkg/container/run.go index 95f3f7b..fdef924 100644 --- a/pkg/state/container_run.go +++ b/pkg/container/run.go @@ -1,4 +1,4 @@ -package state +package container import ( "io" @@ -11,23 +11,8 @@ import ( "golang.org/x/net/context" ) -// EnsureRun makes sure the command is performed. -func (state *State) EnsureRun(ctx context.Context) error { - client, err := state.EnsureClient() - if err != nil { - return err - } - containerID, err := state.EnsureContainer(ctx) - if err != nil { - return err - } - defer state.EnsureCleanup(ctx) - err = state.EnsureEntrypoint(ctx) - if err != nil { - return err - } - - attach, err := client.ContainerAttach( +func runContainer(ctx context.Context, containerID string, options Options) error { + attach, err := options.Client.ContainerAttach( ctx, containerID, types.ContainerAttachOptions{ @@ -101,13 +86,13 @@ func (state *State) EnsureRun(ctx context.Context) error { } }() - waitChannel, waitErrorChannel := client.ContainerWait( + waitChannel, waitErrorChannel := options.Client.ContainerWait( ctx, containerID, container.WaitConditionRemoved, ) - err = client.ContainerStart( + err = options.Client.ContainerStart( ctx, containerID, types.ContainerStartOptions{}, diff --git a/pkg/state/cleanup.go b/pkg/state/cleanup.go deleted file mode 100644 index 6c773fd..0000000 --- a/pkg/state/cleanup.go +++ /dev/null @@ -1,35 +0,0 @@ -package state - -import ( - "github.com/docker/docker/api/types" - log "github.com/sirupsen/logrus" - "golang.org/x/net/context" -) - -// EnsureCleanup makes sure the docker container is removed after run. -func (state *State) EnsureCleanup(ctx context.Context) { - if state.ContainerID == "" { - return - } - client, err := state.EnsureClient() - if err != nil { - return - } - if state.Config.Remove != nil && !*state.Config.Remove { - return - } - - err = client.ContainerRemove( - ctx, - state.ContainerID, - types.ContainerRemoveOptions{ - RemoveVolumes: true, - RemoveLinks: true, - Force: true, - }, - ) - if err != nil { - log.Error(err) - } - state.ContainerID = "" -} diff --git a/pkg/state/client.go b/pkg/state/client.go deleted file mode 100644 index d97ed48..0000000 --- a/pkg/state/client.go +++ /dev/null @@ -1,21 +0,0 @@ -package state - -import ( - "github.com/docker/docker/client" -) - -// TODO: read docker configuration - -// EnsureClient makes sure a client is present on the state. -func (state *State) EnsureClient() (*client.Client, error) { - if state.Client != nil { - return state.Client, nil - } - - newClient, err := client.NewClientWithOpts(client.FromEnv) - if err != nil { - return nil, err - } - state.Client = newClient - return state.Client, nil -} diff --git a/pkg/state/container_create.go b/pkg/state/container_create.go deleted file mode 100644 index 161d3d1..0000000 --- a/pkg/state/container_create.go +++ /dev/null @@ -1,58 +0,0 @@ -package state - -import ( - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/network" - "golang.org/x/net/context" -) - -// EnsureContainer will soon be moved anyway -func (state *State) EnsureContainer(ctx context.Context) (string, error) { - config := state.Config - client := state.Client - image := state.Image - if state.ContainerID != "" { - return state.ContainerID, nil - } - - response, err := client.ContainerCreate( - ctx, - &container.Config{ - User: config.User, - AttachStdin: true, - AttachStdout: true, - AttachStderr: true, - Tty: true, - OpenStdin: true, - StdinOnce: true, - Env: config.Environment, - Cmd: config.Command, - Image: image, - WorkingDir: config.WorkingDir, - Entrypoint: state.getEntrypoint(), - }, - &container.HostConfig{ - AutoRemove: true, - Binds: config.Volumes, - VolumesFrom: config.VolumesFrom, - }, - &network.NetworkingConfig{}, - config.ContainerName, - ) - if err != nil { - return "", err - } - state.ContainerID = response.ID - return state.ContainerID, nil -} - -func (state *State) getEntrypoint() []string { - entrypoint := []string{"/bin/sh"} - if len(state.Config.Interpreter) > 0 { - entrypoint = state.Config.Interpreter - } - if !state.Config.Interactive { - entrypoint = append(entrypoint, state.Entrypoint) - } - return entrypoint -} diff --git a/pkg/state/state.go b/pkg/state/state.go deleted file mode 100644 index ef2470d..0000000 --- a/pkg/state/state.go +++ /dev/null @@ -1,48 +0,0 @@ -package state - -import ( - "github.com/docker/docker/client" - "github.com/oclaussen/dodo/pkg/config" - "github.com/oclaussen/dodo/pkg/image" - "golang.org/x/net/context" -) - -// State represents the state of a command run. -type State struct { - Config *config.BackdropConfig - Client *client.Client - Entrypoint string - Image string - ContainerID string -} - -// NewState creates a new state base on a backdrop configuration. -func NewState(config *config.BackdropConfig) *State { - // TODO: generate a temp file in the container for the entrypoint - return &State{ - Config: config, - Entrypoint: "/tmp/dodo-entrypoint", - } -} - -// Run runs the command. -func (state *State) Run() error { - ctx := context.Background() - - client, err := state.EnsureClient() - if err != nil { - return err - } - - state.Image, err = image.Get(ctx, image.Options{ - Client: client, - Name: state.Config.Image, - Build: state.Config.Build, - ForcePull: state.Config.Pull, - }) - if err != nil { - return err - } - - return state.EnsureRun(ctx) -}