diff --git a/daemon/cluster/cluster.go b/daemon/cluster/cluster.go index 7814b02167..a5a36895aa 100644 --- a/daemon/cluster/cluster.go +++ b/daemon/cluster/cluster.go @@ -1,6 +1,7 @@ package cluster import ( + "encoding/base64" "encoding/json" "fmt" "io/ioutil" @@ -26,6 +27,7 @@ import ( "github.com/docker/docker/opts" "github.com/docker/docker/pkg/ioutils" "github.com/docker/docker/pkg/signal" + "github.com/docker/docker/reference" "github.com/docker/docker/runconfig" swarmapi "github.com/docker/swarmkit/api" swarmnode "github.com/docker/swarmkit/node" @@ -871,6 +873,46 @@ func (c *Cluster) GetServices(options apitypes.ServiceListOptions) ([]types.Serv return services, nil } +// imageWithDigestString takes an image such as name or name:tag +// and returns the image pinned to a digest, such as name@sha256:34234... +func (c *Cluster) imageWithDigestString(ctx context.Context, image string, authConfig *apitypes.AuthConfig) (string, error) { + ref, err := reference.ParseNamed(image) + if err != nil { + return "", err + } + // only query registry if not a canonical reference (i.e. with digest) + if _, ok := ref.(reference.Canonical); !ok { + ref = reference.WithDefaultTag(ref) + + namedTaggedRef, ok := ref.(reference.NamedTagged) + if !ok { + return "", fmt.Errorf("unable to cast image to NamedTagged reference object") + } + + repo, _, err := c.config.Backend.GetRepository(ctx, namedTaggedRef, authConfig) + if err != nil { + return "", err + } + dscrptr, err := repo.Tags(ctx).Get(ctx, namedTaggedRef.Tag()) + if err != nil { + return "", err + } + + // TODO(nishanttotla): Currently, the service would lose the tag while calling WithDigest + // To prevent this, we create the image string manually, which is a bad idea in general + // This will be fixed when https://github.com/docker/distribution/pull/2044 is vendored + // namedDigestedRef, err := reference.WithDigest(ref, dscrptr.Digest) + // if err != nil { + // return "", err + // } + // return namedDigestedRef.String(), nil + return image + "@" + dscrptr.Digest.String(), nil + } else { + // reference already contains a digest, so just return it + return ref.String(), nil + } +} + // CreateService creates a new service in a managed swarm cluster. func (c *Cluster) CreateService(s types.ServiceSpec, encodedAuth string) (string, error) { c.RLock() @@ -893,14 +935,33 @@ func (c *Cluster) CreateService(s types.ServiceSpec, encodedAuth string) (string return "", err } + ctnr := serviceSpec.Task.GetContainer() + if ctnr == nil { + return "", fmt.Errorf("service does not use container tasks") + } + if encodedAuth != "" { - ctnr := serviceSpec.Task.GetContainer() - if ctnr == nil { - return "", fmt.Errorf("service does not use container tasks") - } ctnr.PullOptions = &swarmapi.ContainerSpec_PullOptions{RegistryAuth: encodedAuth} } + // retrieve auth config from encoded auth + authConfig := &apitypes.AuthConfig{} + if encodedAuth != "" { + if err := json.NewDecoder(base64.NewDecoder(base64.URLEncoding, strings.NewReader(encodedAuth))).Decode(authConfig); err != nil { + logrus.Warnf("invalid authconfig: %v", err) + } + } + // pin image by digest + if os.Getenv("DOCKER_SERVICE_PREFER_OFFLINE_IMAGE") != "1" { + digestImage, err := c.imageWithDigestString(ctx, ctnr.Image, authConfig) + if err != nil { + logrus.Warnf("unable to pin image %s to digest: %s", ctnr.Image, err.Error()) + } else { + logrus.Debugf("pinning image %s by digest: %s", ctnr.Image, digestImage) + ctnr.Image = digestImage + } + } + r, err := c.client.CreateService(ctx, &swarmapi.CreateServiceRequest{Spec: &serviceSpec}) if err != nil { return "", err @@ -955,12 +1016,13 @@ func (c *Cluster) UpdateService(serviceIDOrName string, version uint64, spec typ return err } + newCtnr := serviceSpec.Task.GetContainer() + if newCtnr == nil { + return fmt.Errorf("service does not use container tasks") + } + if encodedAuth != "" { - ctnr := serviceSpec.Task.GetContainer() - if ctnr == nil { - return fmt.Errorf("service does not use container tasks") - } - ctnr.PullOptions = &swarmapi.ContainerSpec_PullOptions{RegistryAuth: encodedAuth} + newCtnr.PullOptions = &swarmapi.ContainerSpec_PullOptions{RegistryAuth: encodedAuth} } else { // this is needed because if the encodedAuth isn't being updated then we // shouldn't lose it, and continue to use the one that was already present @@ -979,7 +1041,29 @@ func (c *Cluster) UpdateService(serviceIDOrName string, version uint64, spec typ if ctnr == nil { return fmt.Errorf("service does not use container tasks") } - serviceSpec.Task.GetContainer().PullOptions = ctnr.PullOptions + newCtnr.PullOptions = ctnr.PullOptions + // update encodedAuth so it can be used to pin image by digest + if ctnr.PullOptions != nil { + encodedAuth = ctnr.PullOptions.RegistryAuth + } + } + + // retrieve auth config from encoded auth + authConfig := &apitypes.AuthConfig{} + if encodedAuth != "" { + if err := json.NewDecoder(base64.NewDecoder(base64.URLEncoding, strings.NewReader(encodedAuth))).Decode(authConfig); err != nil { + logrus.Warnf("invalid authconfig: %v", err) + } + } + // pin image by digest + if os.Getenv("DOCKER_SERVICE_PREFER_OFFLINE_IMAGE") != "1" { + digestImage, err := c.imageWithDigestString(ctx, newCtnr.Image, authConfig) + if err != nil { + logrus.Warnf("unable to pin image %s to digest: %s", newCtnr.Image, err.Error()) + } else if newCtnr.Image != digestImage { + logrus.Debugf("pinning image %s by digest: %s", newCtnr.Image, digestImage) + newCtnr.Image = digestImage + } } _, err = c.client.UpdateService( diff --git a/daemon/cluster/executor/backend.go b/daemon/cluster/executor/backend.go index a05673d92e..3af5dfbdb0 100644 --- a/daemon/cluster/executor/backend.go +++ b/daemon/cluster/executor/backend.go @@ -4,6 +4,7 @@ import ( "io" "time" + "github.com/docker/distribution" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/events" @@ -45,5 +46,5 @@ type Backend interface { UnsubscribeFromEvents(listener chan interface{}) UpdateAttachment(string, string, string, *network.NetworkingConfig) error WaitForDetachment(context.Context, string, string, string, string) error - ResolveTagToDigest(context.Context, reference.NamedTagged, *types.AuthConfig) (string, error) + GetRepository(context.Context, reference.NamedTagged, *types.AuthConfig) (distribution.Repository, bool, error) } diff --git a/daemon/image_pull.go b/daemon/image_pull.go index 32e3835cb0..f0b8f55701 100644 --- a/daemon/image_pull.go +++ b/daemon/image_pull.go @@ -4,6 +4,7 @@ import ( "io" "strings" + dist "github.com/docker/distribution" "github.com/docker/distribution/digest" "github.com/docker/docker/api/types" "github.com/docker/docker/builder" @@ -105,40 +106,39 @@ func (daemon *Daemon) pullImageWithReference(ctx context.Context, ref reference. return err } -func (daemon *Daemon) ResolveTagToDigest(ctx context.Context, ref reference.NamedTagged, authConfig *types.AuthConfig) (string, error) { +func (daemon *Daemon) GetRepository(ctx context.Context, ref reference.NamedTagged, authConfig *types.AuthConfig) (dist.Repository, bool, error) { // get repository info repoInfo, err := daemon.RegistryService.ResolveRepository(ref) if err != nil { - return "", err + return nil, false, err } // makes sure name is not empty or `scratch` if err := distribution.ValidateRepoName(repoInfo.Name()); err != nil { - return "", err + return nil, false, err } // get endpoints endpoints, err := daemon.RegistryService.LookupPullEndpoints(repoInfo.Hostname()) if err != nil { - return "", err + return nil, false, err } // retrieve repository - // TODO(nishanttotla): More sophisticated selection of endpoint - repo, confirmedV2, err := distribution.NewV2Repository(ctx, repoInfo, endpoints[0], nil, authConfig, "pull") + var ( + confirmedV2 bool + repository dist.Repository + lastError error + ) - if err != nil { - return "", err - } - digest := "" + for _, endpoint := range endpoints { + if endpoint.Version == registry.APIVersion1 { + continue + } - // only retrieve digest if the repo is v2 - if confirmedV2 { - dscrptr, err := repo.Tags(ctx).Get(ctx, ref.Tag()) - if err != nil { - return "", err - } else { - digest = dscrptr.Digest.String() + repository, confirmedV2, lastError = distribution.NewV2Repository(ctx, repoInfo, endpoint, nil, authConfig, "pull") + if lastError == nil && confirmedV2 { + break } } - return digest, nil + return repository, confirmedV2, lastError }