Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 35 additions & 3 deletions image/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ package image

import (
"errors"
"fmt"
"io"

ocispec "github.com/opencontainers/image-spec/specs-go/v1"

"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.
Expand Down Expand Up @@ -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.
Expand Down
6 changes: 6 additions & 0 deletions image/options_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
}
40 changes: 18 additions & 22 deletions image/pull.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
23 changes: 23 additions & 0 deletions image/pull_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}))
})
}
14 changes: 7 additions & 7 deletions image/pull_unit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,35 +58,35 @@ 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) {
mockCliWithLogger := &errMockCli{
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")
})
}