From b88b3e8050d8f3a84bbf8d145e74baee8aee2ed5 Mon Sep 17 00:00:00 2001 From: Christian Dupuis Date: Sat, 20 Aug 2022 12:25:44 +0200 Subject: [PATCH] Add git provenance labels as per #1290 Signed-off-by: Christian Dupuis --- commands/build.go | 87 +++++++++++++++++++++++++++++++++++++++++- commands/build_test.go | 71 ++++++++++++++++++++++++++++++++++ 2 files changed, 157 insertions(+), 1 deletion(-) create mode 100644 commands/build_test.go diff --git a/commands/build.go b/commands/build.go index 6145d4455edf..da346f7c126a 100644 --- a/commands/build.go +++ b/commands/build.go @@ -9,6 +9,7 @@ import ( "fmt" "io" "os" + "os/exec" "path/filepath" "strconv" "strings" @@ -43,6 +44,10 @@ import ( "google.golang.org/grpc/codes" ) +const ImageSourceLabelName = "org.opencontainers.image.source" +const ImageRevisionLabelName = "org.opencontainers.image.revision" +const DockerfileLabelName = "com.docker.image.dockerfile.path" + const defaultTargetName = "default" type buildOptions struct { @@ -128,6 +133,11 @@ func runBuild(dockerCli command.Cli, in buildOptions) (err error) { return err } + labels, err := addGitProvenance(in.labels, in.contextPath, in.dockerfileName) + if err != nil { + return err + } + opts := build.Options{ Inputs: build.Inputs{ ContextPath: in.contextPath, @@ -138,7 +148,7 @@ func runBuild(dockerCli command.Cli, in buildOptions) (err error) { BuildArgs: listToMap(in.buildArgs, true), ExtraHosts: in.extraHosts, ImageIDFile: in.imageIDFile, - Labels: listToMap(in.labels, false), + Labels: listToMap(labels, false), NetworkMode: in.networkMode, NoCache: noCache, NoCacheFilter: in.noCacheFilter, @@ -643,6 +653,81 @@ func parsePrintFunc(str string) (*build.PrintFunc, error) { return f, nil } +func addGitProvenance(labels []string, contextPath string, dockerfilePath string) ([]string, error) { + v, ok := os.LookupEnv("BUILDX_GIT_LABELS") + if !ok || contextPath == "" { + return labels, nil + } + + if len(labels) == 0 { + labels = make([]string, 0) + } + + // figure out in which directory the git command needs to run + var wd string + if filepath.IsAbs(contextPath) { + wd = contextPath + } else { + cwd, _ := os.Getwd() + wd, _ = filepath.Abs(filepath.Join(cwd, contextPath)) + } + + // obtain Git sha of current HEAD + cmd := exec.Command("git", "rev-parse", "HEAD") + cmd.Dir = wd + sha, err := cmd.Output() + if err != nil { + return nil, fmt.Errorf("error obtaining git head: %w", err) + } + + // check if the current HEAD is clean + cmd = exec.Command("git", "diff", "--quiet", "--exit-code") + cmd.Dir = wd + _, err = cmd.Output() + if err == nil { + labels = append(labels, []string{fmt.Sprintf("%s=%s", ImageRevisionLabelName, strings.TrimSpace(string(sha)))}...) + } else { + labels = append(labels, []string{fmt.Sprintf("%s=%s-dirty", ImageRevisionLabelName, strings.TrimSpace(string(sha)))}...) + } + + // add the origin url if full Git details are requested + if v == "full" { + cmd = exec.Command("git", "config", "--get", "remote.origin.url") + cmd.Dir = wd + remote, err := cmd.Output() + if err != nil { + return nil, fmt.Errorf("failed to obtain remote.origin.url: %w", err) + } + labels = append(labels, []string{fmt.Sprintf("%s=%s", ImageSourceLabelName, strings.TrimSpace(string(remote)))}...) + } + + // add Dockerfile path; there is no org.opencontainers annotation for this + if dockerfilePath == "" { + dockerfilePath = "Dockerfile" + } + + // obtain Git root directory + cmd = exec.Command("git", "rev-parse", "--show-toplevel") + cmd.Dir = wd + root, err := cmd.Output() + if err != nil { + return nil, err + } + + // make the Dockerfile path relative to the Git root + if !filepath.IsAbs(dockerfilePath) { + cwd, _ := os.Getwd() + dockerfilePath = filepath.Join(cwd, dockerfilePath) + } + dockerfilePath, err = filepath.Rel(strings.TrimSpace(string(root)), dockerfilePath) + if err != nil { + return nil, err + } + labels = append(labels, []string{fmt.Sprintf("%s=%s", DockerfileLabelName, dockerfilePath)}...) + + return labels, nil +} + func writeMetadataFile(filename string, dt interface{}) error { b, err := json.MarshalIndent(dt, "", " ") if err != nil { diff --git a/commands/build_test.go b/commands/build_test.go new file mode 100644 index 000000000000..076c9a5de590 --- /dev/null +++ b/commands/build_test.go @@ -0,0 +1,71 @@ +package commands + +import ( + "os" + "strings" + "testing" +) + +// nolint:deadcode,unused +func setupTest(tb testing.TB) func(tb testing.TB) { + return func(tb testing.TB) { + os.Unsetenv("BUILDX_GIT_LABELS") + } +} + +func TestAddGitProvenanceDataWithoutEnv(t *testing.T) { + labels, err := addGitProvenance(nil, ".", "") + if err != nil { + t.Error("No error expected") + } + if labels != nil { + t.Error("No labels expected") + } +} + +func TestAddGitProvenanceDataWithoutLabels(t *testing.T) { + os.Setenv("BUILDX_GIT_LABELS", "full") + labels, err := addGitProvenance(nil, ".", "") + if err != nil { + t.Error("No error expected") + } + if len(labels) != 3 { + t.Error("Exactly 3 git provenance labels expected") + } + dockerfileLabel := strings.Split(labels[2], "=") + if dockerfileLabel[0] != DockerfileLabelName { + t.Error("Expected a dockerfile path provenance label") + } + shaLabel := strings.Split(labels[0], "=") + if shaLabel[0] != ImageRevisionLabelName { + t.Error("Expected a sha provenance label") + } + originLabel := strings.Split(labels[1], "=") + if originLabel[0] != ImageSourceLabelName { + t.Error("Expected a origin provenance label") + } +} + +func TestAddGitProvenanceDataWithLabels(t *testing.T) { + os.Setenv("BUILDX_GIT_LABELS", "full") + existingLabels := []string{"foo=bar"} + labels, err := addGitProvenance(existingLabels, ".", "") + if err != nil { + t.Error("No error expected") + } + if len(labels) != 4 { + t.Error("Exactly 3 git provenance labels expected") + } + dockerfileLabel := strings.Split(labels[3], "=") + if dockerfileLabel[0] != DockerfileLabelName { + t.Error("Expected a dockerfile path provenance label") + } + shaLabel := strings.Split(labels[1], "=") + if shaLabel[0] != ImageRevisionLabelName { + t.Error("Expected a sha provenance label") + } + originLabel := strings.Split(labels[2], "=") + if originLabel[0] != ImageSourceLabelName { + t.Error("Expected a origin provenance label") + } +}