Skip to content

Commit

Permalink
docker: add image pull method (#4989)
Browse files Browse the repository at this point in the history
This is in preparation for directly creating+running containers
with Docker, where it's necessary to do a pull first to ensure
the image exists before creating the container (this is automatic
behavior from the CLI but not when using the API).
  • Loading branch information
milas committed Sep 28, 2021
1 parent 314b4ba commit c7f20fa
Show file tree
Hide file tree
Showing 4 changed files with 103 additions and 15 deletions.
101 changes: 86 additions & 15 deletions internal/docker/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,10 @@ import (
"sync"
"time"

"github.com/docker/cli/cli/connhelper"

"github.com/blang/semver"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/connhelper"
cliflags "github.com/docker/cli/cli/flags"
"github.com/docker/distribution/reference"
"github.com/docker/docker/api/types"
Expand Down Expand Up @@ -87,6 +86,7 @@ type Client interface {
// Returns an ExitError if the command exits with a non-zero exit code.
ExecInContainer(ctx context.Context, cID container.ID, cmd model.Cmd, in io.Reader, out io.Writer) error

ImagePull(ctx context.Context, ref reference.Named) (reference.Canonical, error)
ImagePush(ctx context.Context, image reference.NamedTagged) (io.ReadCloser, error)
ImageBuild(ctx context.Context, buildContext io.Reader, options BuildOptions) (types.ImageBuildResponse, error)
ImageTag(ctx context.Context, source, target string) error
Expand Down Expand Up @@ -384,41 +384,112 @@ func (c *Cli) ServerVersion() types.Version {
return c.serverVersion
}

func (c *Cli) ImagePush(ctx context.Context, ref reference.NamedTagged) (io.ReadCloser, error) {
repoInfo, err := registry.ParseRepositoryInfo(ref)
if err != nil {
return nil, errors.Wrap(err, "ImagePush#ParseRepositoryInfo")
}
type encodedAuth string

logger.Get(ctx).Infof("Authenticating to image repo: %s", repoInfo.Index.Name)
func (c *Cli) authInfo(ctx context.Context, repoInfo *registry.RepositoryInfo, cmdName string) (encodedAuth, types.RequestPrivilegeFunc, error) {
infoWriter := logger.Get(ctx).Writer(logger.InfoLvl)
cli, err := command.NewDockerCli(
command.WithCombinedStreams(infoWriter),
command.WithContentTrust(true),
)
if err != nil {
return nil, errors.Wrap(err, "ImagePush#NewDockerCli")
return "", nil, errors.Wrap(err, "authInfo#NewDockerCli")
}

err = cli.Initialize(cliflags.NewClientOptions())
if err != nil {
return nil, errors.Wrap(err, "ImagePush#InitializeCLI")
return "", nil, errors.Wrap(err, "authInfo#InitializeCLI")
}
authConfig := command.ResolveAuthConfig(ctx, cli, repoInfo.Index)
requestPrivilege := command.RegistryAuthenticationPrivilegedFunc(cli, repoInfo.Index, "push")
requestPrivilege := command.RegistryAuthenticationPrivilegedFunc(cli, repoInfo.Index, cmdName)

auth, err := command.EncodeAuthToBase64(authConfig)
if err != nil {
return "", nil, errors.Wrap(err, "authInfo#EncodeAuthToBase64")
}
return encodedAuth(auth), requestPrivilege, nil
}

func (c *Cli) ImagePull(ctx context.Context, ref reference.Named) (reference.Canonical, error) {
repoInfo, err := registry.ParseRepositoryInfo(ref)
if err != nil {
return nil, fmt.Errorf("could not parse registry for %q: %v", ref.String(), err)
}

encodedAuth, requestPrivilege, err := c.authInfo(ctx, repoInfo, "push")
if err != nil {
return nil, fmt.Errorf("could not authenticate: %v", err)
}

image := ref.String()
pullResp, err := c.Client.ImagePull(ctx, image, types.ImagePullOptions{
RegistryAuth: string(encodedAuth),
PrivilegeFunc: requestPrivilege,
})
if err != nil {
return nil, fmt.Errorf("could not pull image %q: %v", image, err)
}
defer func() {
_ = pullResp.Close()
}()

// the /images/create API is a bit chaotic, returning JSON lines of status as it pulls
// including ASCII progress bar animation etc.
// there's not really any guarantees with it, so the prevailing guidance is to try and
// inspect the image immediately afterwards to ensure it was pulled successfully
// (this is racy and could be improved by _trying_ to get the digest out of this response
// and making sure it matches with the result of inspect, but Docker itself suffers from
// this same race condition during a docker run that triggers a pull, so it's reasonable
// to deem it as acceptable here as well)
_, err = io.Copy(io.Discard, pullResp)
if err != nil {
return nil, fmt.Errorf("connection error while pulling image %q: %v", image, err)
}

imgInspect, _, err := c.ImageInspectWithRaw(ctx, image)
if err != nil {
return nil, fmt.Errorf("failed to inspect after pull for image %q: %v", image, err)
}

pulledRef, err := reference.ParseNormalizedNamed(imgInspect.RepoDigests[0])
if err != nil {
return nil, fmt.Errorf("invalid reference %q for image %q: %v", imgInspect.RepoDigests[0], image, err)
}
cRef, ok := pulledRef.(reference.Canonical)
if !ok {
// this indicates a bug/behavior change within Docker because we just parsed a digest reference
return nil, fmt.Errorf("reference %q is not canonical", pulledRef.String())
}
// the reference from the repo digest will be missing the tag (if specified), so we attach the digest to the
// original reference to get something like `docker.io/library/nginx:1.21.32@sha256:<hash>` for an input of
// `docker.io/library/nginx:1.21.3` (if we used the repo digest, it'd be `docker.io/library/nginx@sha256:<hash>`
// with no tag, so this ensures all parts are preserved).
cRef, err = reference.WithDigest(ref, cRef.Digest())
if err != nil {
return nil, fmt.Errorf("invalid digest for reference %q: %v", pulledRef.String(), err)
}
return cRef, nil
}

encodedAuth, err := command.EncodeAuthToBase64(authConfig)
func (c *Cli) ImagePush(ctx context.Context, ref reference.NamedTagged) (io.ReadCloser, error) {
repoInfo, err := registry.ParseRepositoryInfo(ref)
if err != nil {
return nil, errors.Wrap(err, "ImagePush#ParseRepositoryInfo")
}

logger.Get(ctx).Infof("Authenticating to image repo: %s", repoInfo.Index.Name)
encodedAuth, requestPrivilege, err := c.authInfo(ctx, repoInfo, "push")
if err != nil {
return nil, errors.Wrap(err, "ImagePush#EncodeAuthToBase64")
return nil, errors.Wrap(err, "ImagePush: authenticate")
}

options := types.ImagePushOptions{
RegistryAuth: encodedAuth,
RegistryAuth: string(encodedAuth),
PrivilegeFunc: requestPrivilege,
}

if reference.Domain(ref) == "" {
return nil, errors.Wrap(err, "ImagePush: no domain in container name")
return nil, errors.New("ImagePush: no domain in container name")
}
logger.Get(ctx).Infof("Sending image data")
return c.Client.ImagePush(ctx, ref.String(), options)
Expand Down
3 changes: 3 additions & 0 deletions internal/docker/exploding.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ func (c explodingClient) ContainerRestartNoWait(ctx context.Context, containerID
func (c explodingClient) ExecInContainer(ctx context.Context, cID container.ID, cmd model.Cmd, in io.Reader, out io.Writer) error {
return c.err
}
func (c explodingClient) ImagePull(_ context.Context, _ reference.Named) (reference.Canonical, error) {
return nil, c.err
}
func (c explodingClient) ImagePush(ctx context.Context, ref reference.NamedTagged) (io.ReadCloser, error) {
return nil, c.err
}
Expand Down
9 changes: 9 additions & 0 deletions internal/docker/fake_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"time"

"github.com/docker/go-units"
"github.com/opencontainers/go-digest"
"github.com/pkg/errors"

"github.com/docker/distribution/reference"
Expand Down Expand Up @@ -133,6 +134,8 @@ type FakeClient struct {
ContainersPruned []string
}

var _ Client = &FakeClient{}

func NewFakeClient() *FakeClient {
return &FakeClient{
PushOutput: ExamplePushOutput1,
Expand Down Expand Up @@ -228,6 +231,12 @@ func (c *FakeClient) ExecInContainer(ctx context.Context, cID container.ID, cmd
return err
}

func (c *FakeClient) ImagePull(_ context.Context, ref reference.Named) (reference.Canonical, error) {
// fake digest is the reference itself hashed
// i.e. docker.io/library/_/nginx -> sha256sum(docker.io/library/_/nginx) -> 2ca21a92e8ee99f672764b7619a413019de5ffc7f06dbc7422d41eca17705802
return reference.WithDigest(ref, digest.FromString(ref.String()))
}

func (c *FakeClient) ImagePush(ctx context.Context, ref reference.NamedTagged) (io.ReadCloser, error) {
c.PushCount++
c.PushImage = ref.String()
Expand Down
5 changes: 5 additions & 0 deletions internal/docker/switch.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ type switchCli struct {
mu sync.Mutex
}

var _ Client = &switchCli{}

func ProvideSwitchCli(clusterCli ClusterClient, localCli LocalClient) *switchCli {
return &switchCli{
localCli: localCli,
Expand Down Expand Up @@ -69,6 +71,9 @@ func (c *switchCli) ContainerRestartNoWait(ctx context.Context, containerID stri
func (c *switchCli) ExecInContainer(ctx context.Context, cID container.ID, cmd model.Cmd, in io.Reader, out io.Writer) error {
return c.client().ExecInContainer(ctx, cID, cmd, in, out)
}
func (c *switchCli) ImagePull(ctx context.Context, ref reference.Named) (reference.Canonical, error) {
return c.client().ImagePull(ctx, ref)
}
func (c *switchCli) ImagePush(ctx context.Context, ref reference.NamedTagged) (io.ReadCloser, error) {
return c.client().ImagePush(ctx, ref)
}
Expand Down

0 comments on commit c7f20fa

Please sign in to comment.