From 499c14764cfd788e536ca34ab7d6c5b8917f3112 Mon Sep 17 00:00:00 2001 From: Furkan Date: Wed, 1 Dec 2021 23:26:12 +0300 Subject: [PATCH] feat(dockerfile): add resolve command Fixes #648 Fixes #707 Signed-off-by: Furkan Co-authored-by: Batuhan --- cmd/cosign/cli/dockerfile.go | 23 ++++++ cmd/cosign/cli/dockerfile/resolve.go | 85 +++++++++++++++++++++++ cmd/cosign/cli/dockerfile/resolve_test.go | 77 ++++++++++++++++++++ cmd/cosign/cli/dockerfile/verify.go | 1 + cmd/cosign/cli/options/dockerfile.go | 18 +++++ 5 files changed, 204 insertions(+) create mode 100644 cmd/cosign/cli/dockerfile/resolve.go create mode 100644 cmd/cosign/cli/dockerfile/resolve_test.go create mode 100644 cmd/cosign/cli/options/dockerfile.go diff --git a/cmd/cosign/cli/dockerfile.go b/cmd/cosign/cli/dockerfile.go index 70aa9018672..c8e3330cad6 100644 --- a/cmd/cosign/cli/dockerfile.go +++ b/cmd/cosign/cli/dockerfile.go @@ -31,6 +31,7 @@ func Dockerfile() *cobra.Command { cmd.AddCommand( dockerfileVerify(), + dockerfileResolve(), ) return cmd @@ -115,3 +116,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 +} diff --git a/cmd/cosign/cli/dockerfile/resolve.go b/cmd/cosign/cli/dockerfile/resolve.go new file mode 100644 index 00000000000..24a7bd9cd53 --- /dev/null +++ b/cmd/cosign/cli/dockerfile/resolve.go @@ -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 . +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 +} diff --git a/cmd/cosign/cli/dockerfile/resolve_test.go b/cmd/cosign/cli/dockerfile/resolve_test.go new file mode 100644 index 00000000000..946d9d0c98d --- /dev/null +++ b/cmd/cosign/cli/dockerfile/resolve_test.go @@ -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) + } + }) + } +} diff --git a/cmd/cosign/cli/dockerfile/verify.go b/cmd/cosign/cli/dockerfile/verify.go index da6cb4412ff..274fffdaec2 100644 --- a/cmd/cosign/cli/dockerfile/verify.go +++ b/cmd/cosign/cli/dockerfile/verify.go @@ -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": diff --git a/cmd/cosign/cli/options/dockerfile.go b/cmd/cosign/cli/options/dockerfile.go new file mode 100644 index 00000000000..674c99087e0 --- /dev/null +++ b/cmd/cosign/cli/options/dockerfile.go @@ -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") +} \ No newline at end of file