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 Dec 10, 2021
1 parent 32367c2 commit 888beff
Show file tree
Hide file tree
Showing 6 changed files with 82 additions and 11 deletions.
21 changes: 17 additions & 4 deletions cmd/cosign/cli/dockerfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,12 +114,25 @@ 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 <verified 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)
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
10 changes: 6 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 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 @@ -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:
Expand All @@ -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)
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
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

# blah blah

0 comments on commit 888beff

Please sign in to comment.