1
0
Fork 0
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:
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 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(

View file

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

View file

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