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
|
package cluster
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
@ -26,6 +27,7 @@ import (
|
||||||
"github.com/docker/docker/opts"
|
"github.com/docker/docker/opts"
|
||||||
"github.com/docker/docker/pkg/ioutils"
|
"github.com/docker/docker/pkg/ioutils"
|
||||||
"github.com/docker/docker/pkg/signal"
|
"github.com/docker/docker/pkg/signal"
|
||||||
|
"github.com/docker/docker/reference"
|
||||||
"github.com/docker/docker/runconfig"
|
"github.com/docker/docker/runconfig"
|
||||||
swarmapi "github.com/docker/swarmkit/api"
|
swarmapi "github.com/docker/swarmkit/api"
|
||||||
swarmnode "github.com/docker/swarmkit/node"
|
swarmnode "github.com/docker/swarmkit/node"
|
||||||
|
@ -871,6 +873,46 @@ func (c *Cluster) GetServices(options apitypes.ServiceListOptions) ([]types.Serv
|
||||||
return services, nil
|
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.
|
// CreateService creates a new service in a managed swarm cluster.
|
||||||
func (c *Cluster) CreateService(s types.ServiceSpec, encodedAuth string) (string, error) {
|
func (c *Cluster) CreateService(s types.ServiceSpec, encodedAuth string) (string, error) {
|
||||||
c.RLock()
|
c.RLock()
|
||||||
|
@ -893,14 +935,33 @@ func (c *Cluster) CreateService(s types.ServiceSpec, encodedAuth string) (string
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
if encodedAuth != "" {
|
|
||||||
ctnr := serviceSpec.Task.GetContainer()
|
ctnr := serviceSpec.Task.GetContainer()
|
||||||
if ctnr == nil {
|
if ctnr == nil {
|
||||||
return "", fmt.Errorf("service does not use container tasks")
|
return "", fmt.Errorf("service does not use container tasks")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if encodedAuth != "" {
|
||||||
ctnr.PullOptions = &swarmapi.ContainerSpec_PullOptions{RegistryAuth: encodedAuth}
|
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})
|
r, err := c.client.CreateService(ctx, &swarmapi.CreateServiceRequest{Spec: &serviceSpec})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
|
@ -955,12 +1016,13 @@ func (c *Cluster) UpdateService(serviceIDOrName string, version uint64, spec typ
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if encodedAuth != "" {
|
newCtnr := serviceSpec.Task.GetContainer()
|
||||||
ctnr := serviceSpec.Task.GetContainer()
|
if newCtnr == nil {
|
||||||
if ctnr == nil {
|
|
||||||
return fmt.Errorf("service does not use container tasks")
|
return fmt.Errorf("service does not use container tasks")
|
||||||
}
|
}
|
||||||
ctnr.PullOptions = &swarmapi.ContainerSpec_PullOptions{RegistryAuth: encodedAuth}
|
|
||||||
|
if encodedAuth != "" {
|
||||||
|
newCtnr.PullOptions = &swarmapi.ContainerSpec_PullOptions{RegistryAuth: encodedAuth}
|
||||||
} else {
|
} else {
|
||||||
// this is needed because if the encodedAuth isn't being updated then we
|
// 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
|
// 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 {
|
if ctnr == nil {
|
||||||
return fmt.Errorf("service does not use container tasks")
|
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(
|
_, err = c.client.UpdateService(
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/docker/distribution"
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/docker/docker/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
"github.com/docker/docker/api/types/events"
|
"github.com/docker/docker/api/types/events"
|
||||||
|
@ -45,5 +46,5 @@ type Backend interface {
|
||||||
UnsubscribeFromEvents(listener chan interface{})
|
UnsubscribeFromEvents(listener chan interface{})
|
||||||
UpdateAttachment(string, string, string, *network.NetworkingConfig) error
|
UpdateAttachment(string, string, string, *network.NetworkingConfig) error
|
||||||
WaitForDetachment(context.Context, string, string, string, string) 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"
|
"io"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
dist "github.com/docker/distribution"
|
||||||
"github.com/docker/distribution/digest"
|
"github.com/docker/distribution/digest"
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/docker/docker/builder"
|
"github.com/docker/docker/builder"
|
||||||
|
@ -105,40 +106,39 @@ func (daemon *Daemon) pullImageWithReference(ctx context.Context, ref reference.
|
||||||
return err
|
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
|
// get repository info
|
||||||
repoInfo, err := daemon.RegistryService.ResolveRepository(ref)
|
repoInfo, err := daemon.RegistryService.ResolveRepository(ref)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return nil, false, err
|
||||||
}
|
}
|
||||||
// makes sure name is not empty or `scratch`
|
// makes sure name is not empty or `scratch`
|
||||||
if err := distribution.ValidateRepoName(repoInfo.Name()); err != nil {
|
if err := distribution.ValidateRepoName(repoInfo.Name()); err != nil {
|
||||||
return "", err
|
return nil, false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// get endpoints
|
// get endpoints
|
||||||
endpoints, err := daemon.RegistryService.LookupPullEndpoints(repoInfo.Hostname())
|
endpoints, err := daemon.RegistryService.LookupPullEndpoints(repoInfo.Hostname())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return nil, false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// retrieve repository
|
// retrieve repository
|
||||||
// TODO(nishanttotla): More sophisticated selection of endpoint
|
var (
|
||||||
repo, confirmedV2, err := distribution.NewV2Repository(ctx, repoInfo, endpoints[0], nil, authConfig, "pull")
|
confirmedV2 bool
|
||||||
|
repository dist.Repository
|
||||||
|
lastError error
|
||||||
|
)
|
||||||
|
|
||||||
if err != nil {
|
for _, endpoint := range endpoints {
|
||||||
return "", err
|
if endpoint.Version == registry.APIVersion1 {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
digest := ""
|
|
||||||
|
|
||||||
// only retrieve digest if the repo is v2
|
repository, confirmedV2, lastError = distribution.NewV2Repository(ctx, repoInfo, endpoint, nil, authConfig, "pull")
|
||||||
if confirmedV2 {
|
if lastError == nil && confirmedV2 {
|
||||||
dscrptr, err := repo.Tags(ctx).Get(ctx, ref.Tag())
|
break
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
} else {
|
|
||||||
digest = dscrptr.Digest.String()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return digest, nil
|
return repository, confirmedV2, lastError
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue