package containerengine import ( "context" "encoding/json" "errors" "fmt" "io" cerrdefs "github.com/containerd/errdefs" "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) 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, }) } result, err := e.client.ContainerCreate(ctx, client.ContainerCreateOptions{ Config: &container.Config{ Env: append([]string(nil), spec.Environment...), Labels: cloneMap(spec.Labels), }, 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) 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) } 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)