diff --git a/bundle/bundle.go b/bundle/bundle.go index 8e35e7c9..aed20b54 100644 --- a/bundle/bundle.go +++ b/bundle/bundle.go @@ -96,6 +96,15 @@ func (i *BaseImage) DeepCopy() *BaseImage { return &i2 } +// DigestedRef returns an image reference which can be used to pull by digest using Docker or Kubernetes. +// If no digest is available, only the image will be returned and the boolean value will be false. +func (i *BaseImage) DigestedRef() (string, bool) { + if i.Digest == "" { + return i.Image, false + } + return fmt.Sprintf("%s@%s", i.Image, i.Digest), true +} + // Image describes a container image in the bundle type Image struct { BaseImage `yaml:",inline"` diff --git a/bundle/bundle_test.go b/bundle/bundle_test.go index 49d9b4aa..845718a6 100644 --- a/bundle/bundle_test.go +++ b/bundle/bundle_test.go @@ -2,6 +2,7 @@ package bundle import ( "bytes" + "fmt" "io/ioutil" "testing" @@ -680,3 +681,63 @@ func TestImageDeepCopy(t *testing.T) { assert.Equal(t, map[string]string{"origLabel": "newLabelValue"}, newImg.Labels) assert.Equal(t, "123abcd", newImg.Digest) } + +func ExampleBaseImage_DigestedRef() { + image := BaseImage{ + Image: "foo/bar:latest", + Digest: "sha256:7ea254518cab82f53938523eab0dc6601a3dd1edf700f1f6235e66e27cbc7986", + } + + imageRef, ok := image.DigestedRef() + if !ok { + fmt.Println("no digest") + } + + fmt.Println(imageRef) + // Output: foo/bar:latest@sha256:7ea254518cab82f53938523eab0dc6601a3dd1edf700f1f6235e66e27cbc7986 +} + +func TestBaseImage_DigestedRef(t *testing.T) { + cases := map[string]struct { + expected string + hasDigest bool + image BaseImage + }{ + "no tag": { + expected: "foo", + image: BaseImage{ + Image: "foo", + }, + }, + "tag only": { + expected: "foo:bar", + image: BaseImage{ + Image: "foo:bar", + }, + }, + "digest only": { + expected: "foo@sha256:7ea254518", + hasDigest: true, + image: BaseImage{ + Image: "foo", + Digest: "sha256:7ea254518", + }, + }, + "digest and tag": { + expected: "foo:bar@sha256:7ea254518", + hasDigest: true, + image: BaseImage{ + Image: "foo:bar", + Digest: "sha256:7ea254518", + }, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + ref, ok := tc.image.DigestedRef() + assert.Equal(t, tc.hasDigest, ok) + assert.Equal(t, tc.expected, ref) + }) + } +} diff --git a/driver/docker/docker.go b/driver/docker/docker.go index bf7c5f62..6e0fde43 100644 --- a/driver/docker/docker.go +++ b/driver/docker/docker.go @@ -145,18 +145,21 @@ func (d *Driver) exec(op *driver.Operation) (driver.OperationResult, error) { if d.Simulate { return driver.OperationResult{}, nil } - if d.config["PULL_ALWAYS"] == "1" { - if err := pullImage(ctx, cli, op.Image.Image); err != nil { + + imageRef, ok := op.Image.DigestedRef() + if !ok && d.config["PULL_ALWAYS"] == "1" { + if err := pullImage(ctx, cli, imageRef); err != nil { return driver.OperationResult{}, err } } + var env []string for k, v := range op.Environment { env = append(env, fmt.Sprintf("%s=%v", k, v)) } cfg := &container.Config{ - Image: op.Image.Image, + Image: imageRef, Env: env, Entrypoint: strslice.StrSlice{"/cnab/app/run"}, AttachStderr: true, @@ -173,8 +176,8 @@ func (d *Driver) exec(op *driver.Operation) (driver.OperationResult, error) { resp, err := cli.Client().ContainerCreate(ctx, cfg, hostCfg, 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 { + fmt.Fprintf(cli.Err(), "Unable to find image '%s' locally\n", cfg.Image) + if err := pullImage(ctx, cli, cfg.Image); err != nil { return driver.OperationResult{}, err } if resp, err = cli.Client().ContainerCreate(ctx, cfg, hostCfg, nil, ""); err != nil { diff --git a/driver/kubernetes/kubernetes.go b/driver/kubernetes/kubernetes.go index eaec945c..acc6484d 100644 --- a/driver/kubernetes/kubernetes.go +++ b/driver/kubernetes/kubernetes.go @@ -23,7 +23,6 @@ import ( "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" - "github.com/cnabio/cnab-go/bundle" "github.com/cnabio/cnab-go/driver" ) @@ -166,9 +165,10 @@ func (k *Driver) Run(op *driver.Operation) (driver.OperationResult, error) { }, }, } + digestedRef, _ := op.Image.DigestedRef() container := v1.Container{ Name: k8sContainerName, - Image: imageWithDigest(op.Image), + Image: digestedRef, Command: []string{"/cnab/app/run"}, Resources: v1.ResourceRequirements{ Limits: v1.ResourceList{ @@ -444,10 +444,3 @@ func homeDir() string { } return os.Getenv("USERPROFILE") // windows } - -func imageWithDigest(img bundle.InvocationImage) string { - if img.Digest == "" { - return img.Image - } - return img.Image + "@" + img.Digest -} diff --git a/driver/kubernetes/kubernetes_test.go b/driver/kubernetes/kubernetes_test.go index 9d3c00de..d8595ac4 100644 --- a/driver/kubernetes/kubernetes_test.go +++ b/driver/kubernetes/kubernetes_test.go @@ -4,7 +4,6 @@ import ( "os" "testing" - "github.com/cnabio/cnab-go/bundle" "github.com/cnabio/cnab-go/driver" "github.com/stretchr/testify/assert" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -40,44 +39,6 @@ func TestDriver_Run(t *testing.T) { assert.Equal(t, len(secretList.Items), 1, "expected one secret to be created") } -func TestImageWithDigest(t *testing.T) { - testCases := map[string]bundle.InvocationImage{ - "foo": { - BaseImage: bundle.BaseImage{ - Image: "foo", - }, - }, - "foo/bar": { - BaseImage: bundle.BaseImage{ - Image: "foo/bar", - }, - }, - "foo/bar:baz": { - BaseImage: bundle.BaseImage{ - Image: "foo/bar:baz", - }, - }, - "foo/bar:baz@sha:a1b2c3": { - BaseImage: bundle.BaseImage{ - Image: "foo/bar:baz", - Digest: "sha:a1b2c3", - }, - }, - "foo/bar@sha:a1b2c3": { - BaseImage: bundle.BaseImage{ - Image: "foo/bar", - Digest: "sha:a1b2c3", - }, - }, - } - - for expectedImageRef, img := range testCases { - t.Run(expectedImageRef, func(t *testing.T) { - assert.Equal(t, expectedImageRef, imageWithDigest(img)) - }) - } -} - func TestGenerateNameTemplate(t *testing.T) { testCases := map[string]struct { op *driver.Operation