Skip to content

Commit 7631aab

Browse files
committed
Merge branch 'fix/pull-image-not-found' into 'master'
fix(retrieval): pull image when inspect returns wrapped not-found Closes #700 See merge request postgres-ai/database-lab!1129
2 parents f1d1140 + 6fce587 commit 7631aab

4 files changed

Lines changed: 106 additions & 10 deletions

File tree

engine/internal/retrieval/engine/postgres/logical/dump.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,7 @@ func (d *DumpJob) Run(ctx context.Context) (err error) {
286286
}
287287

288288
if err := tools.PullImage(ctx, d.dockerClient, d.DockerImage); err != nil {
289-
return errors.Wrap(err, "failed to scan pulling image response")
289+
return fmt.Errorf("failed to prepare dump image %q: %w", d.DockerImage, err)
290290
}
291291

292292
if err := os.MkdirAll(d.DumpOptions.DumpLocation, 0666); err != nil {

engine/internal/retrieval/engine/postgres/tools/tools.go

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222

2323
"github.com/AlekSi/pointer"
2424
"github.com/ahmetalpbalkan/dlog"
25+
cerrdefs "github.com/containerd/errdefs"
2526
"github.com/docker/cli/cli/streams"
2627
"github.com/docker/docker/api/types"
2728
"github.com/docker/docker/api/types/container"
@@ -30,7 +31,6 @@ import (
3031
"github.com/docker/docker/api/types/mount"
3132
"github.com/docker/docker/api/types/network"
3233
"github.com/docker/docker/client"
33-
"github.com/docker/docker/errdefs"
3434
"github.com/docker/docker/pkg/jsonmessage"
3535
"github.com/docker/docker/pkg/stdcopy"
3636
"github.com/pkg/errors"
@@ -518,13 +518,18 @@ func RemoveContainer(ctx context.Context, dockerClient *client.Client, container
518518
log.Msg(fmt.Sprintf("Container %q has been removed", containerID))
519519
}
520520

521-
// PullImage pulls a Docker image.
522-
func PullImage(ctx context.Context, dockerClient *client.Client, image string) error {
521+
// imagePuller is the minimal subset of the Docker client used by PullImage.
522+
// Defined as an interface so PullImage can be unit-tested with a fake client.
523+
type imagePuller interface {
524+
ImageInspect(ctx context.Context, imageID string, opts ...client.ImageInspectOption) (imagetypes.InspectResponse, error)
525+
ImagePull(ctx context.Context, refStr string, options imagetypes.PullOptions) (io.ReadCloser, error)
526+
}
527+
528+
// PullImage pulls a Docker image if it is not already present locally.
529+
func PullImage(ctx context.Context, dockerClient imagePuller, image string) error {
523530
inspectionResult, err := dockerClient.ImageInspect(ctx, image)
524-
if err != nil {
525-
if _, ok := err.(errdefs.ErrNotFound); !ok {
526-
return errors.Wrapf(err, "failed to inspect image %q", image)
527-
}
531+
if err != nil && !cerrdefs.IsNotFound(err) {
532+
return errors.Wrapf(err, "failed to inspect image %q", image)
528533
}
529534

530535
if err == nil && inspectionResult.ID != "" {
@@ -540,7 +545,7 @@ func PullImage(ctx context.Context, dockerClient *client.Client, image string) e
540545
defer func() { _ = pullOutput.Close() }()
541546

542547
if err := jsonmessage.DisplayJSONMessagesToStream(pullOutput, streams.NewOut(os.Stdout), nil); err != nil {
543-
log.Err("failed to render pull image output: ", err)
548+
return errors.Wrapf(err, "failed to pull image %q", image)
544549
}
545550

546551
return nil

engine/internal/retrieval/engine/postgres/tools/tools_test.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,20 @@ package tools
66

77
import (
88
"bytes"
9+
"context"
10+
"errors"
11+
"fmt"
12+
"io"
913
"os"
1014
"path/filepath"
15+
"strings"
1116
"testing"
1217

18+
cerrdefs "github.com/containerd/errdefs"
1319
"github.com/docker/docker/api/types/container"
20+
imagetypes "github.com/docker/docker/api/types/image"
1421
"github.com/docker/docker/api/types/mount"
22+
"github.com/docker/docker/client"
1523
"github.com/stretchr/testify/assert"
1624
"github.com/stretchr/testify/require"
1725
)
@@ -317,3 +325,86 @@ func TestIsEmptyDirectory_NonExistent(t *testing.T) {
317325
_, err := IsEmptyDirectory("/nonexistent/path/12345")
318326
assert.Error(t, err)
319327
}
328+
329+
type inspectFn func(ctx context.Context, imageID string, opts ...client.ImageInspectOption) (imagetypes.InspectResponse, error)
330+
type pullFn func(ctx context.Context, refStr string, options imagetypes.PullOptions) (io.ReadCloser, error)
331+
332+
// fakePuller implements imagePuller for testing PullImage in isolation.
333+
type fakePuller struct {
334+
inspect inspectFn
335+
pull pullFn
336+
pullCalled bool
337+
}
338+
339+
func (f *fakePuller) ImageInspect(ctx context.Context, imageID string, opts ...client.ImageInspectOption) (imagetypes.InspectResponse, error) {
340+
if f.inspect != nil {
341+
return f.inspect(ctx, imageID, opts...)
342+
}
343+
return imagetypes.InspectResponse{}, errors.New("inspect not implemented")
344+
}
345+
346+
func (f *fakePuller) ImagePull(ctx context.Context, refStr string, options imagetypes.PullOptions) (io.ReadCloser, error) {
347+
f.pullCalled = true
348+
if f.pull != nil {
349+
return f.pull(ctx, refStr, options)
350+
}
351+
return nil, errors.New("pull not implemented")
352+
}
353+
354+
func TestPullImage(t *testing.T) {
355+
notFound := fmt.Errorf("no such image: %w", cerrdefs.ErrNotFound)
356+
wrappedNotFound := fmt.Errorf("inspect failed: %w", notFound)
357+
358+
localImage := func(context.Context, string, ...client.ImageInspectOption) (imagetypes.InspectResponse, error) {
359+
return imagetypes.InspectResponse{ID: "sha256:abc"}, nil
360+
}
361+
notFoundInspect := func(context.Context, string, ...client.ImageInspectOption) (imagetypes.InspectResponse, error) {
362+
return imagetypes.InspectResponse{}, notFound
363+
}
364+
wrappedNotFoundInspect := func(context.Context, string, ...client.ImageInspectOption) (imagetypes.InspectResponse, error) {
365+
return imagetypes.InspectResponse{}, wrappedNotFound
366+
}
367+
daemonDownInspect := func(context.Context, string, ...client.ImageInspectOption) (imagetypes.InspectResponse, error) {
368+
return imagetypes.InspectResponse{}, errors.New("daemon down")
369+
}
370+
371+
emptyStream := func(context.Context, string, imagetypes.PullOptions) (io.ReadCloser, error) {
372+
return io.NopCloser(strings.NewReader("")), nil
373+
}
374+
errorStream := func(context.Context, string, imagetypes.PullOptions) (io.ReadCloser, error) {
375+
body := `{"errorDetail":{"message":"manifest for postgresai/extended-postgres:99-bogus not found: manifest unknown"},"error":"manifest unknown"}`
376+
return io.NopCloser(strings.NewReader(body)), nil
377+
}
378+
379+
testCases := []struct {
380+
name string
381+
inspect inspectFn
382+
pull pullFn
383+
wantErr bool
384+
wantErrContains string
385+
wantPullCalled bool
386+
}{
387+
{name: "image already local skips pull", inspect: localImage},
388+
{name: "not found triggers pull", inspect: notFoundInspect, pull: emptyStream, wantPullCalled: true},
389+
{name: "wrapped not found triggers pull", inspect: wrappedNotFoundInspect, pull: emptyStream, wantPullCalled: true},
390+
{name: "non-not-found inspect error propagates", inspect: daemonDownInspect, wantErr: true, wantErrContains: "failed to inspect image"},
391+
{name: "pull stream error propagates", inspect: notFoundInspect, pull: errorStream, wantErr: true, wantErrContains: "failed to pull image", wantPullCalled: true},
392+
}
393+
394+
for _, tc := range testCases {
395+
t.Run(tc.name, func(t *testing.T) {
396+
puller := &fakePuller{inspect: tc.inspect, pull: tc.pull}
397+
398+
err := PullImage(context.Background(), puller, "postgresai/extended-postgres:test")
399+
400+
if tc.wantErr {
401+
require.Error(t, err)
402+
assert.Contains(t, err.Error(), tc.wantErrContains)
403+
} else {
404+
require.NoError(t, err)
405+
}
406+
407+
assert.Equal(t, tc.wantPullCalled, puller.pullCalled)
408+
})
409+
}
410+
}

engine/internal/runci/handlers.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ func (s *Server) runMigration(w http.ResponseWriter, r *http.Request) {
163163
func (s *Server) runCommands(ctx context.Context, clone *models.Clone, runID string, volumes, tags map[string]string,
164164
commands, migrationEnvs []string, cfg dblab_types.Config) (*observer.Session, error) {
165165
if err := tools.PullImage(ctx, s.docker, s.config.Runner.Image); err != nil {
166-
return nil, errors.Wrap(err, "failed to scan pulling image response")
166+
return nil, fmt.Errorf("failed to prepare runner image %q: %w", s.config.Runner.Image, err)
167167
}
168168

169169
containerCfg := s.buildContainerConfig(clone, migrationEnvs)

0 commit comments

Comments
 (0)