Skip to content

Commit d512363

Browse files
authored
feat(controlplane): download attestation with cas (#46)
Signed-off-by: Miguel Martinez Trivino <[email protected]>
1 parent 4774567 commit d512363

File tree

7 files changed

+160
-98
lines changed

7 files changed

+160
-98
lines changed

app/controlplane/cmd/wire.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ func wireApp(*conf.Bootstrap, credentials.ReaderWriter, log.Logger) (*app, func(
4242
biz.ProviderSet,
4343
service.ProviderSet,
4444
wire.Bind(new(backend.Provider), new(*oci.BackendProvider)),
45-
wire.Bind(new(biz.CASUploader), new(*biz.CASClientUseCase)),
45+
wire.Bind(new(biz.CASClient), new(*biz.CASClientUseCase)),
4646
oci.NewBackendProvider,
4747
serviceOpts,
4848
wire.FieldsOf(new(*conf.Bootstrap), "Server", "Auth", "Data", "CasServer"),

app/controlplane/internal/biz/attestation.go

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ type Attestation struct {
3737

3838
type AttestationUseCase struct {
3939
logger *log.Helper
40-
CASUploader
40+
CASClient
4141

4242
// DEPRECATED
4343
// We will remove it once we force all the clients to use the CAS instead
@@ -51,24 +51,39 @@ type AttestationRef struct {
5151
SecretRef string
5252
}
5353

54-
func NewAttestationUseCase(uploader CASUploader, p backend.Provider, logger log.Logger) *AttestationUseCase {
54+
func NewAttestationUseCase(client CASClient, p backend.Provider, logger log.Logger) *AttestationUseCase {
5555
if logger == nil {
5656
logger = log.NewStdLogger(io.Discard)
5757
}
5858

5959
return &AttestationUseCase{
6060
logger: servicelogger.ScopedHelper(logger, "biz/attestation"),
61-
CASUploader: uploader,
61+
CASClient: client,
6262
backendProvider: p,
6363
}
6464
}
6565

66-
func (uc *AttestationUseCase) FetchFromStore(ctx context.Context, downloader backend.Downloader, digest string) (*Attestation, error) {
66+
func (uc *AttestationUseCase) FetchFromStore(ctx context.Context, secretID, digest string) (*Attestation, error) {
6767
uc.logger.Infow("msg", "downloading attestation", "digest", digest)
6868
buf := bytes.NewBuffer(nil)
6969

70-
if err := downloader.Download(ctx, buf, digest); err != nil {
71-
return nil, err
70+
if uc.CASClient.Configured() {
71+
if err := uc.CASClient.Download(ctx, secretID, buf, digest); err != nil {
72+
return nil, fmt.Errorf("downloading from CAS: %w", err)
73+
}
74+
} else {
75+
uc.logger.Warnw("msg", "no CAS configured, falling back to old mechanism")
76+
77+
// DEPRECATED
78+
// TODO: remove
79+
downloader, err := uc.backendProvider.FromCredentials(ctx, secretID)
80+
if err != nil {
81+
return nil, err
82+
}
83+
84+
if err := downloader.Download(ctx, buf, digest); err != nil {
85+
return nil, err
86+
}
7287
}
7388

7489
var envelope dsse.Envelope
@@ -90,8 +105,8 @@ func (uc *AttestationUseCase) UploadToCAS(ctx context.Context, envelope *dsse.En
90105
hash.Write(jsonContent)
91106
digest := fmt.Sprintf("%x", hash.Sum(nil))
92107

93-
if uc.CASUploader.Configured() {
94-
if err := uc.CASUploader.Upload(ctx, secretID, bytes.NewBuffer(jsonContent), filename, digest); err != nil {
108+
if uc.CASClient.Configured() {
109+
if err := uc.CASClient.Upload(ctx, secretID, bytes.NewBuffer(jsonContent), filename, digest); err != nil {
95110
return "", fmt.Errorf("uploading to CAS: %w", err)
96111
}
97112

app/controlplane/internal/biz/attestation_test.go

Lines changed: 36 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -34,44 +34,40 @@ import (
3434
"github.com/stretchr/testify/suite"
3535
)
3636

37+
var runID = uuid.NewString()
38+
var envelope = &dsse.Envelope{}
39+
40+
const expectedDigest = "f845058d865c3d4d491c9019f6afe9c543ad2cd11b31620cc512e341fb03d3d8"
41+
3742
// Deprecated method
3843
func (s *attestationTestSuite) TestUploadToCASFallbackOCI() {
39-
runID := uuid.NewString()
40-
envelope := &dsse.Envelope{}
41-
const expectedDigest = "f845058d865c3d4d491c9019f6afe9c543ad2cd11b31620cc512e341fb03d3d8"
42-
4344
ctx := context.Background()
4445
s.uploader.On("Upload", ctx, mock.Anything, &casAPI.CASResource{
4546
FileName: fmt.Sprintf("attestation-%s.json", runID), Digest: expectedDigest,
4647
}).Return(nil)
4748

48-
s.casUploader.On("Configured").Return(false)
49+
s.casClient.On("Configured").Return(false)
4950

5051
gotDigest, err := s.uc.UploadToCAS(ctx, envelope, "my-secret", runID)
5152
assert.NoError(s.T(), err)
5253
assert.Equal(s.T(), expectedDigest, gotDigest)
5354
}
5455

5556
func (s *attestationTestSuite) TestUploadToCAS() {
56-
runID := uuid.NewString()
57-
envelope := &dsse.Envelope{}
58-
const expectedDigest = "f845058d865c3d4d491c9019f6afe9c543ad2cd11b31620cc512e341fb03d3d8"
59-
6057
ctx := context.Background()
61-
s.casUploader.On(
58+
s.casClient.On(
6259
"Upload", ctx, "my-secret", mock.Anything,
6360
fmt.Sprintf("attestation-%s.json", runID), expectedDigest,
6461
).Return(nil)
6562

66-
s.casUploader.On("Configured").Return(true)
63+
s.casClient.On("Configured").Return(true)
6764

6865
gotDigest, err := s.uc.UploadToCAS(ctx, envelope, "my-secret", runID)
6966
assert.NoError(s.T(), err)
7067
assert.Equal(s.T(), expectedDigest, gotDigest)
7168
}
7269

73-
func (s *attestationTestSuite) TestFetchFromStore() {
74-
const expectedDigest = "f845058d865c3d4d491c9019f6afe9c543ad2cd11b31620cc512e341fb03d3d8"
70+
func (s *attestationTestSuite) TestFetchFromStoreFallbackOCI() {
7571
want := &biz.Attestation{Envelope: &dsse.Envelope{}}
7672

7773
ctx := context.Background()
@@ -82,7 +78,27 @@ func (s *attestationTestSuite) TestFetchFromStore() {
8278
require.NoError(s.T(), err)
8379
})
8480

85-
got, err := s.uc.FetchFromStore(ctx, s.downloader, expectedDigest)
81+
s.casClient.On("Configured").Return(false)
82+
83+
got, err := s.uc.FetchFromStore(ctx, "my-secret", expectedDigest)
84+
assert.NoError(s.T(), err)
85+
assert.Equal(s.T(), want, got)
86+
}
87+
88+
func (s *attestationTestSuite) TestFetchFromStore() {
89+
want := &biz.Attestation{Envelope: &dsse.Envelope{}}
90+
91+
ctx := context.Background()
92+
s.casClient.On("Download", ctx, "my-secret", mock.Anything, expectedDigest).Return(nil).Run(
93+
func(args mock.Arguments) {
94+
buf := args.Get(2).(io.Writer)
95+
err := json.NewEncoder(buf).Encode(want)
96+
require.NoError(s.T(), err)
97+
})
98+
99+
s.casClient.On("Configured").Return(true)
100+
101+
got, err := s.uc.FetchFromStore(ctx, "my-secret", expectedDigest)
86102
assert.NoError(s.T(), err)
87103
assert.Equal(s.T(), want, got)
88104
}
@@ -96,18 +112,18 @@ func (s *attestationTestSuite) SetupTest() {
96112
ociBackend := blobmock.NewUploaderDownloader(s.T())
97113
backendProvider.On("FromCredentials", mock.Anything, "my-secret").Maybe().Return(ociBackend, nil)
98114

99-
s.casUploader = mocks.NewCASUploader(s.T())
100-
s.uc = biz.NewAttestationUseCase(s.casUploader, backendProvider, nil)
115+
s.casClient = mocks.NewCASClient(s.T())
116+
s.uc = biz.NewAttestationUseCase(s.casClient, backendProvider, nil)
101117
s.uploader = (*blobmock.Uploader)(ociBackend)
102-
s.downloader = blobmock.NewDownloader(s.T())
118+
s.downloader = (*blobmock.Downloader)(ociBackend)
103119
}
104120

105121
// Utility struct to hold the test suite
106122
type attestationTestSuite struct {
107123
suite.Suite
108124
uc *biz.AttestationUseCase
109125
// Deprecated: attestation should use the casclient instead of the blobmanager
110-
uploader *blobmock.Uploader
111-
downloader *blobmock.Downloader
112-
casUploader *mocks.CASUploader
126+
uploader *blobmock.Uploader
127+
downloader *blobmock.Downloader
128+
casClient *mocks.CASClient
113129
}

app/controlplane/internal/biz/casclient.go

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,20 @@ type CASClientUseCase struct {
3434
logger *log.Helper
3535
}
3636

37-
type CASDescriber interface {
38-
Configured() bool
39-
}
4037
type CASUploader interface {
41-
CASDescriber
4238
Upload(ctx context.Context, secretID string, content io.Reader, filename, digest string) error
4339
}
4440

41+
type CASDownloader interface {
42+
Download(ctx context.Context, secretID string, w io.Writer, digest string) error
43+
}
44+
45+
type CASClient interface {
46+
CASUploader
47+
CASDownloader
48+
Configured() bool
49+
}
50+
4551
func NewCASClientUseCase(credsProvider *CASCredentialsUseCase, config *conf.Bootstrap_CASServer, l log.Logger) *CASClientUseCase {
4652
return &CASClientUseCase{credsProvider, config, servicelogger.ScopedHelper(l, "biz/cas-client")}
4753
}
@@ -66,6 +72,23 @@ func (uc *CASClientUseCase) Upload(ctx context.Context, secretID string, content
6672
return nil
6773
}
6874

75+
func (uc *CASClientUseCase) Download(ctx context.Context, secretID string, w io.Writer, digest string) error {
76+
uc.logger.Infow("msg", "download initialized", "digest", digest)
77+
78+
client, err := uc.casAPIClient(secretID, casJWT.Downloader)
79+
if err != nil {
80+
return fmt.Errorf("failed to create cas client: %w", err)
81+
}
82+
83+
if err := client.Download(ctx, w, digest); err != nil {
84+
return fmt.Errorf("failed to download content: %w", err)
85+
}
86+
87+
uc.logger.Infow("msg", "download finalized", "digest", digest)
88+
89+
return nil
90+
}
91+
6992
// create a client with a temporary set of credentials for a specific operation
7093
func (uc *CASClientUseCase) casAPIClient(secretID string, role casJWT.Role) (*casclient.Client, error) {
7194
token, err := uc.credsProvider.GenerateTemporaryCredentials(secretID, role)

app/controlplane/internal/biz/mocks/CASClient.go

Lines changed: 72 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/mocks/CASUploader.go

Lines changed: 0 additions & 58 deletions
This file was deleted.

app/controlplane/internal/service/workflowrun.go

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import (
2222
craftingpb "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1"
2323
"github.com/chainloop-dev/chainloop/app/controlplane/internal/biz"
2424
"github.com/chainloop-dev/chainloop/app/controlplane/internal/pagination"
25-
"github.com/chainloop-dev/chainloop/internal/blobmanager/oci"
2625
"github.com/chainloop-dev/chainloop/internal/credentials"
2726
sl "github.com/chainloop-dev/chainloop/internal/servicelogger"
2827
errors "github.com/go-kratos/kratos/v2/errors"
@@ -116,12 +115,7 @@ func (s *WorkflowRunService) View(ctx context.Context, req *pb.WorkflowRunServic
116115
var attestation *biz.Attestation
117116
// Download the attestation if the workflow run is successful
118117
if run.AttestationRef != nil {
119-
downloader, err := oci.NewBackendProvider(s.credsReader).FromCredentials(ctx, run.AttestationRef.SecretRef)
120-
if err != nil {
121-
return nil, sl.LogAndMaskErr(err, s.log)
122-
}
123-
124-
attestation, err = s.attestationUseCase.FetchFromStore(ctx, downloader, run.AttestationRef.Sha256)
118+
attestation, err = s.attestationUseCase.FetchFromStore(ctx, run.AttestationRef.SecretRef, run.AttestationRef.Sha256)
125119
if err != nil {
126120
// NOTE: For now we don't return an error if the attestation is not found
127121
// since we do not have a good error recovery in place for assets

0 commit comments

Comments
 (0)