Skip to content

Commit

Permalink
feat(dockerfile): ability to parse COPY --from=image
Browse files Browse the repository at this point in the history
Signed-off-by: Furkan <furkan.turkal@trendyol.com>
Co-authored-by: Batuhan <batuhan.apaydin@trendyol.com>
  • Loading branch information
Dentrax and developer-guy committed May 25, 2022
1 parent 146f943 commit ca84780
Show file tree
Hide file tree
Showing 8 changed files with 127 additions and 16 deletions.
23 changes: 19 additions & 4 deletions cmd/cosign/cli/dockerfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,12 +118,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 <tag>, it rewrites the Dockerfile to FROM <digest>
Using FROM without <digest> 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{
Expand Down
12 changes: 8 additions & 4 deletions cmd/cosign/cli/dockerfile/resolve.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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
Expand Down
17 changes: 17 additions & 0 deletions cmd/cosign/cli/dockerfile/resolve_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand Down
19 changes: 18 additions & 1 deletion cmd/cosign/cli/dockerfile/verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,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:
Expand All @@ -82,6 +85,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)
Expand Down
21 changes: 21 additions & 0 deletions cmd/cosign/cli/dockerfile/verify_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
29 changes: 24 additions & 5 deletions doc/cosign_dockerfile_resolve.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions test/e2e_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 18 additions & 0 deletions test/testdata/with_copy.Dockerfile
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit ca84780

Please sign in to comment.