Browse code

cli: Pin image to digest using content trust

Implement notary-based digest lookup in the client when
DOCKER_CONTENT_TRUST=1.

Signed-off-by: Aaron Lehmann <aaron.lehmann@docker.com>
(cherry picked from commit d4d6f8c0d0c6cd0ba6dc96ab7a9ed07e1e766074)

Aaron Lehmann authored on 2016/12/06 10:02:26
Showing 4 changed files
... ...
@@ -72,6 +72,10 @@ func runCreate(dockerCli *command.DockerCli, opts *serviceOptions) error {
72 72
 
73 73
 	ctx := context.Background()
74 74
 
75
+	if err := resolveServiceImageDigest(dockerCli, &service); err != nil {
76
+		return err
77
+	}
78
+
75 79
 	// only send auth if flag was set
76 80
 	if opts.registryAuth {
77 81
 		// Retrieve encoded auth token from the image reference
78 82
new file mode 100644
... ...
@@ -0,0 +1,96 @@
0
+package service
1
+
2
+import (
3
+	"encoding/hex"
4
+	"fmt"
5
+
6
+	"github.com/Sirupsen/logrus"
7
+	"github.com/docker/distribution/digest"
8
+	distreference "github.com/docker/distribution/reference"
9
+	"github.com/docker/docker/api/types/swarm"
10
+	"github.com/docker/docker/cli/command"
11
+	"github.com/docker/docker/cli/trust"
12
+	"github.com/docker/docker/reference"
13
+	"github.com/docker/docker/registry"
14
+	"github.com/docker/notary/tuf/data"
15
+	"github.com/pkg/errors"
16
+	"golang.org/x/net/context"
17
+)
18
+
19
+func resolveServiceImageDigest(dockerCli *command.DockerCli, service *swarm.ServiceSpec) error {
20
+	if !command.IsTrusted() {
21
+		// Digests are resolved by the daemon when not using content
22
+		// trust.
23
+		return nil
24
+	}
25
+
26
+	image := service.TaskTemplate.ContainerSpec.Image
27
+
28
+	// We only attempt to resolve the digest if the reference
29
+	// could be parsed as a digest reference. Specifying an image ID
30
+	// is valid but not resolvable. There is no warning message for
31
+	// an image ID because it's valid to use one.
32
+	if _, err := digest.ParseDigest(image); err == nil {
33
+		return nil
34
+	}
35
+
36
+	ref, err := reference.ParseNamed(image)
37
+	if err != nil {
38
+		return fmt.Errorf("Could not parse image reference %s", service.TaskTemplate.ContainerSpec.Image)
39
+	}
40
+	if _, ok := ref.(reference.Canonical); !ok {
41
+		ref = reference.WithDefaultTag(ref)
42
+
43
+		taggedRef, ok := ref.(reference.NamedTagged)
44
+		if !ok {
45
+			// This should never happen because a reference either
46
+			// has a digest, or WithDefaultTag would give it a tag.
47
+			return errors.New("Failed to resolve image digest using content trust: reference is missing a tag")
48
+		}
49
+
50
+		resolvedImage, err := trustedResolveDigest(context.Background(), dockerCli, taggedRef)
51
+		if err != nil {
52
+			return fmt.Errorf("Failed to resolve image digest using content trust: %v", err)
53
+		}
54
+		logrus.Debugf("resolved image tag to %s using content trust", resolvedImage.String())
55
+		service.TaskTemplate.ContainerSpec.Image = resolvedImage.String()
56
+	}
57
+	return nil
58
+}
59
+
60
+func trustedResolveDigest(ctx context.Context, cli *command.DockerCli, ref reference.NamedTagged) (distreference.Canonical, error) {
61
+	repoInfo, err := registry.ParseRepositoryInfo(ref)
62
+	if err != nil {
63
+		return nil, err
64
+	}
65
+
66
+	authConfig := command.ResolveAuthConfig(ctx, cli, repoInfo.Index)
67
+
68
+	notaryRepo, err := trust.GetNotaryRepository(cli, repoInfo, authConfig, "pull")
69
+	if err != nil {
70
+		return nil, errors.Wrap(err, "error establishing connection to trust repository")
71
+	}
72
+
73
+	t, err := notaryRepo.GetTargetByName(ref.Tag(), trust.ReleasesRole, data.CanonicalTargetsRole)
74
+	if err != nil {
75
+		return nil, trust.NotaryError(repoInfo.FullName(), err)
76
+	}
77
+	// Only get the tag if it's in the top level targets role or the releases delegation role
78
+	// ignore it if it's in any other delegation roles
79
+	if t.Role != trust.ReleasesRole && t.Role != data.CanonicalTargetsRole {
80
+		return nil, trust.NotaryError(repoInfo.FullName(), fmt.Errorf("No trust data for %s", ref.String()))
81
+	}
82
+
83
+	logrus.Debugf("retrieving target for %s role\n", t.Role)
84
+	h, ok := t.Hashes["sha256"]
85
+	if !ok {
86
+		return nil, errors.New("no valid hash, expecting sha256")
87
+	}
88
+
89
+	dgst := digest.NewDigestFromHex("sha256", hex.EncodeToString(h))
90
+
91
+	// Using distribution reference package to make sure that adding a
92
+	// digest does not erase the tag. When the two reference packages
93
+	// are unified, this will no longer be an issue.
94
+	return distreference.WithDigest(ref, dgst)
95
+}
... ...
@@ -103,6 +103,12 @@ func runUpdate(dockerCli *command.DockerCli, flags *pflag.FlagSet, serviceID str
103 103
 		return err
104 104
 	}
105 105
 
106
+	if flags.Changed("image") {
107
+		if err := resolveServiceImageDigest(dockerCli, spec); err != nil {
108
+			return err
109
+		}
110
+	}
111
+
106 112
 	updatedSecrets, err := getUpdatedSecrets(apiClient, flags, spec.TaskTemplate.ContainerSpec.Secrets)
107 113
 	if err != nil {
108 114
 		return err
... ...
@@ -1111,9 +1111,11 @@ func (c *Cluster) CreateService(s types.ServiceSpec, encodedAuth string) (*apity
1111 1111
 		if err != nil {
1112 1112
 			logrus.Warnf("unable to pin image %s to digest: %s", ctnr.Image, err.Error())
1113 1113
 			resp.Warnings = append(resp.Warnings, fmt.Sprintf("unable to pin image %s to digest: %s", ctnr.Image, err.Error()))
1114
-		} else {
1114
+		} else if ctnr.Image != digestImage {
1115 1115
 			logrus.Debugf("pinning image %s by digest: %s", ctnr.Image, digestImage)
1116 1116
 			ctnr.Image = digestImage
1117
+		} else {
1118
+			logrus.Debugf("creating service using supplied digest reference %s", ctnr.Image)
1117 1119
 		}
1118 1120
 	}
1119 1121
 
... ...
@@ -1223,6 +1225,8 @@ func (c *Cluster) UpdateService(serviceIDOrName string, version uint64, spec typ
1223 1223
 		} else if newCtnr.Image != digestImage {
1224 1224
 			logrus.Debugf("pinning image %s by digest: %s", newCtnr.Image, digestImage)
1225 1225
 			newCtnr.Image = digestImage
1226
+		} else {
1227
+			logrus.Debugf("updating service using supplied digest reference %s", newCtnr.Image)
1226 1228
 		}
1227 1229
 	}
1228 1230