diff --git a/image/options.go b/image/options.go index 79bc2e7d..f5e4b971 100644 --- a/image/options.go +++ b/image/options.go @@ -2,6 +2,7 @@ package image import ( "errors" + "fmt" "io" ocispec "github.com/opencontainers/image-spec/specs-go/v1" @@ -9,6 +10,7 @@ import ( "github.com/docker/docker/api/types/build" "github.com/docker/docker/api/types/image" "github.com/docker/go-sdk/client" + "github.com/docker/go-sdk/config" ) // BuildOption is a function that configures the build options. @@ -40,9 +42,39 @@ func WithBuildOptions(options build.ImageBuildOptions) BuildOption { type PullOption func(*pullOptions) error type pullOptions struct { - client client.SDKClient - pullOptions image.PullOptions - pullHandler func(r io.ReadCloser) error + client client.SDKClient + pullOptions image.PullOptions + pullHandler func(r io.ReadCloser) error + credentialsFn func(string) (string, string, error) +} + +// WithCredentialsFn sets the function to retrieve credentials for an image to be pulled +func WithCredentialsFn(credentialsFn func(string) (string, string, error)) PullOption { + return func(opts *pullOptions) error { + opts.credentialsFn = credentialsFn + return nil + } +} + +// WithCredentialsFromConfig configures pull to retrieve credentials from the CLI config +func WithCredentialsFromConfig(opts *pullOptions) error { + opts.credentialsFn = func(imageName string) (string, string, error) { + authConfigs, err := config.AuthConfigs(imageName) + if err != nil { + return "", "", err + } + + // there must be only one auth config for the image + if len(authConfigs) > 1 { + return "", "", fmt.Errorf("multiple auth configs found for image %s, expected only one", imageName) + } + + for _, ac := range authConfigs { + return ac.Username, ac.Password, nil + } + return "", "", nil + } + return nil } // WithPullClient sets the pull client used to pull the image. diff --git a/image/options_test.go b/image/options_test.go index 93c07337..047b610e 100644 --- a/image/options_test.go +++ b/image/options_test.go @@ -28,4 +28,10 @@ func TestWithOptions(t *testing.T) { require.NoError(t, err) require.Equal(t, opts, pullOpts.pullOptions) }) + + t.Run("with-credentials-from-config", func(t *testing.T) { + opts := &pullOptions{} + err := WithCredentialsFromConfig(opts) + require.NoError(t, err) + }) } diff --git a/image/pull.go b/image/pull.go index df5c6b3d..25921284 100644 --- a/image/pull.go +++ b/image/pull.go @@ -46,34 +46,30 @@ func Pull(ctx context.Context, imageName string, opts ...PullOption) error { pullOpts.client = sdk } + if pullOpts.credentialsFn == nil { + if err := WithCredentialsFromConfig(pullOpts); err != nil { + return fmt.Errorf("set credentials for pull option: %w", err) + } + } + if imageName == "" { return errors.New("image name is not set") } - authConfigs, err := config.AuthConfigs(imageName) + username, password, err := pullOpts.credentialsFn(imageName) if err != nil { - pullOpts.client.Logger().Warn("failed to get image auth, setting empty credentials for the image", "image", imageName, "error", err) - } else { - // there must be only one auth config for the image - if len(authConfigs) > 1 { - return fmt.Errorf("multiple auth configs found for image %s, expected only one", imageName) - } - - var tmp config.AuthConfig - for _, ac := range authConfigs { - tmp = ac - } + return fmt.Errorf("failed to retrieve registry credentials for %s: %w", imageName, err) + } - authConfig := config.AuthConfig{ - Username: tmp.Username, - Password: tmp.Password, - } - encodedJSON, err := json.Marshal(authConfig) - if err != nil { - pullOpts.client.Logger().Warn("failed to marshal image auth, setting empty credentials for the image", "image", imageName, "error", err) - } else { - pullOpts.pullOptions.RegistryAuth = base64.URLEncoding.EncodeToString(encodedJSON) - } + authConfig := config.AuthConfig{ + Username: username, + Password: password, + } + encodedJSON, err := json.Marshal(authConfig) + if err != nil { + pullOpts.client.Logger().Warn("failed to marshal image auth, setting empty credentials for the image", "image", imageName, "error", err) + } else { + pullOpts.pullOptions.RegistryAuth = base64.URLEncoding.EncodeToString(encodedJSON) } var pull io.ReadCloser diff --git a/image/pull_test.go b/image/pull_test.go index 24df4ce1..bf2977c1 100644 --- a/image/pull_test.go +++ b/image/pull_test.go @@ -78,4 +78,27 @@ func TestPull(t *testing.T) { require.Contains(t, buf.String(), "Pulling from library/nginx") }) + + t.Run("with-credentials-fn/success", func(t *testing.T) { + cli, err := client.New(context.Background()) + require.NoError(t, err) + defer cli.Close() + + pull(t, cli, nil, image.WithCredentialsFn(func(_ string) (string, string, error) { + // no credentials because the image is public + return "", "", nil + })) + }) + + t.Run("with-credentials-fn/error", func(t *testing.T) { + cli, err := client.New(context.Background()) + require.NoError(t, err) + defer cli.Close() + + expectedErr := errors.New("test error") + + pull(t, cli, expectedErr, image.WithCredentialsFn(func(_ string) (string, string, error) { + return "test", "test", expectedErr + })) + }) } diff --git a/image/pull_unit_test.go b/image/pull_unit_test.go index 01b687af..732943d4 100644 --- a/image/pull_unit_test.go +++ b/image/pull_unit_test.go @@ -58,27 +58,27 @@ func TestPull(t *testing.T) { }) t.Run("success/no-retry", func(t *testing.T) { - testPull(t, "someTag", defaultPullOpts, &errMockCli{err: nil}, false) + testPull(t, "some_tag", defaultPullOpts, &errMockCli{err: nil}, false) }) t.Run("not-available/no-retry", func(t *testing.T) { - testPull(t, "someTag", defaultPullOpts, &errMockCli{err: errdefs.ErrNotFound.WithMessage("not available")}, false) + testPull(t, "some_tag", defaultPullOpts, &errMockCli{err: errdefs.ErrNotFound.WithMessage("not available")}, false) }) t.Run("invalid-parameters/no-retry", func(t *testing.T) { - testPull(t, "someTag", defaultPullOpts, &errMockCli{err: errdefs.ErrInvalidArgument.WithMessage("invalid")}, false) + testPull(t, "some_tag", defaultPullOpts, &errMockCli{err: errdefs.ErrInvalidArgument.WithMessage("invalid")}, false) }) t.Run("unauthorized/retry", func(t *testing.T) { - testPull(t, "someTag", defaultPullOpts, &errMockCli{err: errdefs.ErrUnauthenticated.WithMessage("not authorized")}, false) + testPull(t, "some_tag", defaultPullOpts, &errMockCli{err: errdefs.ErrUnauthenticated.WithMessage("not authorized")}, false) }) t.Run("forbidden/retry", func(t *testing.T) { - testPull(t, "someTag", defaultPullOpts, &errMockCli{err: errdefs.ErrPermissionDenied.WithMessage("forbidden")}, false) + testPull(t, "some_tag", defaultPullOpts, &errMockCli{err: errdefs.ErrPermissionDenied.WithMessage("forbidden")}, false) }) t.Run("not-implemented/retry", func(t *testing.T) { - testPull(t, "someTag", defaultPullOpts, &errMockCli{err: errdefs.ErrNotImplemented.WithMessage("unknown method")}, false) + testPull(t, "some_tag", defaultPullOpts, &errMockCli{err: errdefs.ErrNotImplemented.WithMessage("unknown method")}, false) }) t.Run("non-permanent-error/retry", func(t *testing.T) { @@ -86,7 +86,7 @@ func TestPull(t *testing.T) { err: errors.New("whoops"), } - out := testPull(t, "someTag", defaultPullOpts, mockCliWithLogger, true) + out := testPull(t, "some_tag", defaultPullOpts, mockCliWithLogger, true) require.Contains(t, out, "failed to pull image, will retry") }) }