Skip to content

Commit 36d453d

Browse files
authored
feat(controlplane) generate mapping for items in CAS (#327)
Signed-off-by: Miguel Martinez Trivino <[email protected]>
1 parent 9efb869 commit 36d453d

36 files changed

+3593
-17
lines changed

app/controlplane/cmd/wire_gen.go

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

app/controlplane/internal/biz/biz.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ var ProviderSet = wire.NewSet(
3333
NewCASClientUseCase,
3434
NewAttestationUseCase,
3535
NewWorkflowRunExpirerUseCase,
36+
NewCASMappingUseCase,
3637
wire.Struct(new(NewIntegrationUseCaseOpts), "*"),
3738
wire.Struct(new(NewUserUseCaseParams), "*"),
3839
)

app/controlplane/internal/biz/casbackend.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -419,7 +419,7 @@ func (uc *CASBackendUseCase) PerformValidation(ctx context.Context, id string) (
419419

420420
if backend.Provider == CASBackendInline {
421421
// Inline CAS backend does not need validation
422-
return
422+
return nil
423423
}
424424

425425
provider, ok := uc.providers[string(backend.Provider)]
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
//
2+
// Copyright 2023 The Chainloop Authors.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
package biz
17+
18+
import (
19+
"bytes"
20+
"context"
21+
"encoding/json"
22+
"fmt"
23+
"time"
24+
25+
"github.com/chainloop-dev/chainloop/internal/attestation/renderer/chainloop"
26+
"github.com/go-kratos/kratos/v2/log"
27+
cr_v1 "github.com/google/go-containerregistry/pkg/v1"
28+
"github.com/google/uuid"
29+
"github.com/secure-systems-lab/go-securesystemslib/dsse"
30+
)
31+
32+
type CASMapping struct {
33+
ID, CASBackendID, OrgID, WorkflowRunID uuid.UUID
34+
Digest string
35+
CreatedAt *time.Time
36+
}
37+
38+
type CASMappingRepo interface {
39+
Create(ctx context.Context, digest string, casBackendID, workflowRunID uuid.UUID) (*CASMapping, error)
40+
}
41+
42+
type CASMappingUseCase struct {
43+
repo CASMappingRepo
44+
logger *log.Helper
45+
}
46+
47+
func NewCASMappingUseCase(repo CASMappingRepo, logger log.Logger) *CASMappingUseCase {
48+
return &CASMappingUseCase{repo, log.NewHelper(logger)}
49+
}
50+
51+
func (uc *CASMappingUseCase) Create(ctx context.Context, digest string, casBackendID, workflowRunID string) (*CASMapping, error) {
52+
casBackendUUID, err := uuid.Parse(casBackendID)
53+
if err != nil {
54+
return nil, NewErrInvalidUUID(err)
55+
}
56+
57+
workflowRunUUID, err := uuid.Parse(workflowRunID)
58+
if err != nil {
59+
return nil, NewErrInvalidUUID(err)
60+
}
61+
62+
// parse the digest to make sure is a valid sha256 sum
63+
if _, err = cr_v1.NewHash(digest); err != nil {
64+
return nil, NewErrValidation(fmt.Errorf("invalid digest format: %w", err))
65+
}
66+
67+
return uc.repo.Create(ctx, digest, casBackendUUID, workflowRunUUID)
68+
}
69+
70+
type CASMappingLookupRef struct {
71+
Name, Digest string
72+
}
73+
74+
// LookupCASItemsInAttestation returns a list of references to the materials that have been uploaded to CAS
75+
// as well as the attestation digest itself
76+
func (uc *CASMappingUseCase) LookupDigestsInAttestation(att *dsse.Envelope) ([]*CASMappingLookupRef, error) {
77+
// Calculate the attestation hash
78+
jsonAtt, err := json.Marshal(att)
79+
if err != nil {
80+
return nil, fmt.Errorf("marshaling attestation: %w", err)
81+
}
82+
83+
// Extract the materials that have been uploaded too
84+
predicate, err := chainloop.ExtractPredicate(att)
85+
if err != nil {
86+
return nil, fmt.Errorf("extracting predicate: %w", err)
87+
}
88+
89+
// Calculate the attestation hash
90+
h, _, err := cr_v1.SHA256(bytes.NewBuffer(jsonAtt))
91+
if err != nil {
92+
return nil, fmt.Errorf("calculating attestation hash: %w", err)
93+
}
94+
95+
references := []*CASMappingLookupRef{
96+
{
97+
Name: "attestation",
98+
Digest: h.String(),
99+
},
100+
}
101+
102+
for _, material := range predicate.GetMaterials() {
103+
if material.UploadedToCAS {
104+
references = append(references, &CASMappingLookupRef{
105+
Name: material.Name,
106+
Digest: material.Hash.String(),
107+
})
108+
}
109+
}
110+
111+
return references, nil
112+
}
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
//
2+
// Copyright 2023 The Chainloop Authors.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
package biz_test
17+
18+
import (
19+
"context"
20+
"testing"
21+
22+
"github.com/chainloop-dev/chainloop/app/controlplane/internal/biz"
23+
"github.com/chainloop-dev/chainloop/app/controlplane/internal/biz/testhelpers"
24+
creds "github.com/chainloop-dev/chainloop/internal/credentials/mocks"
25+
"github.com/google/go-cmp/cmp"
26+
"github.com/google/go-cmp/cmp/cmpopts"
27+
"github.com/google/uuid"
28+
"github.com/stretchr/testify/assert"
29+
"github.com/stretchr/testify/mock"
30+
"github.com/stretchr/testify/suite"
31+
)
32+
33+
func (s *casMappingIntegrationSuite) TestCreate() {
34+
validDigest := "sha256:3b0f04c276be095e62f3ac03b9991913c37df1fcd44548e75069adce313aba4d"
35+
invalidDigest := "sha256:deadbeef"
36+
37+
testCases := []struct {
38+
name string
39+
digest string
40+
casBackendID uuid.UUID
41+
workflowRunID uuid.UUID
42+
wantErr bool
43+
}{
44+
{
45+
name: "valid",
46+
digest: validDigest,
47+
casBackendID: s.casBackend.ID,
48+
workflowRunID: s.workflowRun.ID,
49+
},
50+
{
51+
name: "created again with same digest",
52+
digest: validDigest,
53+
casBackendID: s.casBackend.ID,
54+
workflowRunID: s.workflowRun.ID,
55+
},
56+
{
57+
name: "invalid digest format",
58+
digest: invalidDigest,
59+
casBackendID: s.casBackend.ID,
60+
workflowRunID: s.workflowRun.ID,
61+
wantErr: true,
62+
},
63+
{
64+
name: "invalid digest missing prefix",
65+
digest: "3b0f04c276be095e62f3ac03b9991913c37df1fcd44548e75069adce313aba4d",
66+
casBackendID: s.casBackend.ID,
67+
workflowRunID: s.workflowRun.ID,
68+
wantErr: true,
69+
},
70+
{
71+
name: "non-existing CASBackend",
72+
digest: validDigest,
73+
casBackendID: uuid.New(),
74+
workflowRunID: s.workflowRun.ID,
75+
wantErr: true,
76+
},
77+
{
78+
name: "non-existing WorkflowRunID",
79+
digest: validDigest,
80+
casBackendID: s.casBackend.ID,
81+
workflowRunID: uuid.New(),
82+
wantErr: true,
83+
},
84+
}
85+
86+
want := &biz.CASMapping{
87+
Digest: validDigest,
88+
CASBackendID: s.casBackend.ID,
89+
WorkflowRunID: s.workflowRun.ID,
90+
OrgID: s.casBackend.OrganizationID,
91+
}
92+
93+
for _, tc := range testCases {
94+
s.Run(tc.name, func() {
95+
got, err := s.CASMapping.Create(context.TODO(), tc.digest, tc.casBackendID.String(), tc.workflowRunID.String())
96+
if tc.wantErr {
97+
s.Error(err)
98+
} else {
99+
s.NoError(err)
100+
if diff := cmp.Diff(want, got, cmpopts.IgnoreFields(biz.CASMapping{}, "CreatedAt", "ID")); diff != "" {
101+
assert.Failf(s.T(), "mismatch (-want +got):\n%s", diff)
102+
}
103+
}
104+
})
105+
}
106+
}
107+
108+
type casMappingIntegrationSuite struct {
109+
testhelpers.UseCasesEachTestSuite
110+
casBackend *biz.CASBackend
111+
workflowRun *biz.WorkflowRun
112+
}
113+
114+
func (s *casMappingIntegrationSuite) SetupTest() {
115+
var err error
116+
assert := assert.New(s.T())
117+
ctx := context.Background()
118+
119+
// RunDB
120+
credsWriter := creds.NewReaderWriter(s.T())
121+
credsWriter.On(
122+
"SaveCredentials", ctx, mock.Anything, mock.Anything,
123+
).Return("stored-OCI-secret", nil)
124+
125+
s.TestingUseCases = testhelpers.NewTestingUseCases(s.T(), testhelpers.WithCredsReaderWriter(credsWriter))
126+
127+
// Create casBackend in the database
128+
org, err := s.Organization.Create(ctx, "testing org 1 with one backend")
129+
assert.NoError(err)
130+
s.casBackend, err = s.CASBackend.Create(ctx, org.ID, "my-location", "backend 1 description", biz.CASBackendOCI, nil, true)
131+
assert.NoError(err)
132+
133+
// Create workflowRun in the database
134+
// Workflow
135+
workflow, err := s.Workflow.Create(ctx, &biz.CreateOpts{Name: "test workflow", OrgID: org.ID})
136+
assert.NoError(err)
137+
138+
// Robot account
139+
robotAccount, err := s.RobotAccount.Create(ctx, "name", org.ID, workflow.ID.String())
140+
assert.NoError(err)
141+
142+
// Find contract revision
143+
contractVersion, err := s.WorkflowContract.Describe(ctx, org.ID, workflow.ContractID.String(), 0)
144+
assert.NoError(err)
145+
146+
s.workflowRun, err = s.WorkflowRun.Create(ctx, &biz.WorkflowRunCreateOpts{
147+
WorkflowID: workflow.ID.String(), RobotaccountID: robotAccount.ID.String(), ContractRevisionUUID: contractVersion.Version.ID, CASBackendID: s.casBackend.ID,
148+
RunnerType: "runnerType", RunnerRunURL: "runURL",
149+
})
150+
assert.NoError(err)
151+
}
152+
153+
func TestCASMappingIntegration(t *testing.T) {
154+
suite.Run(t, new(casMappingIntegrationSuite))
155+
}

0 commit comments

Comments
 (0)