From c484bda04da8b28dd6283234e48fd0a2dbea63d7 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 --- build/build.go | 10 +++++ build/git.go | 98 +++++++++++++++++++++++++++++++++++++++++++ build/git_test.go | 103 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 211 insertions(+) create mode 100644 build/git.go create mode 100644 build/git_test.go diff --git a/build/build.go b/build/build.go index 261b377abefe..d0eaa3e49d9d 100644 --- a/build/build.go +++ b/build/build.go @@ -786,6 +786,16 @@ func BuildWithResultHandler(ctx context.Context, drivers []DriverInfo, opt map[s eg, ctx := errgroup.WithContext(ctx) + for _, opt := range opt { + gitLabels, err := addGitProvenance(ctx, opt.Inputs.ContextPath, opt.Inputs.DockerfilePath) + if err != nil { + return nil, err + } + for n, v := range gitLabels { + opt.Labels[n] = v + } + } + for k, opt := range opt { multiDriver := len(m[k]) > 1 hasMobyDriver := false diff --git a/build/git.go b/build/git.go new file mode 100644 index 000000000000..8668c9440ab9 --- /dev/null +++ b/build/git.go @@ -0,0 +1,98 @@ +package build + +import ( + "context" + "os" + "os/exec" + "path/filepath" + "strings" + + ocispecs "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" +) + +const DockerfileLabel = "com.docker.image.source.entrypoint" + +func addGitProvenance(ctx context.Context, contextPath string, dockerfilePath string) (map[string]string, error) { + v, ok := os.LookupEnv("BUILDX_GIT_LABELS") + if !ok || contextPath == "" { + return nil, nil + } + labels := make(map[string]string, 0) + + // figure out in which directory the git command needs to run in + var wd string + if filepath.IsAbs(contextPath) { + wd = contextPath + } else { + cwd, _ := os.Getwd() + wd, _ = filepath.Abs(filepath.Join(cwd, contextPath)) + } + + // check if inside git working tree + cmd := exec.CommandContext(ctx, "git", "rev-parse", "--is-inside-work-tree") + cmd.Dir = wd + err := cmd.Run() + if err != nil { + logrus.Warnf("Unable to determine Git information") + return nil, nil + } + + // obtain Git sha of current HEAD + cmd = exec.CommandContext(ctx, "git", "rev-parse", "HEAD") + cmd.Dir = wd + out, err := cmd.Output() + if err != nil { + return nil, errors.Wrap(err, "error obtaining git head") + } + sha := strings.TrimSpace(string(out)) + + // check if the current HEAD is clean + cmd = exec.CommandContext(ctx, "git", "status", "--porcelain", "--ignored") + cmd.Dir = wd + out, err = cmd.Output() + if err != nil { + return nil, errors.Wrap(err, "error obtaining git status") + } + if len(strings.TrimSpace(string(out))) != 0 { + sha += "-dirty" + } + labels[ocispecs.AnnotationRevision] = sha + + // add a remote url if full Git details are requested; if there aren't any remotes don't fail + if v == "full" { + cmd = exec.CommandContext(ctx, "git", "ls-remote", "--get-url") + cmd.Dir = wd + out, _ := cmd.Output() + if len(out) > 0 { + labels[ocispecs.AnnotationSource] = strings.TrimSpace(string(out)) + } + } + + // add Dockerfile path; there is no org.opencontainers annotation for this + if dockerfilePath == "" { + dockerfilePath = filepath.Join(wd, "Dockerfile") + } + + // obtain Git root directory + cmd = exec.CommandContext(ctx, "git", "rev-parse", "--show-toplevel") + cmd.Dir = wd + out, err = cmd.Output() + if err != nil { + return nil, errors.Wrap(err, "failed to get git root") + } + root := strings.TrimSpace(string(out)) + + // record only Dockerfile paths that are within the Git root + if !filepath.IsAbs(dockerfilePath) { + cwd, _ := os.Getwd() + dockerfilePath = filepath.Join(cwd, dockerfilePath) + } + dockerfilePath, _ = filepath.Rel(root, dockerfilePath) + if !strings.HasPrefix(dockerfilePath, "..") { + labels[DockerfileLabel] = dockerfilePath + } + + return labels, nil +} diff --git a/build/git_test.go b/build/git_test.go new file mode 100644 index 000000000000..37aa3378bb50 --- /dev/null +++ b/build/git_test.go @@ -0,0 +1,103 @@ +package build + +import ( + "context" + "fmt" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + "testing" + + ocispecs "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/stretchr/testify/assert" +) + +var repoDir string + +func setupTest(tb testing.TB) func(tb testing.TB) { + repoDir = tb.TempDir() + // required for local testing on mac to avoid strange /private symlinks + if runtime.GOOS == "darwin" { + repoDir, _ = filepath.EvalSymlinks(repoDir) + } + cmd := exec.Command("git", "init") + cmd.Dir = repoDir + err := cmd.Run() + assert.Nilf(tb, err, "failed to init git repo: %v", err) + + df := []byte("FROM alpine:latest\n") + err = os.WriteFile(filepath.Join(repoDir, "Dockerfile"), df, 0644) + assert.Nilf(tb, err, "failed to write file: %v", err) + + cmd = exec.Command("git", "add", "Dockerfile") + cmd.Dir = repoDir + err = cmd.Run() + assert.Nilf(tb, err, "failed to add file: %v", err) + + cmd = exec.Command("git", "config", "user.name", "buildx") + cmd.Dir = repoDir + err = cmd.Run() + assert.Nilf(tb, err, "failed to set git user.name: %v", err) + + cmd = exec.Command("git", "config", "user.email", "buildx@docker.com") + cmd.Dir = repoDir + err = cmd.Run() + assert.Nilf(tb, err, "failed to set git user.email: %v", err) + + cmd = exec.Command("git", "commit", "-m", "Initial commit") + cmd.Dir = repoDir + err = cmd.Run() + assert.Nilf(tb, err, "failed to commit: %v", err) + + return func(tb testing.TB) { + os.Unsetenv("BUILDX_GIT_LABELS") + os.RemoveAll(repoDir) + } +} + +func TestAddGitProvenanceDataWithoutEnv(t *testing.T) { + defer setupTest(t)(t) + labels, err := addGitProvenance(context.Background(), repoDir, filepath.Join(repoDir, "Dockerfile")) + assert.Nilf(t, err, "No error expected") + assert.Nilf(t, labels, "No labels expected") +} + +func TestAddGitProvenanceDataWithoutLabels(t *testing.T) { + defer setupTest(t)(t) + os.Setenv("BUILDX_GIT_LABELS", "full") + labels, err := addGitProvenance(context.Background(), repoDir, filepath.Join(repoDir, "Dockerfile")) + assert.Nilf(t, err, "No error expected") + assert.Equal(t, 2, len(labels), "Exactly 2 git provenance labels expected") + assert.Equal(t, "Dockerfile", labels[DockerfileLabel], "Expected a dockerfile path provenance label") + + cmd := exec.Command("git", "rev-parse", "HEAD") + cmd.Dir = repoDir + out, _ := cmd.Output() + assert.Equal(t, strings.TrimSpace(string(out)), labels[ocispecs.AnnotationRevision], "Expected a sha provenance label") +} + +func TestAddGitProvenanceDataWithLabels(t *testing.T) { + defer setupTest(t)(t) + // make a change to test dirty flag + df := []byte("FROM alpine:edge\n") + os.Mkdir(filepath.Join(repoDir, "dir"), 0755) + os.WriteFile(filepath.Join(repoDir, "dir", "Dockerfile"), df, 0644) + // add a remote + cmd := exec.Command("git", "remote", "add", "origin", "git@github.com:docker/buildx.git") + cmd.Dir = repoDir + cmd.Run() + + os.Setenv("BUILDX_GIT_LABELS", "full") + labels, err := addGitProvenance(context.Background(), repoDir, filepath.Join(repoDir, "Dockerfile")) + assert.Nilf(t, err, "No error expected") + assert.Equal(t, 3, len(labels), "Exactly 3 git provenance labels expected") + assert.Equal(t, "Dockerfile", labels[DockerfileLabel], "Expected a dockerfile path provenance label") + assert.Equal(t, "git@github.com:docker/buildx.git", labels[ocispecs.AnnotationSource], "Expected a remote provenance label") + + cmd = exec.Command("git", "rev-parse", "HEAD") + cmd.Dir = repoDir + out, _ := cmd.Output() + assert.Equal(t, fmt.Sprintf("%s-dirty", strings.TrimSpace(string(out))), labels[ocispecs.AnnotationRevision], "Expected a sha provenance label") +}