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 5, 2022
1 parent 3bd2369 commit fa28561
Show file tree
Hide file tree
Showing 10 changed files with 134 additions and 23 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 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
27 changes: 22 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 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
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 fa28561

Please sign in to comment.