Merge pull request #28173 from nishanttotla/pin-images-by-digest

Pin images by digest
This commit is contained in:
Andrea Luzzardi 2016-11-10 10:59:28 -08:00 committed by GitHub
commit c13bf5ba51
3 changed files with 135 additions and 10 deletions

View File

@ -1,6 +1,7 @@
package cluster
import (
"encoding/base64"
"encoding/json"
"fmt"
"io/ioutil"
@ -24,6 +25,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"
"github.com/docker/swarmkit/manager/encryption"
@ -974,6 +976,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()
@ -996,14 +1038,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
@ -1058,12 +1119,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
@ -1082,7 +1144,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(

View File

@ -4,12 +4,14 @@ 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"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/network"
clustertypes "github.com/docker/docker/daemon/cluster/provider"
"github.com/docker/docker/reference"
"github.com/docker/libnetwork"
"github.com/docker/libnetwork/cluster"
networktypes "github.com/docker/libnetwork/types"
@ -45,4 +47,5 @@ type Backend interface {
UnsubscribeFromEvents(listener chan interface{})
UpdateAttachment(string, string, string, *network.NetworkingConfig) error
WaitForDetachment(context.Context, string, string, string, string) error
GetRepository(context.Context, reference.NamedTagged, *types.AuthConfig) (distribution.Repository, bool, error)
}

View File

@ -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"
@ -104,3 +105,40 @@ func (daemon *Daemon) pullImageWithReference(ctx context.Context, ref reference.
<-writesDone
return err
}
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 nil, false, err
}
// makes sure name is not empty or `scratch`
if err := distribution.ValidateRepoName(repoInfo.Name()); err != nil {
return nil, false, err
}
// get endpoints
endpoints, err := daemon.RegistryService.LookupPullEndpoints(repoInfo.Hostname())
if err != nil {
return nil, false, err
}
// retrieve repository
var (
confirmedV2 bool
repository dist.Repository
lastError error
)
for _, endpoint := range endpoints {
if endpoint.Version == registry.APIVersion1 {
continue
}
repository, confirmedV2, lastError = distribution.NewV2Repository(ctx, repoInfo, endpoint, nil, authConfig, "pull")
if lastError == nil && confirmedV2 {
break
}
}
return repository, confirmedV2, lastError
}