Skip to content

Commit

Permalink
feat: improve dockerfile verify subcommand
Browse files Browse the repository at this point in the history
support images resolved from ENV, ARG and COPY --from

Signed-off-by: Caleb Woodbine <calebwoodbine.public@gmail.com>
  • Loading branch information
BobyMCbobs committed Sep 28, 2023
1 parent 0044432 commit e6eadee
Show file tree
Hide file tree
Showing 2 changed files with 124 additions and 9 deletions.
88 changes: 81 additions & 7 deletions cmd/cosign/cli/dockerfile/verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ func (c *VerifyDockerfileCommand) Exec(ctx context.Context, args []string) error
}
defer dockerfile.Close()

images, err := getImagesFromDockerfile(ctx, dockerfile)
fc := newFinderCache()
images, err := fc.getImagesFromDockerfile(ctx, dockerfile)
if err != nil {
return fmt.Errorf("failed extracting images from Dockerfile: %w", err)
}
Expand All @@ -62,18 +63,36 @@ func (c *VerifyDockerfileCommand) Exec(ctx context.Context, args []string) error
return c.VerifyCommand.Exec(ctx, images)
}

func getImagesFromDockerfile(ctx context.Context, dockerfile io.Reader) ([]string, error) {
type finderCache struct {
Env map[string]string
}

func newFinderCache() *finderCache {
return &finderCache{
Env: map[string]string{},
}
}

func (fc *finderCache) getImagesFromDockerfile(ctx context.Context, dockerfile io.Reader) ([]string, error) {
var images []string
fileScanner := bufio.NewScanner(dockerfile)
for fileScanner.Scan() {
line := strings.TrimSpace(fileScanner.Text())
if strings.HasPrefix(strings.ToUpper(line), "FROM") {
switch image := getImageFromLine(line); image {
lineUpper := strings.ToUpper(line)
switch {
case strings.HasPrefix(lineUpper, "FROM"):
switch image := fc.getImageFromLine(line); image {
case "scratch":
ui.Infof(ctx, "- scratch image ignored")
default:
images = append(images, image)
}
case strings.HasPrefix(lineUpper, "COPY"):
if image := fc.getImageFromCopyLine(line); image != "" {
images = append(images, image)
}
case strings.HasPrefix(lineUpper, "ENV") || strings.HasPrefix(lineUpper, "ARG"):
fc.getEnvAndArgs(line)
}
}
if err := fileScanner.Err(); err != nil {
Expand All @@ -82,9 +101,17 @@ func getImagesFromDockerfile(ctx context.Context, dockerfile io.Reader) ([]strin
return images, nil
}

func getImageFromLine(line string) string {
line = strings.TrimPrefix(line, "FROM") // Remove "FROM" prefix
line = os.ExpandEnv(line) // Substitute templated vars
func (fc *finderCache) getImageFromLine(line string) string {
line = strings.TrimPrefix(line, "FROM") // Remove "FROM" prefix
line = os.Expand(line, func(key string) string { // Substitute templated vars
if val, ok := fc.Env[key]; ok {
return val
}
if val, ok := os.LookupEnv(key); ok {
return val
}
return ""
})
fields := strings.Fields(line)
for i := len(fields) - 1; i > 0; i-- {
// Remove the "AS" portion of line
Expand All @@ -95,3 +122,50 @@ func getImageFromLine(line string) string {
}
return fields[len(fields)-1] // The image should be the last portion of the line that remains
}

func (fc *finderCache) getImageFromCopyLine(line string) string {
line = strings.TrimPrefix(line, "COPY") // Remove "COPY" prefix
line = os.Expand(line, func(key string) string { // Substitute templated vars
if val, ok := fc.Env[key]; ok {
return val
}
if val, ok := os.LookupEnv(key); ok {
return val
}
return ""
})
fields := strings.Fields(line)
for i := len(fields) - 1; i > 0; i-- {
if strings.Contains(fields[i-1], "--from=") {
fields = fields[:i]
split := strings.Split(fields[i-1], "=")
fields[i-1] = split[1]
break
}
}
return fields[len(fields)-1] // The image should be the last portion of the line that remains
}

func (fc *finderCache) getEnvAndArgs(line string) {
line = strings.TrimPrefix(line, "ENV") // Remove "ENV" prefix
line = strings.TrimPrefix(line, "ARG") // Remove "ARG" prefix
line = os.Expand(line, func(key string) string { // Substitute templated vars
if val, ok := fc.Env[key]; ok {
return val
}
if val, ok := os.LookupEnv(key); ok {
return val
}
return ""
})
fields := strings.Fields(line)
for _, f := range fields {
keyvalue := strings.Split(f, "=")
if len(keyvalue) < 2 {
continue
}
key := strings.Trim(keyvalue[0], " ")
value := strings.Trim(keyvalue[1], " ")
fc.Env[key] = value
}
}
45 changes: 43 additions & 2 deletions cmd/cosign/cli/dockerfile/verify_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,21 +71,61 @@ func TestGetImagesFromDockerfile(t *testing.T) {
},
expected: []string{"gcr.io/env/var/test/repo"},
},
{
name: "with-value-from-arg",
fileContents: `ARG IMAGE=gcr.io/someorg/someimage
FROM ${IMAGE}`,
expected: []string{"gcr.io/someorg/someimage"},
},
{
name: "with-value-from-env",
fileContents: `ENV IMAGE=gcr.io/someorg/someimage
FROM ${IMAGE}`,
expected: []string{"gcr.io/someorg/someimage"},
},
{
name: "with-multiple-values-from-env",
fileContents: `ENV IMAGE_ONE=gcr.io/someorg/someimage IMAGE_TWO=gcr.io/someorg/coolimage
FROM ${IMAGE_ONE}
FROM ${IMAGE_TWO}`,
expected: []string{"gcr.io/someorg/someimage", "gcr.io/someorg/coolimage"},
},
{
name: "with-value-from-arg-from-env",
fileContents: `ARG IMAGE=${THING}
FROM ${IMAGE}`,
expected: []string{"gcr.io/someorg/coolimage"},
env: map[string]string{
"THING": "gcr.io/someorg/coolimage",
},
},
{
name: "image-in-copy",
fileContents: `COPY --from=gcr.io/someorg/someimage /var/www/html /app`,
expected: []string{"gcr.io/someorg/someimage"},
},
{
name: "image-in-copy-with-env",
fileContents: `ENV IMAGE_HERE=gcr.io/someorg/someimage
COPY --from=${IMAGE_HERE} /var/www/html /app`,
expected: []string{"gcr.io/someorg/someimage"},
},
{
name: "gauntlet",
fileContents: `FROM gcr.io/${TEST_IMAGE_REPO_PATH}/one AS one
RUN script1
FROM gcr.io/$TEST_IMAGE_REPO_PATH/${TEST_SUBREPO}:latest
RUN script2
FROM --platform=linux/amd64 gcr.io/${TEST_IMAGE_REPO_PATH}/$TEST_RUNTIME_SUBREPO
COPY --from=gcr.io/someorg/someimage /etc/config /app/etc/config
CMD bin`,
env: map[string]string{
"TEST_IMAGE_REPO_PATH": "gauntlet/test",
"TEST_SUBREPO": "two",
"TEST_RUNTIME_SUBREPO": "runtime",
"SOMETHING_ELSE": "something/else",
},
expected: []string{"gcr.io/gauntlet/test/one", "gcr.io/gauntlet/test/two:latest", "gcr.io/gauntlet/test/runtime"},
expected: []string{"gcr.io/gauntlet/test/one", "gcr.io/gauntlet/test/two:latest", "gcr.io/gauntlet/test/runtime", "gcr.io/someorg/someimage"},
},
}
for _, tc := range testCases {
Expand All @@ -94,8 +134,9 @@ func TestGetImagesFromDockerfile(t *testing.T) {
os.Setenv(k, v)
defer os.Unsetenv(k)
}
fc := newFinderCache()
ctx := context.Background()
got, err := getImagesFromDockerfile(ctx, strings.NewReader(tc.fileContents))
got, err := fc.getImagesFromDockerfile(ctx, strings.NewReader(tc.fileContents))
if err != nil {
t.Fatalf("getImagesFromDockerfile returned error: %v", err)
}
Expand Down

0 comments on commit e6eadee

Please sign in to comment.