Skip to content

Commit 02823c6

Browse files
authored
feat(docker): add verification of image digest(s) (#227)
1 parent 4276c70 commit 02823c6

File tree

4 files changed

+185
-16
lines changed

4 files changed

+185
-16
lines changed

Makefile

+5-1
Original file line numberDiff line numberDiff line change
@@ -61,5 +61,9 @@ endif
6161
git checkout go.mod go.sum
6262

6363
.PHONY: coverage
64-
coverage:
64+
coverage: compile-integration-tests
6565
./e2e-kind.sh
66+
67+
.PHONY: compile-integration-tests
68+
compile-integration-tests:
69+
@go test -tags=integration -run nothing ./...

driver/docker/docker.go

+67-11
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package docker
33
import (
44
"archive/tar"
55
"context"
6-
"errors"
76
"fmt"
87
"io"
98
"io/ioutil"
@@ -22,6 +21,9 @@ import (
2221
"github.com/docker/docker/registry"
2322
"github.com/mitchellh/copystructure"
2423

24+
"github.com/pkg/errors"
25+
26+
"github.com/cnabio/cnab-go/bundle"
2527
"github.com/cnabio/cnab-go/driver"
2628
)
2729

@@ -187,6 +189,17 @@ func (d *Driver) exec(op *driver.Operation) (driver.OperationResult, error) {
187189
return driver.OperationResult{}, err
188190
}
189191
}
192+
193+
ii, err := d.inspectImage(ctx, op.Image)
194+
if err != nil {
195+
return driver.OperationResult{}, err
196+
}
197+
198+
err = d.validateImageDigest(op.Image, ii.RepoDigests)
199+
if err != nil {
200+
return driver.OperationResult{}, errors.Wrap(err, "image digest validation failed")
201+
}
202+
190203
var env []string
191204
for k, v := range op.Environment {
192205
env = append(env, fmt.Sprintf("%s=%v", k, v))
@@ -206,16 +219,7 @@ func (d *Driver) exec(op *driver.Operation) (driver.OperationResult, error) {
206219
}
207220

208221
resp, err := cli.Client().ContainerCreate(ctx, &d.containerCfg, &d.containerHostCfg, nil, "")
209-
switch {
210-
case client.IsErrNotFound(err):
211-
fmt.Fprintf(cli.Err(), "Unable to find image '%s' locally\n", op.Image.Image)
212-
if err := pullImage(ctx, cli, op.Image.Image); err != nil {
213-
return driver.OperationResult{}, err
214-
}
215-
if resp, err = cli.Client().ContainerCreate(ctx, &d.containerCfg, &d.containerHostCfg, nil, ""); err != nil {
216-
return driver.OperationResult{}, fmt.Errorf("cannot create container: %v", err)
217-
}
218-
case err != nil:
222+
if err != nil {
219223
return driver.OperationResult{}, fmt.Errorf("cannot create container: %v", err)
220224
}
221225

@@ -385,3 +389,55 @@ func generateTar(files map[string]string) (io.Reader, error) {
385389

386390
// ConfigurationOption is an option used to customize docker driver container and host config
387391
type ConfigurationOption func(*container.Config, *container.HostConfig) error
392+
393+
// inspectImage inspects the operation image and returns an object of types.ImageInspect,
394+
// pulling the image if not found locally
395+
func (d *Driver) inspectImage(ctx context.Context, image bundle.InvocationImage) (types.ImageInspect, error) {
396+
ii, _, err := d.dockerCli.Client().ImageInspectWithRaw(ctx, image.Image)
397+
switch {
398+
case client.IsErrNotFound(err):
399+
fmt.Fprintf(d.dockerCli.Err(), "Unable to find image '%s' locally\n", image.Image)
400+
if err := pullImage(ctx, d.dockerCli, image.Image); err != nil {
401+
return ii, err
402+
}
403+
if ii, _, err = d.dockerCli.Client().ImageInspectWithRaw(ctx, image.Image); err != nil {
404+
return ii, errors.Wrapf(err, "cannot inspect image %s", image.Image)
405+
}
406+
case err != nil:
407+
return ii, errors.Wrapf(err, "cannot inspect image %s", image.Image)
408+
}
409+
410+
return ii, nil
411+
}
412+
413+
// validateImageDigest validates the operation image digest, if exists, against
414+
// the supplied repoDigests
415+
func (d *Driver) validateImageDigest(image bundle.InvocationImage, repoDigests []string) error {
416+
if image.Digest == "" {
417+
return nil
418+
}
419+
420+
switch count := len(repoDigests); {
421+
case count == 0:
422+
return fmt.Errorf("image %s has no repo digests", image.Image)
423+
case count > 1:
424+
return fmt.Errorf("image %s has more than one repo digest", image.Image)
425+
}
426+
427+
// RepoDigests are of the form 'imageName@sha256:<sha256>'; we parse out the digest itself for comparison
428+
repoDigest := repoDigests[0]
429+
ref, err := reference.ParseNormalizedNamed(repoDigest)
430+
if err != nil {
431+
return fmt.Errorf("unable to parse repo digest %s", repoDigest)
432+
}
433+
digestRef, ok := ref.(reference.Digested)
434+
if !ok {
435+
return fmt.Errorf("unable to parse repo digest %s", repoDigest)
436+
}
437+
digest := digestRef.Digest().String()
438+
439+
if digest == image.Digest {
440+
return nil
441+
}
442+
return fmt.Errorf("content digest mismatch: image %s has digest %s but the value should be %s according to the bundle file", image.Image, digest, image.Digest)
443+
}

driver/docker/docker_integration_test.go

+44-4
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ package docker
44

55
import (
66
"bytes"
7+
"fmt"
78
"os"
89
"testing"
910

@@ -14,6 +15,11 @@ import (
1415
"github.com/cnabio/cnab-go/driver"
1516
)
1617

18+
var defaultBaseImage = bundle.BaseImage{
19+
Image: "pvtlmc/example-outputs",
20+
Digest: "sha256:568461508c8d220742add8abd226b33534d4269868df4b3178fae1cba3818a6e",
21+
}
22+
1723
func TestDriver_Run(t *testing.T) {
1824
imageFromEnv, ok := os.LookupEnv("DOCKER_INTEGRATION_TEST_IMAGE")
1925
var image bundle.InvocationImage
@@ -26,10 +32,7 @@ func TestDriver_Run(t *testing.T) {
2632
}
2733
} else {
2834
image = bundle.InvocationImage{
29-
BaseImage: bundle.BaseImage{
30-
Image: "pvtlmc/example-outputs",
31-
Digest: "sha256:568461508c8d220742add8abd226b33534d4269868df4b3178fae1cba3818a6e",
32-
},
35+
BaseImage: defaultBaseImage,
3336
}
3437
}
3538

@@ -76,3 +79,40 @@ func TestDriver_Run(t *testing.T) {
7679
"output2": "SOME INSTALL CONTENT 2\n",
7780
}, opResult.Outputs)
7881
}
82+
83+
func TestDriver_ValidateImageDigestFail(t *testing.T) {
84+
imageFromEnv, ok := os.LookupEnv("DOCKER_INTEGRATION_TEST_IMAGE")
85+
var image bundle.InvocationImage
86+
87+
badDigest := "sha256:deadbeef"
88+
89+
if ok {
90+
image = bundle.InvocationImage{
91+
BaseImage: bundle.BaseImage{
92+
Image: imageFromEnv,
93+
Digest: badDigest,
94+
},
95+
}
96+
} else {
97+
image = bundle.InvocationImage{
98+
BaseImage: bundle.BaseImage{
99+
Image: defaultBaseImage.Image,
100+
Digest: badDigest,
101+
},
102+
}
103+
}
104+
105+
op := &driver.Operation{
106+
Image: image,
107+
}
108+
109+
docker := &Driver{}
110+
111+
_, err := docker.Run(op)
112+
assert.Error(t, err)
113+
// Not asserting actual image digests to support arbitrary integration test images
114+
assert.Contains(t, err.Error(),
115+
fmt.Sprintf("content digest mismatch: image %s has digest", op.Image.Image))
116+
assert.Contains(t, err.Error(),
117+
fmt.Sprintf("but the value should be %s according to the bundle file", badDigest))
118+
}

driver/docker/docker_test.go

+69
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"github.com/docker/docker/api/types/strslice"
88
"github.com/stretchr/testify/assert"
99

10+
"github.com/cnabio/cnab-go/bundle"
1011
"github.com/cnabio/cnab-go/driver"
1112
)
1213

@@ -84,3 +85,71 @@ func TestDriver_GetConfigurationOptions(t *testing.T) {
8485
is.Equal(expectedHostCfg, hostCfg)
8586
})
8687
}
88+
89+
func TestDriver_ValidateImageDigest(t *testing.T) {
90+
repoDigests := []string{
91+
"myreg/myimg@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a",
92+
}
93+
94+
t.Run("no image digest", func(t *testing.T) {
95+
d := &Driver{}
96+
97+
image := bundle.InvocationImage{}
98+
image.Image = "myreg/myimg"
99+
100+
err := d.validateImageDigest(image, repoDigests)
101+
assert.NoError(t, err)
102+
})
103+
104+
t.Run("image digest exists - no match exists", func(t *testing.T) {
105+
d := &Driver{}
106+
107+
image := bundle.InvocationImage{}
108+
image.Image = "myreg/myimg"
109+
image.Digest = "sha256:185518070891758909c9f839cf4ca393ee977ac378609f700f60a771a2dfe321"
110+
111+
err := d.validateImageDigest(image, repoDigests)
112+
assert.NotNil(t, err, "expected an error")
113+
assert.Contains(t, err.Error(), "content digest mismatch")
114+
})
115+
116+
t.Run("image digest exists - repo digest unparseable", func(t *testing.T) {
117+
d := &Driver{}
118+
119+
image := bundle.InvocationImage{}
120+
image.Image = "myreg/myimg"
121+
image.Digest = "sha256:185518070891758909c9f839cf4ca393ee977ac378609f700f60a771a2dfe321"
122+
123+
badRepoDigests := []string{"myreg/myimg@sha256:deadbeef"}
124+
125+
err := d.validateImageDigest(image, badRepoDigests)
126+
assert.NotNil(t, err, "expected an error")
127+
assert.EqualError(t, err, "unable to parse repo digest myreg/myimg@sha256:deadbeef")
128+
})
129+
130+
t.Run("image digest exists - more than one repo digest exists", func(t *testing.T) {
131+
d := &Driver{}
132+
133+
image := bundle.InvocationImage{}
134+
image.Image = "myreg/myimg"
135+
image.Digest = "sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a"
136+
137+
multipleRepoDigests := append(repoDigests,
138+
"myreg/myimg@sha256:185518070891758909c9f839cf4ca393ee977ac378609f700f60a771a2dfe321")
139+
140+
err := d.validateImageDigest(image, multipleRepoDigests)
141+
assert.NotNil(t, err, "expected an error")
142+
assert.EqualError(t, err, "image myreg/myimg has more than one repo digest")
143+
})
144+
145+
t.Run("image digest exists - an exact match exists", func(t *testing.T) {
146+
d := &Driver{}
147+
148+
image := bundle.InvocationImage{}
149+
image.Image = "myreg/myimg"
150+
image.Digest = "sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a"
151+
152+
err := d.validateImageDigest(image, repoDigests)
153+
assert.NoError(t, err)
154+
})
155+
}

0 commit comments

Comments
 (0)