Skip to content

Commit

Permalink
feat(dockerfile): add resolve command
Browse files Browse the repository at this point in the history
Fixes sigstore#648
Fixes sigstore#707

Signed-off-by: Furkan <furkan.turkal@trendyol.com>
Co-authored-by: Batuhan <batuhan.apaydin@trendyol.com>
  • Loading branch information
Dentrax and developer-guy committed Jun 18, 2022
1 parent 5e261dc commit 4e38dab
Show file tree
Hide file tree
Showing 5 changed files with 204 additions and 0 deletions.
23 changes: 23 additions & 0 deletions cmd/cosign/cli/dockerfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ func Dockerfile() *cobra.Command {

cmd.AddCommand(
dockerfileVerify(),
dockerfileResolve(),
)

return cmd
Expand Down Expand Up @@ -110,3 +111,25 @@ Shell-like variables in the Dockerfile's FROM lines will be substituted with val

return cmd
}

func dockerfileResolve() *cobra.Command {
o := &options.ResolveDockerfileOptions{}

cmd := &cobra.Command{
Use: "resolve",
Short: "",
Long: ``,
Example: ``,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
v := &dockerfile.ResolveDockerfileCommand{
Output: o.Output,
}
return v.Exec(cmd.Context(), args)
},
}

o.AddFlags(cmd)

return cmd
}
85 changes: 85 additions & 0 deletions cmd/cosign/cli/dockerfile/resolve.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package dockerfile

import (
"bufio"
"bytes"
"context"
"flag"
"fmt"
"github.com/google/go-containerregistry/pkg/name"
"github.com/sigstore/cosign/pkg/oci/remote"
"io"
"os"
"strings"

"github.com/pkg/errors"
)

// ResolveDockerfileCommand rewrites the Dockerfile
// base images to FROM <digest>.
type ResolveDockerfileCommand struct {
Output string
}

// Exec runs the resolve dockerfile command
func (c *ResolveDockerfileCommand) Exec(ctx context.Context, args []string) error {
if len(args) != 1 {
return flag.ErrHelp
}

dockerfile, err := os.Open(args[0])
if err != nil {
return fmt.Errorf("could not open Dockerfile: %w", err)
}
defer dockerfile.Close()

resolvedDockerfile, err := resolveDigest(dockerfile)
if err != nil {
return fmt.Errorf("failed extracting images from Dockerfile: %w", err)
}

fmt.Fprintln(os.Stderr, string(resolvedDockerfile))

return nil
}

func resolveDigest(dockerfile io.Reader) ([]byte, error) {
fileScanner := bufio.NewScanner(dockerfile)
tmp := bytes.NewBuffer([]byte{})

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") {
switch image := getImageFromLine(line); image {
case "scratch":
tmp.WriteString(line)
default:
ref, err := name.ParseReference(image)
if err != nil {
// we should not return err here since
// we can define the image refs smth like
// i.e., FROM alpine:$(TAG), FROM $(IMAGE), etc.
tmp.WriteString(line)
tmp.WriteString("\n")
continue
}

d, err := remote.ResolveDigest(ref)
if err != nil {
return nil, errors.Wrap(err, "resolving digest")
}

// rewrite the image as follows:
// alpine:3.13 => index.docker.io/library/alpine@sha256:026f721af4cf2843e07bba648e158fb35ecc876d822130633cc49f707f0fc88c
tmp.WriteString(strings.ReplaceAll(line, image, d.String()))
}
} else {
tmp.WriteString(line)
}
tmp.WriteString("\n")
}

return tmp.Bytes(), nil
}
77 changes: 77 additions & 0 deletions cmd/cosign/cli/dockerfile/resolve_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package dockerfile

import (
"bytes"
"reflect"
"testing"
)

func Test_resolveDigest(t *testing.T) {
tests := []struct {
name string
dockerfile string
want string
wantErr bool
}{
{
"happy alpine",
`FROM alpine:3.13`,
`FROM index.docker.io/library/alpine@sha256:026f721af4cf2843e07bba648e158fb35ecc876d822130633cc49f707f0fc88c
`,
false,
},
{
"alpine with digest",
`FROM alpine@sha256:026f721af4cf2843e07bba648e158fb35ecc876d822130633cc49f707f0fc88c`,
`FROM alpine@sha256:026f721af4cf2843e07bba648e158fb35ecc876d822130633cc49f707f0fc88c
`,
false,
},
{
"multi-line",
`FROM alpine:3.13
COPY . .
RUN ls`,
`FROM index.docker.io/library/alpine@sha256:026f721af4cf2843e07bba648e158fb35ecc876d822130633cc49f707f0fc88c
COPY . .
RUN ls
`,
false,
},
{
"skip scratch",
`FROM alpine:3.13
FROM scratch
RUN ls`,
`FROM index.docker.io/library/alpine@sha256:026f721af4cf2843e07bba648e158fb35ecc876d822130633cc49f707f0fc88c
FROM scratch
RUN ls
`,
false,
},
{
"should not break invalid image ref",
`FROM alpine:$(TAG)
FROM $(IMAGE)
`,
`FROM alpine:$(TAG)
FROM $(IMAGE)
`,
false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := resolveDigest(bytes.NewBuffer([]byte(tt.dockerfile)))
if (err != nil) != tt.wantErr {
t.Errorf("resolveDigest() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(string(got), tt.want) {
t.Errorf("resolveDigest() got = %v, want %v", string(got), tt.want)
}
})
}
}
1 change: 1 addition & 0 deletions cmd/cosign/cli/dockerfile/verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ func getImagesFromDockerfile(dockerfile io.Reader) ([]string, error) {
fileScanner := bufio.NewScanner(dockerfile)
for fileScanner.Scan() {
line := strings.TrimSpace(fileScanner.Text())
// what about the COPY --from=image:tag cases?
if strings.HasPrefix(strings.ToUpper(line), "FROM") {
switch image := getImageFromLine(line); image {
case "scratch":
Expand Down
18 changes: 18 additions & 0 deletions cmd/cosign/cli/options/dockerfile.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package options

import (
"github.com/spf13/cobra"
)

// ResolveDockerfileOptions is the top level wrapper for the `verify blob` command.
type ResolveDockerfileOptions struct {
Output string
}

var _ Interface = (*ResolveDockerfileOptions)(nil)

// AddFlags implements Interface
func (o *ResolveDockerfileOptions) AddFlags(cmd *cobra.Command) {
cmd.Flags().StringVar(&o.Output, "output", "",
"output an updated Dockerfile to file")
}

0 comments on commit 4e38dab

Please sign in to comment.