Skip to content

Commit

Permalink
feat(docker): add verification of image digest(s) (#227)
Browse files Browse the repository at this point in the history
  • Loading branch information
vdice authored Oct 1, 2020
1 parent 4276c70 commit 02823c6
Show file tree
Hide file tree
Showing 4 changed files with 185 additions and 16 deletions.
6 changes: 5 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -61,5 +61,9 @@ endif
git checkout go.mod go.sum

.PHONY: coverage
coverage:
coverage: compile-integration-tests
./e2e-kind.sh

.PHONY: compile-integration-tests
compile-integration-tests:
@go test -tags=integration -run nothing ./...
78 changes: 67 additions & 11 deletions driver/docker/docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package docker
import (
"archive/tar"
"context"
"errors"
"fmt"
"io"
"io/ioutil"
Expand All @@ -22,6 +21,9 @@ import (
"github.com/docker/docker/registry"
"github.com/mitchellh/copystructure"

"github.com/pkg/errors"

"github.com/cnabio/cnab-go/bundle"
"github.com/cnabio/cnab-go/driver"
)

Expand Down Expand Up @@ -187,6 +189,17 @@ func (d *Driver) exec(op *driver.Operation) (driver.OperationResult, error) {
return driver.OperationResult{}, err
}
}

ii, err := d.inspectImage(ctx, op.Image)
if err != nil {
return driver.OperationResult{}, err
}

err = d.validateImageDigest(op.Image, ii.RepoDigests)
if err != nil {
return driver.OperationResult{}, errors.Wrap(err, "image digest validation failed")
}

var env []string
for k, v := range op.Environment {
env = append(env, fmt.Sprintf("%s=%v", k, v))
Expand All @@ -206,16 +219,7 @@ func (d *Driver) exec(op *driver.Operation) (driver.OperationResult, error) {
}

resp, err := cli.Client().ContainerCreate(ctx, &d.containerCfg, &d.containerHostCfg, nil, "")
switch {
case client.IsErrNotFound(err):
fmt.Fprintf(cli.Err(), "Unable to find image '%s' locally\n", op.Image.Image)
if err := pullImage(ctx, cli, op.Image.Image); err != nil {
return driver.OperationResult{}, err
}
if resp, err = cli.Client().ContainerCreate(ctx, &d.containerCfg, &d.containerHostCfg, nil, ""); err != nil {
return driver.OperationResult{}, fmt.Errorf("cannot create container: %v", err)
}
case err != nil:
if err != nil {
return driver.OperationResult{}, fmt.Errorf("cannot create container: %v", err)
}

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

// ConfigurationOption is an option used to customize docker driver container and host config
type ConfigurationOption func(*container.Config, *container.HostConfig) error

// inspectImage inspects the operation image and returns an object of types.ImageInspect,
// pulling the image if not found locally
func (d *Driver) inspectImage(ctx context.Context, image bundle.InvocationImage) (types.ImageInspect, error) {
ii, _, err := d.dockerCli.Client().ImageInspectWithRaw(ctx, image.Image)
switch {
case client.IsErrNotFound(err):
fmt.Fprintf(d.dockerCli.Err(), "Unable to find image '%s' locally\n", image.Image)
if err := pullImage(ctx, d.dockerCli, image.Image); err != nil {
return ii, err
}
if ii, _, err = d.dockerCli.Client().ImageInspectWithRaw(ctx, image.Image); err != nil {
return ii, errors.Wrapf(err, "cannot inspect image %s", image.Image)
}
case err != nil:
return ii, errors.Wrapf(err, "cannot inspect image %s", image.Image)
}

return ii, nil
}

// validateImageDigest validates the operation image digest, if exists, against
// the supplied repoDigests
func (d *Driver) validateImageDigest(image bundle.InvocationImage, repoDigests []string) error {
if image.Digest == "" {
return nil
}

switch count := len(repoDigests); {
case count == 0:
return fmt.Errorf("image %s has no repo digests", image.Image)
case count > 1:
return fmt.Errorf("image %s has more than one repo digest", image.Image)
}

// RepoDigests are of the form 'imageName@sha256:<sha256>'; we parse out the digest itself for comparison
repoDigest := repoDigests[0]
ref, err := reference.ParseNormalizedNamed(repoDigest)
if err != nil {
return fmt.Errorf("unable to parse repo digest %s", repoDigest)
}
digestRef, ok := ref.(reference.Digested)
if !ok {
return fmt.Errorf("unable to parse repo digest %s", repoDigest)
}
digest := digestRef.Digest().String()

if digest == image.Digest {
return nil
}
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)
}
48 changes: 44 additions & 4 deletions driver/docker/docker_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package docker

import (
"bytes"
"fmt"
"os"
"testing"

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

var defaultBaseImage = bundle.BaseImage{
Image: "pvtlmc/example-outputs",
Digest: "sha256:568461508c8d220742add8abd226b33534d4269868df4b3178fae1cba3818a6e",
}

func TestDriver_Run(t *testing.T) {
imageFromEnv, ok := os.LookupEnv("DOCKER_INTEGRATION_TEST_IMAGE")
var image bundle.InvocationImage
Expand All @@ -26,10 +32,7 @@ func TestDriver_Run(t *testing.T) {
}
} else {
image = bundle.InvocationImage{
BaseImage: bundle.BaseImage{
Image: "pvtlmc/example-outputs",
Digest: "sha256:568461508c8d220742add8abd226b33534d4269868df4b3178fae1cba3818a6e",
},
BaseImage: defaultBaseImage,
}
}

Expand Down Expand Up @@ -76,3 +79,40 @@ func TestDriver_Run(t *testing.T) {
"output2": "SOME INSTALL CONTENT 2\n",
}, opResult.Outputs)
}

func TestDriver_ValidateImageDigestFail(t *testing.T) {
imageFromEnv, ok := os.LookupEnv("DOCKER_INTEGRATION_TEST_IMAGE")
var image bundle.InvocationImage

badDigest := "sha256:deadbeef"

if ok {
image = bundle.InvocationImage{
BaseImage: bundle.BaseImage{
Image: imageFromEnv,
Digest: badDigest,
},
}
} else {
image = bundle.InvocationImage{
BaseImage: bundle.BaseImage{
Image: defaultBaseImage.Image,
Digest: badDigest,
},
}
}

op := &driver.Operation{
Image: image,
}

docker := &Driver{}

_, err := docker.Run(op)
assert.Error(t, err)
// Not asserting actual image digests to support arbitrary integration test images
assert.Contains(t, err.Error(),
fmt.Sprintf("content digest mismatch: image %s has digest", op.Image.Image))
assert.Contains(t, err.Error(),
fmt.Sprintf("but the value should be %s according to the bundle file", badDigest))
}
69 changes: 69 additions & 0 deletions driver/docker/docker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/docker/docker/api/types/strslice"
"github.com/stretchr/testify/assert"

"github.com/cnabio/cnab-go/bundle"
"github.com/cnabio/cnab-go/driver"
)

Expand Down Expand Up @@ -84,3 +85,71 @@ func TestDriver_GetConfigurationOptions(t *testing.T) {
is.Equal(expectedHostCfg, hostCfg)
})
}

func TestDriver_ValidateImageDigest(t *testing.T) {
repoDigests := []string{
"myreg/myimg@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a",
}

t.Run("no image digest", func(t *testing.T) {
d := &Driver{}

image := bundle.InvocationImage{}
image.Image = "myreg/myimg"

err := d.validateImageDigest(image, repoDigests)
assert.NoError(t, err)
})

t.Run("image digest exists - no match exists", func(t *testing.T) {
d := &Driver{}

image := bundle.InvocationImage{}
image.Image = "myreg/myimg"
image.Digest = "sha256:185518070891758909c9f839cf4ca393ee977ac378609f700f60a771a2dfe321"

err := d.validateImageDigest(image, repoDigests)
assert.NotNil(t, err, "expected an error")
assert.Contains(t, err.Error(), "content digest mismatch")
})

t.Run("image digest exists - repo digest unparseable", func(t *testing.T) {
d := &Driver{}

image := bundle.InvocationImage{}
image.Image = "myreg/myimg"
image.Digest = "sha256:185518070891758909c9f839cf4ca393ee977ac378609f700f60a771a2dfe321"

badRepoDigests := []string{"myreg/myimg@sha256:deadbeef"}

err := d.validateImageDigest(image, badRepoDigests)
assert.NotNil(t, err, "expected an error")
assert.EqualError(t, err, "unable to parse repo digest myreg/myimg@sha256:deadbeef")
})

t.Run("image digest exists - more than one repo digest exists", func(t *testing.T) {
d := &Driver{}

image := bundle.InvocationImage{}
image.Image = "myreg/myimg"
image.Digest = "sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a"

multipleRepoDigests := append(repoDigests,
"myreg/myimg@sha256:185518070891758909c9f839cf4ca393ee977ac378609f700f60a771a2dfe321")

err := d.validateImageDigest(image, multipleRepoDigests)
assert.NotNil(t, err, "expected an error")
assert.EqualError(t, err, "image myreg/myimg has more than one repo digest")
})

t.Run("image digest exists - an exact match exists", func(t *testing.T) {
d := &Driver{}

image := bundle.InvocationImage{}
image.Image = "myreg/myimg"
image.Digest = "sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a"

err := d.validateImageDigest(image, repoDigests)
assert.NoError(t, err)
})
}

0 comments on commit 02823c6

Please sign in to comment.