Browse code

Pin image by digest on service create and update

Signed-off-by: Nishant Totla <nishanttotla@gmail.com>

Nishant Totla authored on 2016/11/09 02:32:29
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"
... ...
@@ -26,6 +27,7 @@ import (
26 26
 	"github.com/docker/docker/opts"
27 27
 	"github.com/docker/docker/pkg/ioutils"
28 28
 	"github.com/docker/docker/pkg/signal"
29
+	"github.com/docker/docker/reference"
29 30
 	"github.com/docker/docker/runconfig"
30 31
 	swarmapi "github.com/docker/swarmkit/api"
31 32
 	swarmnode "github.com/docker/swarmkit/node"
... ...
@@ -871,6 +873,46 @@ func (c *Cluster) GetServices(options apitypes.ServiceListOptions) ([]types.Serv
871 871
 	return services, nil
872 872
 }
873 873
 
874
+// imageWithDigestString takes an image such as name or name:tag
875
+// and returns the image pinned to a digest, such as name@sha256:34234...
876
+func (c *Cluster) imageWithDigestString(ctx context.Context, image string, authConfig *apitypes.AuthConfig) (string, error) {
877
+	ref, err := reference.ParseNamed(image)
878
+	if err != nil {
879
+		return "", err
880
+	}
881
+	// only query registry if not a canonical reference (i.e. with digest)
882
+	if _, ok := ref.(reference.Canonical); !ok {
883
+		ref = reference.WithDefaultTag(ref)
884
+
885
+		namedTaggedRef, ok := ref.(reference.NamedTagged)
886
+		if !ok {
887
+			return "", fmt.Errorf("unable to cast image to NamedTagged reference object")
888
+		}
889
+
890
+		repo, _, err := c.config.Backend.GetRepository(ctx, namedTaggedRef, authConfig)
891
+		if err != nil {
892
+			return "", err
893
+		}
894
+		dscrptr, err := repo.Tags(ctx).Get(ctx, namedTaggedRef.Tag())
895
+		if err != nil {
896
+			return "", err
897
+		}
898
+
899
+		// TODO(nishanttotla): Currently, the service would lose the tag while calling WithDigest
900
+		// To prevent this, we create the image string manually, which is a bad idea in general
901
+		// This will be fixed when https://github.com/docker/distribution/pull/2044 is vendored
902
+		// namedDigestedRef, err := reference.WithDigest(ref, dscrptr.Digest)
903
+		// if err != nil {
904
+		// 	return "", err
905
+		// }
906
+		// return namedDigestedRef.String(), nil
907
+		return image + "@" + dscrptr.Digest.String(), nil
908
+	} else {
909
+		// reference already contains a digest, so just return it
910
+		return ref.String(), nil
911
+	}
912
+}
913
+
874 914
 // CreateService creates a new service in a managed swarm cluster.
875 915
 func (c *Cluster) CreateService(s types.ServiceSpec, encodedAuth string) (string, error) {
876 916
 	c.RLock()
... ...
@@ -893,14 +935,33 @@ func (c *Cluster) CreateService(s types.ServiceSpec, encodedAuth string) (string
893 893
 		return "", err
894 894
 	}
895 895
 
896
+	ctnr := serviceSpec.Task.GetContainer()
897
+	if ctnr == nil {
898
+		return "", fmt.Errorf("service does not use container tasks")
899
+	}
900
+
896 901
 	if encodedAuth != "" {
897
-		ctnr := serviceSpec.Task.GetContainer()
898
-		if ctnr == nil {
899
-			return "", fmt.Errorf("service does not use container tasks")
900
-		}
901 902
 		ctnr.PullOptions = &swarmapi.ContainerSpec_PullOptions{RegistryAuth: encodedAuth}
902 903
 	}
903 904
 
905
+	// retrieve auth config from encoded auth
906
+	authConfig := &apitypes.AuthConfig{}
907
+	if encodedAuth != "" {
908
+		if err := json.NewDecoder(base64.NewDecoder(base64.URLEncoding, strings.NewReader(encodedAuth))).Decode(authConfig); err != nil {
909
+			logrus.Warnf("invalid authconfig: %v", err)
910
+		}
911
+	}
912
+	// pin image by digest
913
+	if os.Getenv("DOCKER_SERVICE_PREFER_OFFLINE_IMAGE") != "1" {
914
+		digestImage, err := c.imageWithDigestString(ctx, ctnr.Image, authConfig)
915
+		if err != nil {
916
+			logrus.Warnf("unable to pin image %s to digest: %s", ctnr.Image, err.Error())
917
+		} else {
918
+			logrus.Debugf("pinning image %s by digest: %s", ctnr.Image, digestImage)
919
+			ctnr.Image = digestImage
920
+		}
921
+	}
922
+
904 923
 	r, err := c.client.CreateService(ctx, &swarmapi.CreateServiceRequest{Spec: &serviceSpec})
905 924
 	if err != nil {
906 925
 		return "", err
... ...
@@ -955,12 +1016,13 @@ func (c *Cluster) UpdateService(serviceIDOrName string, version uint64, spec typ
955 955
 		return err
956 956
 	}
957 957
 
958
+	newCtnr := serviceSpec.Task.GetContainer()
959
+	if newCtnr == nil {
960
+		return fmt.Errorf("service does not use container tasks")
961
+	}
962
+
958 963
 	if encodedAuth != "" {
959
-		ctnr := serviceSpec.Task.GetContainer()
960
-		if ctnr == nil {
961
-			return fmt.Errorf("service does not use container tasks")
962
-		}
963
-		ctnr.PullOptions = &swarmapi.ContainerSpec_PullOptions{RegistryAuth: encodedAuth}
964
+		newCtnr.PullOptions = &swarmapi.ContainerSpec_PullOptions{RegistryAuth: encodedAuth}
964 965
 	} else {
965 966
 		// this is needed because if the encodedAuth isn't being updated then we
966 967
 		// 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
979 979
 		if ctnr == nil {
980 980
 			return fmt.Errorf("service does not use container tasks")
981 981
 		}
982
-		serviceSpec.Task.GetContainer().PullOptions = ctnr.PullOptions
982
+		newCtnr.PullOptions = ctnr.PullOptions
983
+		// update encodedAuth so it can be used to pin image by digest
984
+		if ctnr.PullOptions != nil {
985
+			encodedAuth = ctnr.PullOptions.RegistryAuth
986
+		}
987
+	}
988
+
989
+	// retrieve auth config from encoded auth
990
+	authConfig := &apitypes.AuthConfig{}
991
+	if encodedAuth != "" {
992
+		if err := json.NewDecoder(base64.NewDecoder(base64.URLEncoding, strings.NewReader(encodedAuth))).Decode(authConfig); err != nil {
993
+			logrus.Warnf("invalid authconfig: %v", err)
994
+		}
995
+	}
996
+	// pin image by digest
997
+	if os.Getenv("DOCKER_SERVICE_PREFER_OFFLINE_IMAGE") != "1" {
998
+		digestImage, err := c.imageWithDigestString(ctx, newCtnr.Image, authConfig)
999
+		if err != nil {
1000
+			logrus.Warnf("unable to pin image %s to digest: %s", newCtnr.Image, err.Error())
1001
+		} else if newCtnr.Image != digestImage {
1002
+			logrus.Debugf("pinning image %s by digest: %s", newCtnr.Image, digestImage)
1003
+			newCtnr.Image = digestImage
1004
+		}
983 1005
 	}
984 1006
 
985 1007
 	_, err = c.client.UpdateService(
... ...
@@ -4,6 +4,7 @@ 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"
... ...
@@ -45,5 +46,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
-	ResolveTagToDigest(context.Context, reference.NamedTagged, *types.AuthConfig) (string, error)
48
+	GetRepository(context.Context, reference.NamedTagged, *types.AuthConfig) (distribution.Repository, bool, error)
49 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"
... ...
@@ -105,40 +106,39 @@ func (daemon *Daemon) pullImageWithReference(ctx context.Context, ref reference.
105 105
 	return err
106 106
 }
107 107
 
108
-func (daemon *Daemon) ResolveTagToDigest(ctx context.Context, ref reference.NamedTagged, authConfig *types.AuthConfig) (string, error) {
108
+func (daemon *Daemon) GetRepository(ctx context.Context, ref reference.NamedTagged, authConfig *types.AuthConfig) (dist.Repository, bool, error) {
109 109
 	// get repository info
110 110
 	repoInfo, err := daemon.RegistryService.ResolveRepository(ref)
111 111
 	if err != nil {
112
-		return "", err
112
+		return nil, false, err
113 113
 	}
114 114
 	// makes sure name is not empty or `scratch`
115 115
 	if err := distribution.ValidateRepoName(repoInfo.Name()); err != nil {
116
-		return "", err
116
+		return nil, false, err
117 117
 	}
118 118
 
119 119
 	// get endpoints
120 120
 	endpoints, err := daemon.RegistryService.LookupPullEndpoints(repoInfo.Hostname())
121 121
 	if err != nil {
122
-		return "", err
122
+		return nil, false, err
123 123
 	}
124 124
 
125 125
 	// retrieve repository
126
-	// TODO(nishanttotla): More sophisticated selection of endpoint
127
-	repo, confirmedV2, err := distribution.NewV2Repository(ctx, repoInfo, endpoints[0], nil, authConfig, "pull")
128
-
129
-	if err != nil {
130
-		return "", err
131
-	}
132
-	digest := ""
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
+		}
133 136
 
134
-	// only retrieve digest if the repo is v2
135
-	if confirmedV2 {
136
-		dscrptr, err := repo.Tags(ctx).Get(ctx, ref.Tag())
137
-		if err != nil {
138
-			return "", err
139
-		} else {
140
-			digest = dscrptr.Digest.String()
137
+		repository, confirmedV2, lastError = distribution.NewV2Repository(ctx, repoInfo, endpoint, nil, authConfig, "pull")
138
+		if lastError == nil && confirmedV2 {
139
+			break
141 140
 		}
142 141
 	}
143
-	return digest, nil
142
+	return repository, confirmedV2, lastError
144 143
 }