Browse code

Moving docker service digest pinning to client side

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

Nishant Totla authored on 2017/04/06 07:43:17
Showing 6 changed files
... ...
@@ -276,6 +276,12 @@ type ServiceCreateOptions struct {
276 276
 	//
277 277
 	// This field follows the format of the X-Registry-Auth header.
278 278
 	EncodedRegistryAuth string
279
+
280
+	// QueryRegistry indicates whether the service update requires
281
+	// contacting a registry. A registry may be contacted to retrieve
282
+	// the image digest and manifest, which in turn can be used to update
283
+	// platform or other information about the service.
284
+	QueryRegistry bool
279 285
 }
280 286
 
281 287
 // ServiceCreateResponse contains the information returned to a client
... ...
@@ -315,6 +321,12 @@ type ServiceUpdateOptions struct {
315 315
 	// The valid values are "previous" and "none". An empty value is the
316 316
 	// same as "none".
317 317
 	Rollback string
318
+
319
+	// QueryRegistry indicates whether the service update requires
320
+	// contacting a registry. A registry may be contacted to retrieve
321
+	// the image digest and manifest, which in turn can be used to update
322
+	// platform or other information about the service.
323
+	QueryRegistry bool
318 324
 }
319 325
 
320 326
 // ServiceListOptions holds parameters to list services with.
321 327
new file mode 100644
... ...
@@ -0,0 +1,31 @@
0
+package client
1
+
2
+import (
3
+	"encoding/json"
4
+	"net/url"
5
+
6
+	registrytypes "github.com/docker/docker/api/types/registry"
7
+	"golang.org/x/net/context"
8
+)
9
+
10
+// DistributionInspect returns the image digest with full Manifest
11
+func (cli *Client) DistributionInspect(ctx context.Context, image, encodedRegistryAuth string) (registrytypes.DistributionInspect, error) {
12
+	var headers map[string][]string
13
+
14
+	if encodedRegistryAuth != "" {
15
+		headers = map[string][]string{
16
+			"X-Registry-Auth": {encodedRegistryAuth},
17
+		}
18
+	}
19
+
20
+	// Contact the registry to retrieve digest and platform information
21
+	var distributionInspect registrytypes.DistributionInspect
22
+	resp, err := cli.get(ctx, "/distribution/"+image+"/json", url.Values{}, headers)
23
+	if err != nil {
24
+		return distributionInspect, err
25
+	}
26
+
27
+	err = json.NewDecoder(resp.body).Decode(&distributionInspect)
28
+	ensureReaderClosed(resp)
29
+	return distributionInspect, err
30
+}
... ...
@@ -20,6 +20,7 @@ import (
20 20
 type CommonAPIClient interface {
21 21
 	ConfigAPIClient
22 22
 	ContainerAPIClient
23
+	DistributionAPIClient
23 24
 	ImageAPIClient
24 25
 	NodeAPIClient
25 26
 	NetworkAPIClient
... ...
@@ -69,6 +70,11 @@ type ContainerAPIClient interface {
69 69
 	ContainersPrune(ctx context.Context, pruneFilters filters.Args) (types.ContainersPruneReport, error)
70 70
 }
71 71
 
72
+// DistributionAPIClient defines API client methods for the registry
73
+type DistributionAPIClient interface {
74
+	DistributionInspect(ctx context.Context, image, encodedRegistryAuth string) (registry.DistributionInspect, error)
75
+}
76
+
72 77
 // ImageAPIClient defines API client methods for the images
73 78
 type ImageAPIClient interface {
74 79
 	ImageBuild(ctx context.Context, context io.Reader, options types.ImageBuildOptions) (types.ImageBuildResponse, error)
... ...
@@ -2,15 +2,21 @@ package client
2 2
 
3 3
 import (
4 4
 	"encoding/json"
5
+	"fmt"
5 6
 
7
+	"github.com/docker/distribution/reference"
6 8
 	"github.com/docker/docker/api/types"
7 9
 	"github.com/docker/docker/api/types/swarm"
10
+	"github.com/opencontainers/go-digest"
8 11
 	"golang.org/x/net/context"
9 12
 )
10 13
 
11 14
 // ServiceCreate creates a new Service.
12 15
 func (cli *Client) ServiceCreate(ctx context.Context, service swarm.ServiceSpec, options types.ServiceCreateOptions) (types.ServiceCreateResponse, error) {
13
-	var headers map[string][]string
16
+	var (
17
+		headers map[string][]string
18
+		distErr error
19
+	)
14 20
 
15 21
 	if options.EncodedRegistryAuth != "" {
16 22
 		headers = map[string][]string{
... ...
@@ -18,6 +24,18 @@ func (cli *Client) ServiceCreate(ctx context.Context, service swarm.ServiceSpec,
18 18
 		}
19 19
 	}
20 20
 
21
+	// Contact the registry to retrieve digest and platform information
22
+	if options.QueryRegistry {
23
+		distributionInspect, err := cli.DistributionInspect(ctx, service.TaskTemplate.ContainerSpec.Image, options.EncodedRegistryAuth)
24
+		distErr = err
25
+		if err == nil {
26
+			// now pin by digest if the image doesn't already contain a digest
27
+			img := imageWithDigestString(service.TaskTemplate.ContainerSpec.Image, distributionInspect.Descriptor.Digest)
28
+			if img != "" {
29
+				service.TaskTemplate.ContainerSpec.Image = img
30
+			}
31
+		}
32
+	}
21 33
 	var response types.ServiceCreateResponse
22 34
 	resp, err := cli.post(ctx, "/services/create", nil, service, headers)
23 35
 	if err != nil {
... ...
@@ -25,6 +43,38 @@ func (cli *Client) ServiceCreate(ctx context.Context, service swarm.ServiceSpec,
25 25
 	}
26 26
 
27 27
 	err = json.NewDecoder(resp.body).Decode(&response)
28
+
29
+	if distErr != nil {
30
+		response.Warnings = append(response.Warnings, digestWarning(service.TaskTemplate.ContainerSpec.Image))
31
+	}
32
+
28 33
 	ensureReaderClosed(resp)
29 34
 	return response, err
30 35
 }
36
+
37
+// imageWithDigestString takes an image string and a digest, and updates
38
+// the image string if it didn't originally contain a digest. It assumes
39
+// that the image string is not an image ID
40
+func imageWithDigestString(image string, dgst digest.Digest) string {
41
+	isCanonical := false
42
+	ref, err := reference.ParseAnyReference(image)
43
+	if err == nil {
44
+		_, isCanonical = ref.(reference.Canonical)
45
+
46
+		if !isCanonical {
47
+			namedRef, _ := ref.(reference.Named)
48
+			img, err := reference.WithDigest(namedRef, dgst)
49
+			if err == nil {
50
+				return img.String()
51
+			}
52
+		}
53
+	}
54
+	return ""
55
+}
56
+
57
+// digestWarning constructs a formatted warning string using the
58
+// image name that could not be pinned by digest. The formatting
59
+// is hardcoded, but could me made smarter in the future
60
+func digestWarning(image string) string {
61
+	return fmt.Sprintf("image %s could not be accessed on a registry to record\nits digest. Each node will access %s independently,\npossibly leading to different nodes running different\nversions of the image.\n", image, image)
62
+}
... ...
@@ -15,6 +15,7 @@ func (cli *Client) ServiceUpdate(ctx context.Context, serviceID string, version
15 15
 	var (
16 16
 		headers map[string][]string
17 17
 		query   = url.Values{}
18
+		distErr error
18 19
 	)
19 20
 
20 21
 	if options.EncodedRegistryAuth != "" {
... ...
@@ -33,6 +34,20 @@ func (cli *Client) ServiceUpdate(ctx context.Context, serviceID string, version
33 33
 
34 34
 	query.Set("version", strconv.FormatUint(version.Index, 10))
35 35
 
36
+	// Contact the registry to retrieve digest and platform information
37
+	// This happens only when the image has changed
38
+	if options.QueryRegistry {
39
+		distributionInspect, err := cli.DistributionInspect(ctx, service.TaskTemplate.ContainerSpec.Image, options.EncodedRegistryAuth)
40
+		distErr = err
41
+		if err == nil {
42
+			// now pin by digest if the image doesn't already contain a digest
43
+			img := imageWithDigestString(service.TaskTemplate.ContainerSpec.Image, distributionInspect.Descriptor.Digest)
44
+			if img != "" {
45
+				service.TaskTemplate.ContainerSpec.Image = img
46
+			}
47
+		}
48
+	}
49
+
36 50
 	var response types.ServiceUpdateResponse
37 51
 	resp, err := cli.post(ctx, "/services/"+serviceID+"/update", query, service, headers)
38 52
 	if err != nil {
... ...
@@ -40,6 +55,11 @@ func (cli *Client) ServiceUpdate(ctx context.Context, serviceID string, version
40 40
 	}
41 41
 
42 42
 	err = json.NewDecoder(resp.body).Decode(&response)
43
+
44
+	if distErr != nil {
45
+		response.Warnings = append(response.Warnings, digestWarning(service.TaskTemplate.ContainerSpec.Image))
46
+	}
47
+
43 48
 	ensureReaderClosed(resp)
44 49
 	return response, err
45 50
 }
... ...
@@ -1,9 +1,10 @@
1 1
 package client
2 2
 
3 3
 import (
4
-	"github.com/docker/docker/api/types/filters"
5 4
 	"net/url"
6 5
 	"regexp"
6
+
7
+	"github.com/docker/docker/api/types/filters"
7 8
 )
8 9
 
9 10
 var headerRegexp = regexp.MustCompile(`\ADocker/.+\s\((.+)\)\z`)