Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add git provenance labels #1297

Merged
merged 1 commit into from
Sep 6, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions build/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -786,6 +786,18 @@ 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 {
if _, ok := opt.Labels[n]; !ok {
opt.Labels[n] = v
}
}
}

for k, opt := range opt {
multiDriver := len(m[k]) > 1
hasMobyDriver := false
Expand Down
98 changes: 98 additions & 0 deletions build/git.go
Original file line number Diff line number Diff line change
@@ -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
}
115 changes: 115 additions & 0 deletions build/git_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
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")
}

func TestAddGitProvenanceDataOutsideOfGitRepository(t *testing.T) {
defer setupTest(t)(t)
os.Setenv("BUILDX_GIT_LABELS", "full")
parentDir := filepath.Dir(repoDir)
cwd, _ := os.Getwd()
os.Chdir(parentDir)
labels, err := addGitProvenance(context.Background(), filepath.Base(repoDir), "")
assert.Nilf(t, err, "No error expected")
assert.Equal(t, "Dockerfile", labels[DockerfileLabel], "Expected a dockerfile path provenance label")
os.Chdir(cwd)
}