yao/tai/sandbox/image_docker.go
Max d25bfefee0 Enhance Sandbox V2 testing and integration with Workspace
- Introduce new unit test targets for Workspace and Sandbox V2 integration in the Makefile, ensuring comprehensive testing of the new features.
- Update CI workflows to support MongoDB service for Sandbox V2 tests and improve Docker image handling.
- Implement Workspace as a first-class entity, allowing for persistent storage decoupled from container lifecycles.
- Enhance the Box struct to manage workspace IDs and update related methods for improved functionality.
- Refactor tests to accommodate new Workspace features, ensuring robust testing of the integration.

These changes significantly improve the testing framework and functionality of the Sandbox V2, enhancing the overall architecture and user experience.
2026-03-05 21:00:50 +08:00

132 lines
3.1 KiB
Go

package sandbox
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"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 dockerSandbox (via Tai proxy) 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) 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
}