package runtime import ( "context" "encoding/base64" "encoding/json" "fmt" "io" "strings" "time" "github.com/docker/docker/api/types/image" "github.com/docker/docker/api/types/registry" "github.com/docker/docker/client" ) // dockerImage implements Image using the Docker SDK. // Shared by both local and docker (via Tai proxy) runtime modes. type dockerImage struct { cli *client.Client } // NewDockerImage creates an Image backed by a Docker client. func NewDockerImage(cli *client.Client) Image { return &dockerImage{cli: cli} } func (d *dockerImage) Exists(ctx context.Context, ref string) (bool, error) { _, _, err := d.cli.ImageInspectWithRaw(ctx, ref) if err != nil { if client.IsErrNotFound(err) { return false, nil } return false, fmt.Errorf("image inspect %q: %w", ref, err) } return true, nil } func (d *dockerImage) Inspect(ctx context.Context, ref string) (*ImageMeta, error) { inspect, _, err := d.cli.ImageInspectWithRaw(ctx, ref) if err != nil { return nil, fmt.Errorf("image inspect %q: %w", ref, err) } meta := &ImageMeta{ OS: inspect.Os, Arch: inspect.Architecture, } if inspect.Config != nil { meta.WorkDir = inspect.Config.WorkingDir if len(inspect.Config.Shell) > 0 { meta.Shell = inspect.Config.Shell[0] } if meta.Shell == "" { for _, e := range inspect.Config.Env { if strings.HasPrefix(e, "SHELL=") { meta.Shell = e[6:] break } } } } if meta.Shell == "" { if strings.EqualFold(meta.OS, "windows") { meta.Shell = "cmd.exe" } else { meta.Shell = "bash" } } return meta, nil } func (d *dockerImage) Pull(ctx context.Context, ref string, opts PullOptions) (<-chan PullProgress, error) { pullOpts := image.PullOptions{} if opts.Auth != nil { encoded, err := encodeAuth(opts.Auth) if err != nil { return nil, err } pullOpts.RegistryAuth = encoded } reader, err := d.cli.ImagePull(ctx, ref, pullOpts) if err != nil { return nil, fmt.Errorf("image pull %q: %w", ref, err) } ch := make(chan PullProgress, 32) go func() { defer close(ch) defer reader.Close() decodePullStream(reader, ch) }() return ch, nil } func (d *dockerImage) Remove(ctx context.Context, ref string, force bool) error { _, err := d.cli.ImageRemove(ctx, ref, image.RemoveOptions{Force: force, PruneChildren: true}) if err != nil { return fmt.Errorf("image remove %q: %w", ref, err) } return nil } func (d *dockerImage) List(ctx context.Context) ([]ImageInfo, error) { imgs, err := d.cli.ImageList(ctx, image.ListOptions{}) if err != nil { return nil, fmt.Errorf("image list: %w", err) } result := make([]ImageInfo, len(imgs)) for i, img := range imgs { result[i] = ImageInfo{ ID: img.ID, Tags: img.RepoTags, Size: img.Size, Created: time.Unix(img.Created, 0), } } return result, nil } // dockerPullEvent mirrors the JSON lines emitted by Docker's ImagePull stream. type dockerPullEvent struct { Status string `json:"status"` ID string `json:"id"` ProgressDetail struct { Current int64 `json:"current"` Total int64 `json:"total"` } `json:"progressDetail"` Error string `json:"error"` } func decodePullStream(r io.Reader, ch chan<- PullProgress) { dec := json.NewDecoder(r) for { var ev dockerPullEvent if err := dec.Decode(&ev); err != nil { if err != io.EOF { ch <- PullProgress{Error: err.Error()} } return } p := PullProgress{ Status: ev.Status, Layer: ev.ID, Current: ev.ProgressDetail.Current, Total: ev.ProgressDetail.Total, } if ev.Error != "" { p.Error = ev.Error } ch <- p } } func encodeAuth(auth *RegistryAuth) (string, error) { cfg := registry.AuthConfig{ Username: auth.Username, Password: auth.Password, ServerAddress: auth.Server, } data, err := json.Marshal(cfg) if err != nil { return "", fmt.Errorf("encode registry auth: %w", err) } return base64.URLEncoding.EncodeToString(data), nil }