diff --git a/Makefile b/Makefile index e9758f42..babf9132 100644 --- a/Makefile +++ b/Makefile @@ -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 ./... diff --git a/driver/docker/docker.go b/driver/docker/docker.go index fb07884c..860ebf4c 100644 --- a/driver/docker/docker.go +++ b/driver/docker/docker.go @@ -3,7 +3,6 @@ package docker import ( "archive/tar" "context" - "errors" "fmt" "io" "io/ioutil" @@ -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" ) @@ -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)) @@ -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) } @@ -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:'; 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) +} diff --git a/driver/docker/docker_integration_test.go b/driver/docker/docker_integration_test.go index 1b658985..f9ed32a0 100644 --- a/driver/docker/docker_integration_test.go +++ b/driver/docker/docker_integration_test.go @@ -4,6 +4,7 @@ package docker import ( "bytes" + "fmt" "os" "testing" @@ -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 @@ -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, } } @@ -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)) +} diff --git a/driver/docker/docker_test.go b/driver/docker/docker_test.go index 40376854..352f8d4d 100644 --- a/driver/docker/docker_test.go +++ b/driver/docker/docker_test.go @@ -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" ) @@ -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) + }) +}