Skip to content

Commit d8ccd9f

Browse files
authored
feat: add commit digest in subject (#382)
Signed-off-by: Miguel Martinez Trivino <miguel@chainloop.dev>
1 parent 46431db commit d8ccd9f

File tree

11 files changed

+477
-162
lines changed

11 files changed

+477
-162
lines changed

app/cli/api/attestation/v1/crafting_state.pb.go

Lines changed: 118 additions & 107 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/cli/api/attestation/v1/crafting_state.pb.validate.go

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/cli/api/attestation/v1/crafting_state.proto

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,9 @@ message Attestation {
8383
map<string, string> env_vars = 6;
8484
string runner_url = 7;
8585
workflowcontract.v1.CraftingSchema.Runner.RunnerType runner_type = 8;
86+
87+
// SHA1 of the environment where the attestation was executed
88+
string sha1_commit = 9;
8689
}
8790

8891
// Intermediate information that will get stored in the system while the run is being executed

go.mod

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ require (
5656
go.uber.org/zap v1.25.0
5757
golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63
5858
golang.org/x/oauth2 v0.11.0
59-
golang.org/x/term v0.11.0
59+
golang.org/x/term v0.12.0
6060
google.golang.org/api v0.138.0
6161
google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d // indirect
6262
google.golang.org/grpc v1.57.0
@@ -83,26 +83,38 @@ require (
8383
dario.cat/mergo v1.0.0 // indirect
8484
github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 // indirect
8585
github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0 // indirect
86+
github.com/acomagu/bufpipe v1.0.4 // indirect
8687
github.com/anchore/go-struct-converter v0.0.0-20230627203149-c72ef8859ca9 // indirect
8788
github.com/cockroachdb/apd/v3 v3.2.0 // indirect
8889
github.com/cpuguy83/dockercfg v0.3.1 // indirect
90+
github.com/cyphar/filepath-securejoin v0.2.4 // indirect
91+
github.com/emirpasic/gods v1.18.1 // indirect
8992
github.com/fatih/color v1.15.0 // indirect
9093
github.com/felixge/httpsnoop v1.0.3 // indirect
94+
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
95+
github.com/go-git/go-billy/v5 v5.5.0 // indirect
9196
github.com/google/gnostic-models v0.6.8 // indirect
9297
github.com/google/pprof v0.0.0-20221103000818-d260c55eee4c // indirect
9398
github.com/google/renameio/v2 v2.0.0 // indirect
9499
github.com/gorilla/handlers v1.5.1 // indirect
95100
github.com/hashicorp/go-hclog v1.5.0 // indirect
96101
github.com/hashicorp/yamux v0.1.1 // indirect
102+
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
103+
github.com/kevinburke/ssh_config v1.2.0 // indirect
97104
github.com/kylelemons/godebug v1.1.0 // indirect
98105
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
99106
github.com/nozzle/throttler v0.0.0-20180817012639-2ea982251481 // indirect
100107
github.com/oklog/run v1.1.0 // indirect
101108
github.com/package-url/packageurl-go v0.1.1 // indirect
109+
github.com/pjbgf/sha1cd v0.3.0 // indirect
102110
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect
103111
github.com/pkg/xattr v0.4.9 // indirect
112+
github.com/sergi/go-diff v1.3.1 // indirect
113+
github.com/skeema/knownhosts v1.2.0 // indirect
114+
github.com/xanzy/ssh-agent v0.3.3 // indirect
104115
go.opentelemetry.io/otel/metric v1.17.0 // indirect
105116
gopkg.in/go-jose/go-jose.v2 v2.6.1 // indirect
117+
gopkg.in/warnings.v0 v0.1.2 // indirect
106118
)
107119

108120
require (
@@ -146,6 +158,7 @@ require (
146158
github.com/fsouza/fake-gcs-server v1.45.2
147159
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
148160
github.com/go-chi/chi v4.1.2+incompatible // indirect
161+
github.com/go-git/go-git/v5 v5.9.0
149162
github.com/go-jose/go-jose/v3 v3.0.0 // indirect
150163
github.com/go-kratos/aegis v0.2.0 // indirect
151164
github.com/go-logr/logr v1.2.4 // indirect
@@ -254,14 +267,14 @@ require (
254267
go.opentelemetry.io/otel v1.17.0 // indirect
255268
go.opentelemetry.io/otel/trace v1.17.0 // indirect
256269
go.uber.org/multierr v1.11.0 // indirect
257-
golang.org/x/crypto v0.12.0 // indirect
270+
golang.org/x/crypto v0.13.0 // indirect
258271
golang.org/x/mod v0.12.0 // indirect
259-
golang.org/x/net v0.14.0 // indirect
272+
golang.org/x/net v0.15.0 // indirect
260273
golang.org/x/sync v0.3.0 // indirect
261-
golang.org/x/sys v0.11.0 // indirect
262-
golang.org/x/text v0.12.0 // indirect
274+
golang.org/x/sys v0.12.0 // indirect
275+
golang.org/x/text v0.13.0 // indirect
263276
golang.org/x/time v0.3.0 // indirect
264-
golang.org/x/tools v0.12.1-0.20230815132531-74c255bcf846 // indirect
277+
golang.org/x/tools v0.13.0 // indirect
265278
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
266279
google.golang.org/appengine v1.6.7 // indirect
267280
google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect

go.sum

Lines changed: 53 additions & 16 deletions
Large diffs are not rendered by default.

internal/attestation/crafter/crafter.go

Lines changed: 60 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ import (
3131
schemaapi "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1"
3232
"github.com/chainloop-dev/chainloop/internal/attestation/crafter/materials"
3333
"github.com/chainloop-dev/chainloop/internal/casclient"
34+
"github.com/go-git/go-git/v5"
35+
"github.com/go-git/go-git/v5/plumbing"
3436
"github.com/rs/zerolog"
3537
"google.golang.org/protobuf/encoding/protojson"
3638
"google.golang.org/protobuf/types/known/timestamppb"
@@ -42,6 +44,7 @@ type Crafter struct {
4244
statePath string
4345
CraftingState *api.CraftingState
4446
Runner supportedRunner
47+
workingDir string
4548
}
4649

4750
var ErrAttestationStateNotLoaded = errors.New("crafting state not loaded")
@@ -61,14 +64,22 @@ func WithLogger(l *zerolog.Logger) NewOpt {
6164
}
6265
}
6366

67+
func WithWorkingDirPath(path string) NewOpt {
68+
return func(c *Crafter) {
69+
c.workingDir = path
70+
}
71+
}
72+
6473
// Create a completely new crafter
6574
func NewCrafter(opts ...NewOpt) *Crafter {
6675
noopLogger := zerolog.Nop()
6776
defaultStatePath := filepath.Join(os.TempDir(), "chainloop_attestation.tmp.json")
6877

78+
cw, _ := os.Getwd()
6979
c := &Crafter{
70-
logger: &noopLogger,
71-
statePath: defaultStatePath,
80+
logger: &noopLogger,
81+
statePath: defaultStatePath,
82+
workingDir: cw,
7283
}
7384

7485
for _, opt := range opts {
@@ -175,9 +186,13 @@ func LoadSchema(pathOrURI string) (*schemaapi.CraftingSchema, error) {
175186
// Initialize the temporary file with the content of the schema
176187
func (c *Crafter) initCraftingStateFile(schema *schemaapi.CraftingSchema, wf *api.WorkflowMetadata, dryRun bool, runnerType schemaapi.CraftingSchema_Runner_RunnerType, jobURL string) error {
177188
// Generate Crafting state
178-
state := initialCraftingState(schema, wf, dryRun, runnerType, jobURL)
189+
state, err := initialCraftingState(c.workingDir, schema, wf, dryRun, runnerType, jobURL)
190+
if err != nil {
191+
return fmt.Errorf("initializing crafting state: %w", err)
192+
}
193+
179194
if err := persistCraftingState(state, c.statePath); err != nil {
180-
return err
195+
return fmt.Errorf("failed to persist crafting state: %w", err)
181196
}
182197

183198
c.logger.Debug().Str("path", c.statePath).Msg("created state file")
@@ -222,7 +237,45 @@ func (c *Crafter) LoadCraftingState() error {
222237
return nil
223238
}
224239

225-
func initialCraftingState(schema *schemaapi.CraftingSchema, wf *api.WorkflowMetadata, dryRun bool, runnerType schemaapi.CraftingSchema_Runner_RunnerType, jobURL string) *api.CraftingState {
240+
// Returns the current directory git commit hash if possible
241+
// If we are not in a git repo it will return an empty string
242+
func gracefulGitRepoHead(path string) (string, error) {
243+
repo, err := git.PlainOpenWithOptions(path, &git.PlainOpenOptions{
244+
// walk up the directory tree until we find a git repo
245+
DetectDotGit: true,
246+
})
247+
248+
if err != nil {
249+
if errors.Is(err, git.ErrRepositoryNotExists) {
250+
return "", nil
251+
}
252+
253+
return "", fmt.Errorf("opening repository: %w", err)
254+
}
255+
256+
head, err := repo.Head()
257+
if err != nil {
258+
if errors.Is(err, plumbing.ErrReferenceNotFound) {
259+
return "", nil
260+
}
261+
return "", fmt.Errorf("finding repo head: %w", err)
262+
}
263+
264+
commit, err := repo.CommitObject(head.Hash())
265+
if err != nil {
266+
return "", fmt.Errorf("finding head commit: %w", err)
267+
}
268+
269+
return commit.Hash.String(), nil
270+
}
271+
272+
func initialCraftingState(cwd string, schema *schemaapi.CraftingSchema, wf *api.WorkflowMetadata, dryRun bool, runnerType schemaapi.CraftingSchema_Runner_RunnerType, jobURL string) (*api.CraftingState, error) {
273+
// Get git commit hash
274+
commitHash, err := gracefulGitRepoHead(cwd)
275+
if err != nil {
276+
return nil, fmt.Errorf("getting git commit hash: %w", err)
277+
}
278+
226279
// Generate Crafting state
227280
return &api.CraftingState{
228281
InputSchema: schema,
@@ -231,9 +284,10 @@ func initialCraftingState(schema *schemaapi.CraftingSchema, wf *api.WorkflowMeta
231284
Workflow: wf,
232285
RunnerType: runnerType,
233286
RunnerUrl: jobURL,
287+
Sha1Commit: commitHash,
234288
},
235289
DryRun: dryRun,
236-
}
290+
}, nil
237291
}
238292

239293
func persistCraftingState(craftState *api.CraftingState, stateFilePath string) error {

internal/attestation/crafter/crafter_test.go

Lines changed: 58 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,15 @@ package crafter_test
1818
import (
1919
"fmt"
2020
"os"
21+
"path/filepath"
2122
"testing"
23+
"time"
2224

2325
v1 "github.com/chainloop-dev/chainloop/app/cli/api/attestation/v1"
2426
schemaapi "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1"
2527
"github.com/chainloop-dev/chainloop/internal/attestation/crafter"
28+
"github.com/go-git/go-git/v5"
29+
"github.com/go-git/go-git/v5/plumbing/object"
2630
"github.com/stretchr/testify/require"
2731
"google.golang.org/protobuf/proto"
2832

@@ -33,21 +37,34 @@ type crafterSuite struct {
3337
suite.Suite
3438
// initOpts
3539
workflowMetadata *v1.WorkflowMetadata
40+
repoPath string
41+
repoHead string
3642
}
3743

3844
func (s *crafterSuite) TestInit() {
3945
testCases := []struct {
4046
name string
4147
contractPath string
48+
workingDir string
4249
workflowMetadata *v1.WorkflowMetadata
4350
wantErr bool
51+
wantRepoDigest bool
4452
dryRun bool
4553
}{
4654
{
47-
name: "happy path",
55+
name: "happy path inside a git repo",
4856
contractPath: "testdata/contracts/empty_generic.yaml",
4957
workflowMetadata: s.workflowMetadata,
5058
dryRun: true,
59+
workingDir: s.repoPath,
60+
wantRepoDigest: true,
61+
},
62+
{
63+
name: "happy path outside a git repo",
64+
contractPath: "testdata/contracts/empty_generic.yaml",
65+
workflowMetadata: s.workflowMetadata,
66+
workingDir: s.T().TempDir(),
67+
dryRun: true,
5168
},
5269
{
5370
name: "missing metadata",
@@ -67,11 +84,13 @@ func (s *crafterSuite) TestInit() {
6784
workflowMetadata: s.workflowMetadata,
6885
wantErr: false,
6986
dryRun: true,
87+
workingDir: s.T().TempDir(),
7088
},
7189
{
7290
name: "with annotations",
7391
contractPath: "testdata/contracts/with_material_annotations.yaml",
7492
workflowMetadata: s.workflowMetadata,
93+
workingDir: s.T().TempDir(),
7594
dryRun: true,
7695
},
7796
}
@@ -83,7 +102,7 @@ func (s *crafterSuite) TestInit() {
83102

84103
// Make sure that the tests context indicate that we are not in a CI
85104
// this makes the github action runner context to fail
86-
c, err := newInitializedCrafter(s.T(), tc.contractPath, tc.workflowMetadata, tc.dryRun)
105+
c, err := newInitializedCrafter(s.T(), tc.contractPath, tc.workflowMetadata, tc.dryRun, tc.workingDir)
87106
if tc.wantErr {
88107
s.Error(err)
89108
return
@@ -100,6 +119,10 @@ func (s *crafterSuite) TestInit() {
100119
DryRun: tc.dryRun,
101120
}
102121

122+
if tc.wantRepoDigest {
123+
want.Attestation.Sha1Commit = s.repoHead
124+
}
125+
103126
// reset to nil to easily compare them
104127
s.NotNil(c.CraftingState.Attestation.InitializedAt)
105128
c.CraftingState.Attestation.InitializedAt = nil
@@ -122,14 +145,19 @@ type testingCrafter struct {
122145
statePath string
123146
}
124147

125-
func newInitializedCrafter(t *testing.T, contractPath string, wfMeta *v1.WorkflowMetadata, dryRun bool) (*testingCrafter, error) {
148+
func newInitializedCrafter(t *testing.T, contractPath string, wfMeta *v1.WorkflowMetadata, dryRun bool, workingDir string) (*testingCrafter, error) {
126149
contract, err := crafter.LoadSchema(contractPath)
127150
if err != nil {
128151
return nil, err
129152
}
130153

131154
statePath := fmt.Sprintf("%s/attestation.json", t.TempDir())
132-
c := crafter.NewCrafter(crafter.WithStatePath(statePath))
155+
opts := []crafter.NewOpt{crafter.WithStatePath(statePath)}
156+
if workingDir != "" {
157+
opts = append(opts, crafter.WithWorkingDirPath(workingDir))
158+
}
159+
160+
c := crafter.NewCrafter(opts...)
133161
if err = c.Init(&crafter.InitOpts{SchemaV1: contract, WfInfo: wfMeta, DryRun: dryRun}); err != nil {
134162
return nil, err
135163
}
@@ -313,7 +341,7 @@ func (s *crafterSuite) TestResolveEnvVars() {
313341
}
314342
}
315343

316-
c, err := newInitializedCrafter(s.T(), "testdata/contracts/with_env_vars.yaml", &v1.WorkflowMetadata{}, true)
344+
c, err := newInitializedCrafter(s.T(), "testdata/contracts/with_env_vars.yaml", &v1.WorkflowMetadata{}, true, "")
317345
require.NoError(s.T(), err)
318346

319347
err = c.ResolveEnvVars(tc.strict)
@@ -371,6 +399,31 @@ func (s *crafterSuite) SetupTest() {
371399
}
372400

373401
s.T().Setenv("CI", "")
402+
403+
s.repoPath = s.T().TempDir()
404+
repo, err := git.PlainInit(s.repoPath, false)
405+
require.NoError(s.T(), err)
406+
wt, err := repo.Worktree()
407+
require.NoError(s.T(), err)
408+
409+
filename := filepath.Join(s.repoPath, "example-git-file")
410+
if err = os.WriteFile(filename, []byte("hello world!"), 0600); err != nil {
411+
require.NoError(s.T(), err)
412+
}
413+
414+
_, err = wt.Add("example-git-file")
415+
require.NoError(s.T(), err)
416+
417+
h, err := wt.Commit("test commit", &git.CommitOptions{
418+
Author: &object.Signature{
419+
Name: "John Doe",
420+
Email: "john@doe.org",
421+
When: time.Now(),
422+
},
423+
})
424+
require.NoError(s.T(), err)
425+
426+
s.repoHead = h.String()
374427
}
375428

376429
func TestSuite(t *testing.T) {

0 commit comments

Comments
 (0)