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>
Signed-off-by: Furkan <furkan.turkal@trendyol.com>
  • Loading branch information
Dentrax and developer-guy committed Oct 6, 2022
1 parent 3bd2369 commit e3bb571
Show file tree
Hide file tree
Showing 10 changed files with 155 additions and 25 deletions.
23 changes: 19 additions & 4 deletions cmd/cosign/cli/dockerfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,12 +123,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 is not supported yet. consider setting all environment variables before resolving the Dockerfile. Image: %s.\n", image)
tmp.WriteString(line)
tmp.WriteString("\n")
continue
Expand Down
46 changes: 41 additions & 5 deletions cmd/cosign/cli/dockerfile/resolve_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,31 @@ func Test_resolveDigest(t *testing.T) {
{
"happy alpine",
`FROM alpine:3.13`,
`FROM index.docker.io/library/alpine@sha256:026f721af4cf2843e07bba648e158fb35ecc876d822130633cc49f707f0fc88c
`FROM index.docker.io/library/alpine@sha256:100448e45467d4f3838fc8d95faab2965e22711b6edf67bbd8ec9c07f612b553
`,
false,
},
{
"happy alpine trim",
` FROM alpine:3.13 `,
`FROM index.docker.io/library/alpine@sha256:100448e45467d4f3838fc8d95faab2965e22711b6edf67bbd8ec9c07f612b553
`,
false,
},
{
"happy alpine copy",
`FROM alpine:3.13
COPY --from=alpine:3.13
`,
`FROM index.docker.io/library/alpine@sha256:100448e45467d4f3838fc8d95faab2965e22711b6edf67bbd8ec9c07f612b553
COPY --from=index.docker.io/library/alpine@sha256:100448e45467d4f3838fc8d95faab2965e22711b6edf67bbd8ec9c07f612b553
`,
false,
},
{
"alpine with digest",
`FROM alpine@sha256:026f721af4cf2843e07bba648e158fb35ecc876d822130633cc49f707f0fc88c`,
`FROM alpine@sha256:026f721af4cf2843e07bba648e158fb35ecc876d822130633cc49f707f0fc88c
`FROM alpine@sha256:100448e45467d4f3838fc8d95faab2965e22711b6edf67bbd8ec9c07f612b553`,
`FROM alpine@sha256:100448e45467d4f3838fc8d95faab2965e22711b6edf67bbd8ec9c07f612b553
`,
false,
},
Expand All @@ -47,7 +64,7 @@ func Test_resolveDigest(t *testing.T) {
COPY . .
RUN ls`,
`FROM index.docker.io/library/alpine@sha256:026f721af4cf2843e07bba648e158fb35ecc876d822130633cc49f707f0fc88c
`FROM index.docker.io/library/alpine@sha256:100448e45467d4f3838fc8d95faab2965e22711b6edf67bbd8ec9c07f612b553
COPY . .
RUN ls
Expand All @@ -59,7 +76,7 @@ RUN ls
`FROM alpine:3.13
FROM scratch
RUN ls`,
`FROM index.docker.io/library/alpine@sha256:026f721af4cf2843e07bba648e158fb35ecc876d822130633cc49f707f0fc88c
`FROM index.docker.io/library/alpine@sha256:100448e45467d4f3838fc8d95faab2965e22711b6edf67bbd8ec9c07f612b553
FROM scratch
RUN ls
`,
Expand All @@ -72,6 +89,25 @@ FROM $(IMAGE)
`,
`FROM alpine:$(TAG)
FROM $(IMAGE)
`,
false,
},
{
"should not break for invalid --from image reference",
`FROM golang:latest AS builder
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .
FROM alpine:latest
WORKDIR /root/
COPY --from=builder /go/src/github.com/foo/bar/app .
CMD ["./app"]`,
`FROM index.docker.io/library/golang@sha256:27ff940e5e460ef6dc80311c7bb9c633871bb99a1f45e190fa29864a1ea7209a AS builder
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .
FROM index.docker.io/library/alpine@sha256:bc41182d7ef5ffc53a40b044e725193bc10142a1243f395ee852a8d9730fc2ad
WORKDIR /root/
COPY --from=builder /go/src/github.com/foo/bar/app .
CMD ["./app"]
`,
false,
},
Expand Down
23 changes: 20 additions & 3 deletions cmd/cosign/cli/dockerfile/verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,14 @@ import (
"github.com/sigstore/cosign/cmd/cosign/cli/verify"
)

// VerifyCommand verifies a signature on a supplied container image
// VerifyDockerfileCommand verifies a signature on a supplied container image.
// nolint
type VerifyDockerfileCommand struct {
verify.VerifyCommand
BaseOnly bool
}

// Exec runs the verification command
// Exec runs the verification command.
func (c *VerifyDockerfileCommand) Exec(ctx context.Context, args []string) error {
if len(args) != 1 {
return flag.ErrHelp
Expand Down 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
2 changes: 1 addition & 1 deletion doc/cosign_dockerfile.md

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

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.

2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ require (
github.com/miekg/pkcs11 v1.1.1
github.com/mozillazg/docker-credential-acr-helper v0.3.0
github.com/open-policy-agent/opa v0.44.0
github.com/pkg/errors v0.9.1
github.com/secure-systems-lab/go-securesystemslib v0.4.0
github.com/sigstore/fulcio v0.6.0
github.com/sigstore/rekor v0.12.1-0.20220915152154-4bb6f441c1b2
Expand Down Expand Up @@ -220,7 +221,6 @@ require (
github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pelletier/go-toml/v2 v2.0.5 // indirect
github.com/pierrec/lz4 v2.6.1+incompatible // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_golang v1.13.0 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
Expand Down
4 changes: 2 additions & 2 deletions test/e2e_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ if (./cosign dockerfile verify ./test/testdata/unsigned_build_stage.Dockerfile);
test_image="ghcr.io/distroless/alpine-base" ./cosign dockerfile verify ./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 ./test/testdata/with_copy.Dockerfile.resolved
# Image exists, but is unsigned
if (test_image="ubuntu" ./cosign dockerfile verify ./test/testdata/with_arg.Dockerfile); then false; fi
./cosign dockerfile verify ./test/testdata/with_lowercase.Dockerfile
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 e3bb571

Please sign in to comment.