mirror of
https://github.com/moby/moby.git
synced 2022-11-09 12:21:53 -05:00
Pin image by digest on service create and update
Signed-off-by: Nishant Totla <nishanttotla@gmail.com>
This commit is contained in:
parent
87075353dc
commit
764a9ed357
3 changed files with 114 additions and 29 deletions
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue