feat: conditionally pull images

This commit is contained in:
Rob Watson 2025-03-04 21:44:36 +01:00
parent 8b65a3573c
commit f5bfa62330

View File

@ -59,13 +59,15 @@ const (
// Client provides a thin wrapper around the Docker API client, and provides // Client provides a thin wrapper around the Docker API client, and provides
// additional functionality such as exposing container stats. // additional functionality such as exposing container stats.
type Client struct { type Client struct {
id shortid.ID id shortid.ID
ctx context.Context ctx context.Context
cancel context.CancelFunc cancel context.CancelFunc
wg sync.WaitGroup mu sync.Mutex
apiClient DockerClient wg sync.WaitGroup
networkID string apiClient DockerClient
logger *slog.Logger networkID string
pulledImages map[string]struct{}
logger *slog.Logger
} }
// NewClient creates a new Client. // NewClient creates a new Client.
@ -79,12 +81,13 @@ func NewClient(ctx context.Context, apiClient DockerClient, logger *slog.Logger)
ctx, cancel := context.WithCancel(ctx) ctx, cancel := context.WithCancel(ctx)
client := &Client{ client := &Client{
id: id, id: id,
ctx: ctx, ctx: ctx,
cancel: cancel, cancel: cancel,
apiClient: apiClient, apiClient: apiClient,
networkID: network.ID, networkID: network.ID,
logger: logger, pulledImages: make(map[string]struct{}),
logger: logger,
} }
return client, nil return client, nil
@ -154,15 +157,10 @@ func (a *Client) RunContainer(ctx context.Context, params RunContainerParams) (<
defer a.wg.Done() defer a.wg.Done()
defer close(errC) defer close(errC)
containerStateC <- domain.Container{State: "pulling"} if err := a.pullImageIfNeeded(ctx, params.ContainerConfig.Image, containerStateC); err != nil {
pullReader, err := a.apiClient.ImagePull(ctx, params.ContainerConfig.Image, image.PullOptions{})
if err != nil {
sendError(fmt.Errorf("image pull: %w", err)) sendError(fmt.Errorf("image pull: %w", err))
return return
} }
_, _ = io.Copy(io.Discard, pullReader)
_ = pullReader.Close()
containerConfig := *params.ContainerConfig containerConfig := *params.ContainerConfig
containerConfig.Labels = make(map[string]string) containerConfig.Labels = make(map[string]string)
@ -208,6 +206,32 @@ func (a *Client) RunContainer(ctx context.Context, params RunContainerParams) (<
return containerStateC, errC return containerStateC, errC
} }
// pullImageIfNeeded pulls the image if it has not already been pulled.
func (a *Client) pullImageIfNeeded(ctx context.Context, imageName string, containerStateC chan<- domain.Container) error {
a.mu.Lock()
_, ok := a.pulledImages[imageName]
a.mu.Unlock()
if ok {
return nil
}
containerStateC <- domain.Container{State: "pulling"}
pullReader, err := a.apiClient.ImagePull(ctx, imageName, image.PullOptions{})
if err != nil {
return err
}
_, _ = io.Copy(io.Discard, pullReader)
_ = pullReader.Close()
a.mu.Lock()
a.pulledImages[imageName] = struct{}{}
a.mu.Unlock()
return nil
}
// runContainerLoop is the control loop for a single container. It returns only // runContainerLoop is the control loop for a single container. It returns only
// when the container exits. // when the container exits.
func (a *Client) runContainerLoop( func (a *Client) runContainerLoop(