From 2e73eb84e08c6de63c15e36cc0d380138c3266ef Mon Sep 17 00:00:00 2001 From: Furkan Date: Wed, 8 Dec 2021 00:15:25 +0300 Subject: [PATCH] feat(dockerfile): ability to parse COPY --from=image Signed-off-by: Furkan Co-authored-by: Batuhan --- cmd/cosign/cli/dockerfile.go | 23 ++++++++++++++---- cmd/cosign/cli/dockerfile/resolve.go | 12 ++++++---- cmd/cosign/cli/dockerfile/resolve_test.go | 17 +++++++++++++ cmd/cosign/cli/dockerfile/verify.go | 19 ++++++++++++++- cmd/cosign/cli/dockerfile/verify_test.go | 21 ++++++++++++++++ doc/cosign_dockerfile_resolve.md | 29 +++++++++++++++++++---- test/e2e_test.sh | 4 ++-- test/testdata/with_copy.Dockerfile | 18 ++++++++++++++ 8 files changed, 127 insertions(+), 16 deletions(-) create mode 100644 test/testdata/with_copy.Dockerfile diff --git a/cmd/cosign/cli/dockerfile.go b/cmd/cosign/cli/dockerfile.go index 9e5b2fce8a8..87d6b88eb9b 100644 --- a/cmd/cosign/cli/dockerfile.go +++ b/cmd/cosign/cli/dockerfile.go @@ -116,12 +116,27 @@ func dockerfileResolve() *cobra.Command { cmd := &cobra.Command{ Use: "resolve", Short: "Resolve the digest of the images and rewrites them with fully qualified image reference", - Long: ``, + Long: `Resolve the digest of the images and rewrites them with fully qualified image reference + +This command creates a surface which resolves mutable image tags into immutable image digests: + - FROM , it rewrites the Dockerfile to FROM + +Using FROM without is dangerous because even if what's currently tagged on the registry is signed properly, +there is a race before the FROM is evaluated (what if it changes!), or (with docker build) it's possible that +what is in the local cache(!) is what's actually used, and not what was verified! (See issue #648) + +This command does NOT do image verification; instead it only rewrites all image tags to corresponding digest(s). + +The following image reference definitions are currently supported: + - FROM --platform=linux/amd64 gcr.io/distroless/base AS base + - COPY --from=gcr.io/distroless/base`, Example: ` cosign dockerfile resolve Dockerfile - # specify output file - cosign dockerfile resolve -o Dockerfile.edited Dockerfile -`, + # print to stdout + cosign dockerfile resolve Dockerfile + + # specify a output file + cosign dockerfile resolve -o Dockerfile.resolved Dockerfile`, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { v := &dockerfile.ResolveDockerfileCommand{ diff --git a/cmd/cosign/cli/dockerfile/resolve.go b/cmd/cosign/cli/dockerfile/resolve.go index 6628b98b1f0..fd752d5f74e 100644 --- a/cmd/cosign/cli/dockerfile/resolve.go +++ b/cmd/cosign/cli/dockerfile/resolve.go @@ -54,9 +54,11 @@ func (c *ResolveDockerfileCommand) Exec(ctx context.Context, args []string) erro } if c.Output != "" { - _ = os.WriteFile(c.Output, resolvedDockerfile, 0600) + if err := os.WriteFile(c.Output, resolvedDockerfile, 0600); err != nil { + return fmt.Errorf("failed writing resolved Dockerfile: %w", err) + } } else { - _, _ = fmt.Fprintln(os.Stdout, string(resolvedDockerfile)) + fmt.Fprintln(os.Stdout, string(resolvedDockerfile)) } return nil @@ -69,8 +71,8 @@ func resolveDigest(dockerfile io.Reader) ([]byte, error) { for fileScanner.Scan() { line := strings.TrimSpace(fileScanner.Text()) - // TODO(developer-guy): support the COPY --from=image:tag cases - if strings.HasPrefix(strings.ToUpper(line), "FROM") { + if strings.HasPrefix(strings.ToUpper(line), "FROM") || + strings.HasPrefix(strings.ToUpper(line), "COPY") { switch image := getImageFromLine(line); image { case "scratch": tmp.WriteString(line) @@ -80,6 +82,8 @@ func resolveDigest(dockerfile io.Reader) ([]byte, error) { // we should not return err here since // we can define the image refs smth like // i.e., FROM alpine:$(TAG), FROM $(IMAGE), etc. + // TODO: support parameter substitution by passing a `--build-arg` flag + fmt.Fprintf(os.Stderr, "WARNING: parameter substitution for images does not supported yet. consider set all environment variables before resolving the Dockerfile. Image: %s.\n", image) tmp.WriteString(line) tmp.WriteString("\n") continue diff --git a/cmd/cosign/cli/dockerfile/resolve_test.go b/cmd/cosign/cli/dockerfile/resolve_test.go index 3b43ac3db2c..c4ec8ed3d22 100644 --- a/cmd/cosign/cli/dockerfile/resolve_test.go +++ b/cmd/cosign/cli/dockerfile/resolve_test.go @@ -31,6 +31,23 @@ func Test_resolveDigest(t *testing.T) { "happy alpine", `FROM alpine:3.13`, `FROM index.docker.io/library/alpine@sha256:026f721af4cf2843e07bba648e158fb35ecc876d822130633cc49f707f0fc88c +`, + false, + }, + { + "happy alpine trim", + ` FROM alpine:3.13 `, + `FROM index.docker.io/library/alpine@sha256:026f721af4cf2843e07bba648e158fb35ecc876d822130633cc49f707f0fc88c +`, + false, + }, + { + "happy alpine copy", + `FROM alpine:3.13 +COPY --from=alpine:3.13 +`, + `FROM index.docker.io/library/alpine@sha256:026f721af4cf2843e07bba648e158fb35ecc876d822130633cc49f707f0fc88c +COPY --from=index.docker.io/library/alpine@sha256:026f721af4cf2843e07bba648e158fb35ecc876d822130633cc49f707f0fc88c `, false, }, diff --git a/cmd/cosign/cli/dockerfile/verify.go b/cmd/cosign/cli/dockerfile/verify.go index 72af8be668d..f4b210b7436 100644 --- a/cmd/cosign/cli/dockerfile/verify.go +++ b/cmd/cosign/cli/dockerfile/verify.go @@ -67,8 +67,11 @@ func getImagesFromDockerfile(dockerfile io.Reader) ([]string, error) { fileScanner := bufio.NewScanner(dockerfile) for fileScanner.Scan() { line := strings.TrimSpace(fileScanner.Text()) - if strings.HasPrefix(strings.ToUpper(line), "FROM") { + if strings.HasPrefix(strings.ToUpper(line), "FROM") || + strings.HasPrefix(strings.ToUpper(line), "COPY") { switch image := getImageFromLine(line); image { + case "": + continue case "scratch": fmt.Fprintln(os.Stderr, "- scratch image ignored") default: @@ -83,6 +86,20 @@ func getImagesFromDockerfile(dockerfile io.Reader) ([]string, error) { } func getImageFromLine(line string) string { + if strings.HasPrefix(strings.ToUpper(line), "COPY") { + line = strings.TrimPrefix(line, "COPY") // Remove "COPY" prefix + line = strings.TrimSpace(line) + line = strings.TrimPrefix(line, "--from") // Remove "--from" prefix + foo := strings.Split(line, "=") // Get image ref after "=" + if len(foo) != 2 { + return "" + } + // to support `COPY --from=stage /foo/bar` cases + if strings.Contains(foo[1], " ") { + return "" + } + return os.ExpandEnv(foo[1]) // Substitute templated vars + } line = strings.TrimPrefix(line, "FROM") // Remove "FROM" prefix line = os.ExpandEnv(line) // Substitute templated vars fields := strings.Fields(line) diff --git a/cmd/cosign/cli/dockerfile/verify_test.go b/cmd/cosign/cli/dockerfile/verify_test.go index 86672c6f321..3f808c96010 100644 --- a/cmd/cosign/cli/dockerfile/verify_test.go +++ b/cmd/cosign/cli/dockerfile/verify_test.go @@ -86,6 +86,27 @@ func TestGetImagesFromDockerfile(t *testing.T) { }, expected: []string{"gcr.io/gauntlet/test/one", "gcr.io/gauntlet/test/two:latest", "gcr.io/gauntlet/test/runtime"}, }, + { + name: "copy", + fileContents: `FROM gcr.io/test/image@sha256:d131624e6f5d8695e9aea7a0439f7bac0fcc50051282e0c3d4d627cab8845ba5 +COPY --from=gcr.io/hello/world`, + expected: []string{ + "gcr.io/test/image@sha256:d131624e6f5d8695e9aea7a0439f7bac0fcc50051282e0c3d4d627cab8845ba5", + "gcr.io/hello/world", + }, + }, + { + name: "copy with stage", + fileContents: `FROM gcr.io/test/image@sha256:d131624e6f5d8695e9aea7a0439f7bac0fcc50051282e0c3d4d627cab8845ba5 +COPY --from=stage /foo/bar`, + expected: []string{"gcr.io/test/image@sha256:d131624e6f5d8695e9aea7a0439f7bac0fcc50051282e0c3d4d627cab8845ba5"}, + }, + { + name: "copy files", + fileContents: `FROM gcr.io/test/image@sha256:d131624e6f5d8695e9aea7a0439f7bac0fcc50051282e0c3d4d627cab8845ba5 +COPY . .`, + expected: []string{"gcr.io/test/image@sha256:d131624e6f5d8695e9aea7a0439f7bac0fcc50051282e0c3d4d627cab8845ba5"}, + }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { diff --git a/doc/cosign_dockerfile_resolve.md b/doc/cosign_dockerfile_resolve.md index 4220c11488c..598f16fb5ac 100644 --- a/doc/cosign_dockerfile_resolve.md +++ b/doc/cosign_dockerfile_resolve.md @@ -2,6 +2,23 @@ Resolve the digest of the images and rewrites them with fully qualified image reference +### Synopsis + +Resolve the digest of the images and rewrites them with fully qualified image reference + +This command creates a surface which resolves mutable image tags into immutable image digests: + - FROM , it rewrites the Dockerfile to FROM + +Using FROM without is dangerous because even if what's currently tagged on the registry is signed properly, +there is a race before the FROM is evaluated (what if it changes!), or (with docker build) it's possible that +what is in the local cache(!) is what's actually used, and not what was verified! (See issue #648) + +This command does NOT do image verification; instead it only rewrites all image tags to corresponding digest(s). + +The following image reference definitions are currently supported: + - FROM --platform=linux/amd64 gcr.io/distroless/base AS base + - COPY --from=gcr.io/distroless/base + ``` cosign dockerfile resolve [flags] ``` @@ -11,9 +28,11 @@ cosign dockerfile resolve [flags] ``` cosign dockerfile resolve Dockerfile - # specify output file - cosign dockerfile resolve -o Dockerfile.edited Dockerfile + # print to stdout + cosign dockerfile resolve Dockerfile + # specify a output file + cosign dockerfile resolve -o Dockerfile.resolved Dockerfile ``` ### Options @@ -26,9 +45,9 @@ cosign dockerfile resolve [flags] ### Options inherited from parent commands ``` - --azure-container-registry-config string Path to the file containing Azure container registry configuration information. - --output-file string log output to a file - -d, --verbose log debug output + --output-file string log output to a file + -t, --timeout duration timeout for commands (default 3m0s) + -d, --verbose log debug output ``` ### SEE ALSO diff --git a/test/e2e_test.sh b/test/e2e_test.sh index 76aff31c82d..2466e5242d6 100755 --- a/test/e2e_test.sh +++ b/test/e2e_test.sh @@ -57,8 +57,8 @@ if (./cosign dockerfile verify --key ${DISTROLESS_PUB_KEY} ./test/testdata/unsig test_image="gcr.io/distroless/base" ./cosign dockerfile verify --key ${DISTROLESS_PUB_KEY} ./test/testdata/with_arg.Dockerfile # Test dockerfile resolve and verify -./cosign dockerfile resolve -o ./test/testdata/fancy_from.Dockerfile.resolved ./test/testdata/fancy_from.Dockerfile -./cosign dockerfile verify --key ${DISTROLESS_PUB_KEY} ./test/testdata/fancy_from.Dockerfile.resolved +./cosign dockerfile resolve -o ./test/testdata/with_copy.Dockerfile.resolved ./test/testdata/with_copy.Dockerfile +./cosign dockerfile verify --key ${DISTROLESS_PUB_KEY} ./test/testdata/with_copy.Dockerfile.resolved # Image exists, but is unsigned if (test_image="ubuntu" ./cosign dockerfile verify --key ${DISTROLESS_PUB_KEY} ./test/testdata/with_arg.Dockerfile); then false; fi diff --git a/test/testdata/with_copy.Dockerfile b/test/testdata/with_copy.Dockerfile new file mode 100644 index 00000000000..1aec4097e9a --- /dev/null +++ b/test/testdata/with_copy.Dockerfile @@ -0,0 +1,18 @@ +# Copyright 2021 The Sigstore Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http:#www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +FROM --platform=linux/amd64 gcr.io/distroless/base AS base +COPY --from=gcr.io/distroless/base /foo /foo + +# blah blah