248 lines
7.4 KiB
Go
248 lines
7.4 KiB
Go
package containerengine
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
|
|
cerrdefs "github.com/containerd/errdefs"
|
|
"github.com/moby/moby/api/pkg/stdcopy"
|
|
"github.com/moby/moby/api/types/container"
|
|
"github.com/moby/moby/api/types/jsonstream"
|
|
"github.com/moby/moby/api/types/mount"
|
|
"github.com/moby/moby/client"
|
|
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
|
)
|
|
|
|
// MobyEngine adapts the official Docker Engine Go client.
|
|
type MobyEngine struct {
|
|
client *client.Client
|
|
}
|
|
|
|
// NewMobyEngine creates a client from Docker's documented environment variables.
|
|
// API negotiation remains enabled, including when DOCKER_HOST selects a non-default socket.
|
|
func NewMobyEngine() (*MobyEngine, error) {
|
|
apiClient, err := client.New(client.FromEnv)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create Docker Engine client: %w", err)
|
|
}
|
|
return &MobyEngine{client: apiClient}, nil
|
|
}
|
|
|
|
func (e *MobyEngine) Ping(ctx context.Context) error {
|
|
if _, err := e.client.Ping(ctx, client.PingOptions{NegotiateAPIVersion: true}); err != nil {
|
|
return fmt.Errorf("ping Docker Engine: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (e *MobyEngine) PullImage(ctx context.Context, imageReference string) error {
|
|
response, err := e.client.ImagePull(ctx, imageReference, client.ImagePullOptions{})
|
|
if err != nil {
|
|
return fmt.Errorf("pull image %s: %w", imageReference, err)
|
|
}
|
|
defer response.Close()
|
|
if err := decodeImageLoadResponse(response); err != nil {
|
|
return fmt.Errorf("pull image %s response: %w", imageReference, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (e *MobyEngine) LoadImage(ctx context.Context, input io.Reader) error {
|
|
if input == nil {
|
|
return errors.New("image archive reader is required")
|
|
}
|
|
response, err := e.client.ImageLoad(ctx, input)
|
|
if err != nil {
|
|
return fmt.Errorf("load image archive: %w", err)
|
|
}
|
|
defer response.Close()
|
|
if err := decodeImageLoadResponse(response); err != nil {
|
|
return fmt.Errorf("load image archive response: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func decodeImageLoadResponse(input io.Reader) error {
|
|
decoder := json.NewDecoder(input)
|
|
for {
|
|
var message jsonstream.Message
|
|
if err := decoder.Decode(&message); err != nil {
|
|
if errors.Is(err, io.EOF) {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("decode Docker JSON stream: %w", err)
|
|
}
|
|
if message.Error != nil {
|
|
return message.Error
|
|
}
|
|
}
|
|
}
|
|
|
|
func (e *MobyEngine) InspectImage(ctx context.Context, reference string) (Image, error) {
|
|
response, err := e.client.ImageInspect(ctx, reference)
|
|
if err != nil {
|
|
return Image{}, engineError("inspect image", err)
|
|
}
|
|
descriptorDigest := ""
|
|
if response.Descriptor != nil {
|
|
descriptorDigest = response.Descriptor.Digest.String()
|
|
}
|
|
return Image{
|
|
ID: response.ID,
|
|
RepoDigests: append([]string(nil), response.RepoDigests...),
|
|
DescriptorDigest: descriptorDigest,
|
|
Platform: Platform{
|
|
OS: response.Os,
|
|
Architecture: response.Architecture,
|
|
Variant: response.Variant,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
func (e *MobyEngine) CreateContainer(ctx context.Context, spec ContainerSpec) (Container, error) {
|
|
apiMounts := make([]mount.Mount, 0, len(spec.Mounts))
|
|
for _, item := range spec.Mounts {
|
|
apiMounts = append(apiMounts, mount.Mount{
|
|
Type: mount.Type(item.Type),
|
|
Source: item.Source,
|
|
Target: item.Target,
|
|
ReadOnly: item.ReadOnly,
|
|
})
|
|
}
|
|
stopTimeout := spec.StopTimeoutSeconds
|
|
result, err := e.client.ContainerCreate(ctx, client.ContainerCreateOptions{
|
|
Config: &container.Config{
|
|
Env: append([]string(nil), spec.Environment...),
|
|
Labels: cloneMap(spec.Labels),
|
|
User: spec.User,
|
|
StopTimeout: &stopTimeout,
|
|
},
|
|
HostConfig: &container.HostConfig{
|
|
NetworkMode: container.NetworkMode(spec.NetworkMode),
|
|
RestartPolicy: container.RestartPolicy{
|
|
Name: container.RestartPolicyMode(spec.RestartPolicy.Name),
|
|
MaximumRetryCount: spec.RestartPolicy.MaximumRetryCount,
|
|
},
|
|
Mounts: apiMounts,
|
|
},
|
|
Platform: &ocispec.Platform{
|
|
OS: spec.Platform.OS,
|
|
Architecture: spec.Platform.Architecture,
|
|
Variant: spec.Platform.Variant,
|
|
},
|
|
Name: spec.Name,
|
|
Image: spec.ImageReference,
|
|
})
|
|
if err != nil {
|
|
return Container{}, engineError("create container", err)
|
|
}
|
|
return e.InspectContainer(ctx, result.ID)
|
|
}
|
|
|
|
func (e *MobyEngine) StartContainer(ctx context.Context, idOrName string) error {
|
|
if _, err := e.client.ContainerStart(ctx, idOrName, client.ContainerStartOptions{}); err != nil {
|
|
return engineError("start container", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (e *MobyEngine) ContainerLogs(ctx context.Context, idOrName string) (io.ReadCloser, error) {
|
|
stream, err := e.client.ContainerLogs(ctx, idOrName, client.ContainerLogsOptions{ShowStdout: true, ShowStderr: true, Tail: "all"})
|
|
if err != nil {
|
|
return nil, engineError("read container logs", err)
|
|
}
|
|
defer stream.Close()
|
|
var output bytes.Buffer
|
|
if _, err := stdcopy.StdCopy(&output, &output, stream); err != nil {
|
|
return nil, fmt.Errorf("decode container logs: %w", err)
|
|
}
|
|
return io.NopCloser(bytes.NewReader(output.Bytes())), nil
|
|
}
|
|
|
|
func (e *MobyEngine) StopContainer(ctx context.Context, idOrName string) error {
|
|
if _, err := e.client.ContainerStop(ctx, idOrName, client.ContainerStopOptions{}); err != nil {
|
|
return engineError("stop container", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (e *MobyEngine) InspectContainer(ctx context.Context, idOrName string) (Container, error) {
|
|
result, err := e.client.ContainerInspect(ctx, idOrName, client.ContainerInspectOptions{})
|
|
if err != nil {
|
|
return Container{}, engineError("inspect container", err)
|
|
}
|
|
response := result.Container
|
|
record := Container{
|
|
ID: response.ID,
|
|
Name: response.Name,
|
|
ImageID: response.Image,
|
|
Platform: response.Platform,
|
|
}
|
|
if response.State != nil {
|
|
record.Running = response.State.Running
|
|
record.Dead = response.State.Dead
|
|
record.Status = string(response.State.Status)
|
|
}
|
|
if response.Config != nil {
|
|
record.ImageReference = response.Config.Image
|
|
record.Environment = append([]string(nil), response.Config.Env...)
|
|
record.Labels = cloneMap(response.Config.Labels)
|
|
record.User = response.Config.User
|
|
if response.Config.StopTimeout != nil {
|
|
record.StopTimeoutSeconds = *response.Config.StopTimeout
|
|
}
|
|
}
|
|
if response.HostConfig != nil {
|
|
record.NetworkMode = string(response.HostConfig.NetworkMode)
|
|
record.RestartPolicy = RestartPolicy{
|
|
Name: string(response.HostConfig.RestartPolicy.Name),
|
|
MaximumRetryCount: response.HostConfig.RestartPolicy.MaximumRetryCount,
|
|
}
|
|
}
|
|
for _, item := range response.Mounts {
|
|
record.Mounts = append(record.Mounts, Mount{
|
|
Type: string(item.Type),
|
|
Source: item.Source,
|
|
Target: item.Destination,
|
|
ReadOnly: !item.RW,
|
|
})
|
|
}
|
|
return record, nil
|
|
}
|
|
|
|
func (e *MobyEngine) RemoveContainer(ctx context.Context, idOrName string, force bool) error {
|
|
_, err := e.client.ContainerRemove(ctx, idOrName, client.ContainerRemoveOptions{Force: force})
|
|
if err != nil {
|
|
return engineError("remove container", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (e *MobyEngine) Close() error {
|
|
return e.client.Close()
|
|
}
|
|
|
|
func engineError(action string, err error) error {
|
|
if cerrdefs.IsNotFound(err) {
|
|
return fmt.Errorf("%s: %w: %v", action, ErrNotFound, err)
|
|
}
|
|
return fmt.Errorf("%s: %w", action, err)
|
|
}
|
|
|
|
func cloneMap(source map[string]string) map[string]string {
|
|
if source == nil {
|
|
return nil
|
|
}
|
|
result := make(map[string]string, len(source))
|
|
for key, value := range source {
|
|
result[key] = value
|
|
}
|
|
return result
|
|
}
|
|
|
|
var _ Engine = (*MobyEngine)(nil)
|