Skip to content

Commit 140a032

Browse files
fix(artifact-cas): return ResourceExhausted for oversized uploads
Signed-off-by: Matías Insaurralde <matias@chainloop.dev>
1 parent cccdf3e commit 140a032

3 files changed

Lines changed: 57 additions & 8 deletions

File tree

app/artifact-cas/internal/service/bytestream.go

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// Copyright 2024 The Chainloop Authors.
2+
// Copyright 2024-2026 The Chainloop Authors.
33
//
44
// Licensed under the Apache License, Version 2.0 (the "License");
55
// you may not use this file except in compliance with the License.
@@ -75,15 +75,15 @@ func (s *ByteStreamService) Write(stream bytestream.ByteStream_WriteServer) erro
7575
return kerrors.BadRequest("resource name", err.Error())
7676
}
7777

78-
backend, err := s.loadBackend(ctx, info.BackendType, info.StoredSecretID)
78+
storageBackend, err := s.loadBackend(ctx, info.BackendType, info.StoredSecretID)
7979
if err != nil && kerrors.IsNotFound(err) {
8080
return err
8181
} else if err != nil {
8282
return sl.LogAndMaskErr(err, s.log)
8383
}
8484

8585
// We check if the file already exists even before we wait for the whole buffer to be filled
86-
if exists, err := backend.Exists(ctx, req.resource.Digest); err != nil {
86+
if exists, err := storageBackend.Exists(ctx, req.resource.Digest); err != nil {
8787
return sl.LogAndMaskErr(err, s.log)
8888
} else if exists {
8989
s.log.Infow("msg", "artifact already exists", "digest", req.resource.Digest)
@@ -95,6 +95,9 @@ func (s *ByteStreamService) Write(stream bytestream.ByteStream_WriteServer) erro
9595
buffer := newStreamReader(info.MaxBytes)
9696
// Add data from first request
9797
if err = buffer.Write(req.GetData()); err != nil {
98+
if backend.IsUploadSizeExceeded(err) {
99+
return status.Error(codes.ResourceExhausted, err.Error())
100+
}
98101
return sl.LogAndMaskErr(err, s.log)
99102
}
100103

@@ -115,11 +118,14 @@ func (s *ByteStreamService) Write(stream bytestream.ByteStream_WriteServer) erro
115118
s.log.Infow("msg", "upload canceled", "digest", req.resource.Digest, "name", req.resource.FileName)
116119
return nil
117120
}
121+
if backend.IsUploadSizeExceeded(err) {
122+
return status.Error(codes.ResourceExhausted, err.Error())
123+
}
118124
return sl.LogAndMaskErr(err, s.log)
119125
}
120126

121127
s.log.Infow("msg", "artifact received, uploading now to backend", "name", req.resource.FileName, "digest", req.resource.Digest, "size", buffer.size)
122-
if err := backend.Upload(ctx, buffer, req.resource); err != nil {
128+
if err := storageBackend.Upload(ctx, buffer, req.resource); err != nil {
123129
return sl.LogAndMaskErr(err, s.log)
124130
}
125131

@@ -245,7 +251,7 @@ func (r *streamReader) Write(data []byte) error {
245251
// Check if the size of the buffer has exceeded the maximum allowed size
246252
// if maxSize is 0, then there is no limit
247253
if r.maxSize != 0 && r.size > r.maxSize {
248-
return fmt.Errorf("max size of upload exceeded: want=%d, max=%d", r.size, r.maxSize)
254+
return backend.NewErrUploadSizeExceeded(r.size, r.maxSize)
249255
}
250256

251257
_, err := r.Buffer.Write(data)

app/artifact-cas/internal/service/bytestream_test.go

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// Copyright 2024 The Chainloop Authors.
2+
// Copyright 2024-2026 The Chainloop Authors.
33
//
44
// Licensed under the Apache License, Version 2.0 (the "License");
55
// you may not use this file except in compliance with the License.
@@ -23,6 +23,7 @@ import (
2323
"errors"
2424
"io"
2525
"net"
26+
"strconv"
2627
"testing"
2728

2829
v1 "github.com/chainloop-dev/chainloop/app/artifact-cas/api/cas/v1"
@@ -77,6 +78,7 @@ func (s *bytestreamSuite) TestStreamReaderOverflow() {
7778
s.Equal(int64(5), buffer.size)
7879
err = buffer.Write([]byte("chainloop"))
7980
s.Error(err)
81+
s.True(backend.IsUploadSizeExceeded(err))
8082
s.ErrorContains(err, "max size of upload exceeded")
8183
}
8284

@@ -119,6 +121,25 @@ func (s *bytestreamSuite) TestWrite() {
119121
_, err = stream.CloseAndRecv()
120122
assertGRPCError(t, err, codes.InvalidArgument, "resourceName must be set")
121123
})
124+
125+
s.T().Run("max size exceeded", func(t *testing.T) {
126+
ctx := metadata.NewOutgoingContext(context.Background(), metadata.Pairs("role", "uploader", "max-bytes", "8"))
127+
s.ociBackend.On("Exists", mock.Anything, s.resource.Digest).Return(false, nil)
128+
129+
stream, err := s.client.Write(ctx)
130+
s.NoError(err)
131+
s.NoError(stream.Send(&bytestream.WriteRequest{
132+
ResourceName: encodeResource(s.T(), s.resource),
133+
Data: []byte("hello"),
134+
}))
135+
s.NoError(stream.Send(&bytestream.WriteRequest{
136+
ResourceName: encodeResource(s.T(), s.resource),
137+
Data: []byte("chainloop"),
138+
}))
139+
140+
_, err = stream.CloseAndRecv()
141+
assertGRPCError(t, err, codes.ResourceExhausted, "max size of upload exceeded")
142+
})
122143
}
123144

124145
// NOTE: separated test cases for each error case to make sure the context and stubs are re-set
@@ -171,7 +192,7 @@ func (s *bytestreamSuite) TestWriteErrorUploading() {
171192
}))
172193

173194
_, err = stream.CloseAndRecv()
174-
s.Error(err)
195+
assertGRPCError(s.T(), err, codes.Internal, "")
175196
}
176197

177198
func (s *bytestreamSuite) TestRead() {
@@ -298,6 +319,11 @@ func (s *bytestreamSuite) SetupTest() {
298319
claims := &casJWT.Claims{
299320
StoredSecretID: "secret-id", BackendType: backendType,
300321
}
322+
if maxBytes := md.Get("max-bytes"); len(maxBytes) > 0 {
323+
parsedMaxBytes, err := strconv.ParseInt(maxBytes[0], 10, 64)
324+
require.NoError(s.T(), err)
325+
claims.MaxBytes = parsedMaxBytes
326+
}
301327

302328
if roles := md.Get("role"); len(roles) > 0 {
303329
if roles[0] == "downloader" {

pkg/blobmanager/errors.go

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// Copyright 2023 The Chainloop Authors.
2+
// Copyright 2023-2026 The Chainloop Authors.
33
//
44
// Licensed under the Apache License, Version 2.0 (the "License");
55
// you may not use this file except in compliance with the License.
@@ -35,3 +35,20 @@ func (e ErrNotFound) Error() string {
3535
func IsNotFound(err error) bool {
3636
return errors.As(err, &ErrNotFound{})
3737
}
38+
39+
type ErrUploadSizeExceeded struct {
40+
want int64
41+
max int64
42+
}
43+
44+
func NewErrUploadSizeExceeded(want, maxSize int64) ErrUploadSizeExceeded {
45+
return ErrUploadSizeExceeded{want: want, max: maxSize}
46+
}
47+
48+
func (e ErrUploadSizeExceeded) Error() string {
49+
return fmt.Sprintf("max size of upload exceeded: want=%d, max=%d", e.want, e.max)
50+
}
51+
52+
func IsUploadSizeExceeded(err error) bool {
53+
return errors.As(err, &ErrUploadSizeExceeded{})
54+
}

0 commit comments

Comments
 (0)