Pin image by digest on service create and update

Signed-off-by: Nishant Totla <nishanttotla@gmail.com>
This commit is contained in:
Nishant Totla 2016-11-08 09:32:29 -08:00
parent 87075353dc
commit 764a9ed357
No known key found for this signature in database
GPG Key ID: 7EA5781C9B3D0C19
3 changed files with 114 additions and 29 deletions

View File

@ -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(

View File

@ -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)
}

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"
@ -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
}