Browse code

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

Pin images by digest

Andrea Luzzardi authored on 2016/11/11 03:59:28
Showing 3 changed files
... ...
@@ -1,6 +1,7 @@
1 1
 package cluster
2 2
 
3 3
 import (
4
+	"encoding/base64"
4 5
 	"encoding/json"
5 6
 	"fmt"
6 7
 	"io/ioutil"
... ...
@@ -24,6 +25,7 @@ import (
24 24
 	"github.com/docker/docker/opts"
25 25
 	"github.com/docker/docker/pkg/ioutils"
26 26
 	"github.com/docker/docker/pkg/signal"
27
+	"github.com/docker/docker/reference"
27 28
 	"github.com/docker/docker/runconfig"
28 29
 	swarmapi "github.com/docker/swarmkit/api"
29 30
 	"github.com/docker/swarmkit/manager/encryption"
... ...
@@ -974,6 +976,46 @@ func (c *Cluster) GetServices(options apitypes.ServiceListOptions) ([]types.Serv
974 974
 	return services, nil
975 975
 }
976 976
 
977
+// imageWithDigestString takes an image such as name or name:tag
978
+// and returns the image pinned to a digest, such as name@sha256:34234...
979
+func (c *Cluster) imageWithDigestString(ctx context.Context, image string, authConfig *apitypes.AuthConfig) (string, error) {
980
+	ref, err := reference.ParseNamed(image)
981
+	if err != nil {
982
+		return "", err
983
+	}
984
+	// only query registry if not a canonical reference (i.e. with digest)
985
+	if _, ok := ref.(reference.Canonical); !ok {
986
+		ref = reference.WithDefaultTag(ref)
987
+
988
+		namedTaggedRef, ok := ref.(reference.NamedTagged)
989
+		if !ok {
990
+			return "", fmt.Errorf("unable to cast image to NamedTagged reference object")
991
+		}
992
+
993
+		repo, _, err := c.config.Backend.GetRepository(ctx, namedTaggedRef, authConfig)
994
+		if err != nil {
995
+			return "", err
996
+		}
997
+		dscrptr, err := repo.Tags(ctx).Get(ctx, namedTaggedRef.Tag())
998
+		if err != nil {
999
+			return "", err
1000
+		}
1001
+
1002
+		// TODO(nishanttotla): Currently, the service would lose the tag while calling WithDigest
1003
+		// To prevent this, we create the image string manually, which is a bad idea in general
1004
+		// This will be fixed when https://github.com/docker/distribution/pull/2044 is vendored
1005
+		// namedDigestedRef, err := reference.WithDigest(ref, dscrptr.Digest)
1006
+		// if err != nil {
1007
+		// 	return "", err
1008
+		// }
1009
+		// return namedDigestedRef.String(), nil
1010
+		return image + "@" + dscrptr.Digest.String(), nil
1011
+	} else {
1012
+		// reference already contains a digest, so just return it
1013
+		return ref.String(), nil
1014
+	}
1015
+}
1016
+
977 1017
 // CreateService creates a new service in a managed swarm cluster.
978 1018
 func (c *Cluster) CreateService(s types.ServiceSpec, encodedAuth string) (string, error) {
979 1019
 	c.RLock()
... ...
@@ -996,14 +1038,33 @@ func (c *Cluster) CreateService(s types.ServiceSpec, encodedAuth string) (string
996 996
 		return "", err
997 997
 	}
998 998
 
999
+	ctnr := serviceSpec.Task.GetContainer()
1000
+	if ctnr == nil {
1001
+		return "", fmt.Errorf("service does not use container tasks")
1002
+	}
1003
+
999 1004
 	if encodedAuth != "" {
1000
-		ctnr := serviceSpec.Task.GetContainer()
1001
-		if ctnr == nil {
1002
-			return "", fmt.Errorf("service does not use container tasks")
1003
-		}
1004 1005
 		ctnr.PullOptions = &swarmapi.ContainerSpec_PullOptions{RegistryAuth: encodedAuth}
1005 1006
 	}
1006 1007
 
1008
+	// retrieve auth config from encoded auth
1009
+	authConfig := &apitypes.AuthConfig{}
1010
+	if encodedAuth != "" {
1011
+		if err := json.NewDecoder(base64.NewDecoder(base64.URLEncoding, strings.NewReader(encodedAuth))).Decode(authConfig); err != nil {
1012
+			logrus.Warnf("invalid authconfig: %v", err)
1013
+		}
1014
+	}
1015
+	// pin image by digest
1016
+	if os.Getenv("DOCKER_SERVICE_PREFER_OFFLINE_IMAGE") != "1" {
1017
+		digestImage, err := c.imageWithDigestString(ctx, ctnr.Image, authConfig)
1018
+		if err != nil {
1019
+			logrus.Warnf("unable to pin image %s to digest: %s", ctnr.Image, err.Error())
1020
+		} else {
1021
+			logrus.Debugf("pinning image %s by digest: %s", ctnr.Image, digestImage)
1022
+			ctnr.Image = digestImage
1023
+		}
1024
+	}
1025
+
1007 1026
 	r, err := c.client.CreateService(ctx, &swarmapi.CreateServiceRequest{Spec: &serviceSpec})
1008 1027
 	if err != nil {
1009 1028
 		return "", err
... ...
@@ -1058,12 +1119,13 @@ func (c *Cluster) UpdateService(serviceIDOrName string, version uint64, spec typ
1058 1058
 		return err
1059 1059
 	}
1060 1060
 
1061
+	newCtnr := serviceSpec.Task.GetContainer()
1062
+	if newCtnr == nil {
1063
+		return fmt.Errorf("service does not use container tasks")
1064
+	}
1065
+
1061 1066
 	if encodedAuth != "" {
1062
-		ctnr := serviceSpec.Task.GetContainer()
1063
-		if ctnr == nil {
1064
-			return fmt.Errorf("service does not use container tasks")
1065
-		}
1066
-		ctnr.PullOptions = &swarmapi.ContainerSpec_PullOptions{RegistryAuth: encodedAuth}
1067
+		newCtnr.PullOptions = &swarmapi.ContainerSpec_PullOptions{RegistryAuth: encodedAuth}
1067 1068
 	} else {
1068 1069
 		// this is needed because if the encodedAuth isn't being updated then we
1069 1070
 		// 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
1082 1082
 		if ctnr == nil {
1083 1083
 			return fmt.Errorf("service does not use container tasks")
1084 1084
 		}
1085
-		serviceSpec.Task.GetContainer().PullOptions = ctnr.PullOptions
1085
+		newCtnr.PullOptions = ctnr.PullOptions
1086
+		// update encodedAuth so it can be used to pin image by digest
1087
+		if ctnr.PullOptions != nil {
1088
+			encodedAuth = ctnr.PullOptions.RegistryAuth
1089
+		}
1090
+	}
1091
+
1092
+	// retrieve auth config from encoded auth
1093
+	authConfig := &apitypes.AuthConfig{}
1094
+	if encodedAuth != "" {
1095
+		if err := json.NewDecoder(base64.NewDecoder(base64.URLEncoding, strings.NewReader(encodedAuth))).Decode(authConfig); err != nil {
1096
+			logrus.Warnf("invalid authconfig: %v", err)
1097
+		}
1098
+	}
1099
+	// pin image by digest
1100
+	if os.Getenv("DOCKER_SERVICE_PREFER_OFFLINE_IMAGE") != "1" {
1101
+		digestImage, err := c.imageWithDigestString(ctx, newCtnr.Image, authConfig)
1102
+		if err != nil {
1103
+			logrus.Warnf("unable to pin image %s to digest: %s", newCtnr.Image, err.Error())
1104
+		} else if newCtnr.Image != digestImage {
1105
+			logrus.Debugf("pinning image %s by digest: %s", newCtnr.Image, digestImage)
1106
+			newCtnr.Image = digestImage
1107
+		}
1086 1108
 	}
1087 1109
 
1088 1110
 	_, err = c.client.UpdateService(
... ...
@@ -4,12 +4,14 @@ import (
4 4
 	"io"
5 5
 	"time"
6 6
 
7
+	"github.com/docker/distribution"
7 8
 	"github.com/docker/docker/api/types"
8 9
 	"github.com/docker/docker/api/types/container"
9 10
 	"github.com/docker/docker/api/types/events"
10 11
 	"github.com/docker/docker/api/types/filters"
11 12
 	"github.com/docker/docker/api/types/network"
12 13
 	clustertypes "github.com/docker/docker/daemon/cluster/provider"
14
+	"github.com/docker/docker/reference"
13 15
 	"github.com/docker/libnetwork"
14 16
 	"github.com/docker/libnetwork/cluster"
15 17
 	networktypes "github.com/docker/libnetwork/types"
... ...
@@ -45,4 +47,5 @@ type Backend interface {
45 45
 	UnsubscribeFromEvents(listener chan interface{})
46 46
 	UpdateAttachment(string, string, string, *network.NetworkingConfig) error
47 47
 	WaitForDetachment(context.Context, string, string, string, string) error
48
+	GetRepository(context.Context, reference.NamedTagged, *types.AuthConfig) (distribution.Repository, bool, error)
48 49
 }
... ...
@@ -4,6 +4,7 @@ import (
4 4
 	"io"
5 5
 	"strings"
6 6
 
7
+	dist "github.com/docker/distribution"
7 8
 	"github.com/docker/distribution/digest"
8 9
 	"github.com/docker/docker/api/types"
9 10
 	"github.com/docker/docker/builder"
... ...
@@ -104,3 +105,40 @@ func (daemon *Daemon) pullImageWithReference(ctx context.Context, ref reference.
104 104
 	<-writesDone
105 105
 	return err
106 106
 }
107
+
108
+func (daemon *Daemon) GetRepository(ctx context.Context, ref reference.NamedTagged, authConfig *types.AuthConfig) (dist.Repository, bool, error) {
109
+	// get repository info
110
+	repoInfo, err := daemon.RegistryService.ResolveRepository(ref)
111
+	if err != nil {
112
+		return nil, false, err
113
+	}
114
+	// makes sure name is not empty or `scratch`
115
+	if err := distribution.ValidateRepoName(repoInfo.Name()); err != nil {
116
+		return nil, false, err
117
+	}
118
+
119
+	// get endpoints
120
+	endpoints, err := daemon.RegistryService.LookupPullEndpoints(repoInfo.Hostname())
121
+	if err != nil {
122
+		return nil, false, err
123
+	}
124
+
125
+	// retrieve repository
126
+	var (
127
+		confirmedV2 bool
128
+		repository  dist.Repository
129
+		lastError   error
130
+	)
131
+
132
+	for _, endpoint := range endpoints {
133
+		if endpoint.Version == registry.APIVersion1 {
134
+			continue
135
+		}
136
+
137
+		repository, confirmedV2, lastError = distribution.NewV2Repository(ctx, repoInfo, endpoint, nil, authConfig, "pull")
138
+		if lastError == nil && confirmedV2 {
139
+			break
140
+		}
141
+	}
142
+	return repository, confirmedV2, lastError
143
+}