Skip to content

Commit f3937af

Browse files
committed
fix(controlplane): fallback CAS downloads from invalid backends
Use the organization's current default or fallback backend when a historical CAS mapping points to an invalid backend. Fixes: #3190 Assisted-by: pi Signed-off-by: Vibhav Bobade <vibhav.bobde@gmail.com>
1 parent 4429cd2 commit f3937af

5 files changed

Lines changed: 92 additions & 9 deletions

File tree

app/controlplane/cmd/wire_gen.go

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/controlplane/internal/service/cascredential.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,10 @@ func (s *CASCredentialsService) Get(ctx context.Context, req *pb.CASCredentialsS
130130
}
131131

132132
if mapping != nil {
133-
backend = mapping.CASBackend
133+
backend, err = s.casBackendUC.FindDownloadBackend(ctx, mapping.CASBackend)
134+
if err != nil {
135+
return nil, handleUseCaseErr(err, s.log)
136+
}
134137
} else {
135138
// fallback to default backend if the user or the token is allowed to
136139
if ok, err := s.authzUC.Enforce(ctx, currentAuthzSubject, authz.PolicyDefaultBackendArtifactRead); err != nil {

app/controlplane/internal/service/casredirect.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,11 @@ type CASRedirectService struct {
4949

5050
casMappingUC *biz.CASMappingUseCase
5151
casCredsUseCase *biz.CASCredentialsUseCase
52+
casBackendUC *biz.CASBackendUseCase
5253
casServerConf *conf.Bootstrap_CASServer
5354
}
5455

55-
func NewCASRedirectService(casmUC *biz.CASMappingUseCase, casCredsUC *biz.CASCredentialsUseCase, conf *conf.Bootstrap_CASServer, opts ...NewOpt) (*CASRedirectService, error) {
56+
func NewCASRedirectService(casmUC *biz.CASMappingUseCase, casCredsUC *biz.CASCredentialsUseCase, casBackendUC *biz.CASBackendUseCase, conf *conf.Bootstrap_CASServer, opts ...NewOpt) (*CASRedirectService, error) {
5657
if conf == nil || conf.GetDownloadUrl() == "" {
5758
return nil, errors.New("CASServer.downloadURL configuration is missing")
5859
}
@@ -61,6 +62,7 @@ func NewCASRedirectService(casmUC *biz.CASMappingUseCase, casCredsUC *biz.CASCre
6162
service: newService(opts...),
6263
casMappingUC: casmUC,
6364
casCredsUseCase: casCredsUC,
65+
casBackendUC: casBackendUC,
6466
casServerConf: conf,
6567
}, nil
6668
}
@@ -103,18 +105,16 @@ func (s *CASRedirectService) GetDownloadURL(ctx context.Context, req *pb.GetDown
103105
return nil, handleUseCaseErr(err, s.log)
104106
}
105107

106-
backend := mapping.CASBackend
108+
backend, err := s.casBackendUC.FindDownloadBackend(ctx, mapping.CASBackend)
109+
if err != nil {
110+
return nil, handleUseCaseErr(err, s.log)
111+
}
107112

108113
// inline backends don't have a download URL
109114
if backend.Inline {
110115
return nil, kerrors.NotFound("not found", "CAS backend is inline")
111116
}
112117

113-
// check if the backend is on a valid state, if not return an error
114-
if backend.ValidationStatus != biz.CASBackendValidationOK {
115-
return nil, pb.ErrorCasBackendErrorReasonInvalid("CAS Storage is in an invalid state and can't download artifacts, please fix it before attempting it again")
116-
}
117-
118118
// Create an URL to download the artifact from the CAS backend
119119
downloadBase, err := url.Parse(s.casServerConf.GetDownloadUrl())
120120
if err != nil {

app/controlplane/pkg/biz/casbackend.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,30 @@ func (uc *CASBackendUseCase) FindDefaultOrFallbackBackend(ctx context.Context, o
350350
return fallbackBackend, nil
351351
}
352352

353+
// FindDownloadBackend returns the backend that should serve a mapped artifact download.
354+
//
355+
// CAS mappings are historical: digest -> backend ID. If credentials are rotated by
356+
// creating a new backend for the same storage location and making it the org default,
357+
// old mappings still point at the backend with revoked credentials.
358+
//
359+
// For that case, try the org's current default backend first, then the configured
360+
// fallback. This is safe because CAS downloads are verified by digest: if the chosen
361+
// backend does not contain the exact content, the download fails.
362+
//
363+
// The stored mapping is not changed; this only chooses credentials for this read.
364+
func (uc *CASBackendUseCase) FindDownloadBackend(ctx context.Context, mapped *CASBackend) (*CASBackend, error) {
365+
if mapped == nil {
366+
return nil, NewErrNotFound("CAS Backend")
367+
}
368+
369+
if mapped.ValidationStatus == CASBackendValidationOK {
370+
return mapped, nil
371+
}
372+
373+
uc.logger.Infow("msg", "mapped CAS backend validation failed, attempting default/fallback", "backend", mapped.Name, "status", mapped.ValidationStatus, "orgID", mapped.OrganizationID)
374+
return uc.FindDefaultOrFallbackBackend(ctx, mapped.OrganizationID.String())
375+
}
376+
353377
func (uc *CASBackendUseCase) CreateInlineBackend(ctx context.Context, orgID string) (*CASBackend, error) {
354378
ctx, span := otelx.Start(ctx, casBackendTracer, "CASBackendUseCase.CreateInlineBackend")
355379
defer span.End()

app/controlplane/pkg/biz/casbackend_test.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,62 @@ func (s *casBackendTestSuite) TestFindDefaultBackendFound() {
7272
assert.Equal(backend, wantBackend)
7373
}
7474

75+
func (s *casBackendTestSuite) TestFindDownloadBackend() {
76+
ctx := context.Background()
77+
mapped := &biz.CASBackend{ID: uuid.New(), OrganizationID: s.validUUID, ValidationStatus: biz.CASBackendValidationOK}
78+
79+
got, err := s.useCase.FindDownloadBackend(ctx, mapped)
80+
s.Require().NoError(err)
81+
s.Same(mapped, got)
82+
83+
s.resetMock()
84+
mapped.ValidationStatus = biz.CASBackendValidationFailed
85+
defaultBackend := &biz.CASBackend{ID: uuid.New(), ValidationStatus: biz.CASBackendValidationOK}
86+
s.repo.On("FindDefaultBackend", mock.Anything, s.validUUID).Return(defaultBackend, nil).Once()
87+
88+
got, err = s.useCase.FindDownloadBackend(ctx, mapped)
89+
s.Require().NoError(err)
90+
s.Same(defaultBackend, got)
91+
92+
s.resetMock()
93+
defaultBackend.ValidationStatus = biz.CASBackendValidationFailed
94+
fallbackBackend := &biz.CASBackend{ID: uuid.New(), ValidationStatus: biz.CASBackendValidationOK}
95+
s.repo.On("FindDefaultBackend", mock.Anything, s.validUUID).Return(defaultBackend, nil).Once()
96+
s.repo.On("FindFallbackBackend", mock.Anything, s.validUUID).Return(fallbackBackend, nil).Once()
97+
98+
got, err = s.useCase.FindDownloadBackend(ctx, mapped)
99+
s.Require().NoError(err)
100+
s.Same(fallbackBackend, got)
101+
102+
got, err = s.useCase.FindDownloadBackend(ctx, nil)
103+
s.Nil(got)
104+
s.True(biz.IsNotFound(err))
105+
106+
s.resetMock()
107+
s.repo.On("FindDefaultBackend", mock.Anything, s.validUUID).Return(nil, nil).Once()
108+
109+
got, err = s.useCase.FindDownloadBackend(ctx, mapped)
110+
s.Nil(got)
111+
s.True(biz.IsNotFound(err))
112+
113+
s.resetMock()
114+
s.repo.On("FindDefaultBackend", mock.Anything, s.validUUID).Return(defaultBackend, nil).Once()
115+
s.repo.On("FindFallbackBackend", mock.Anything, s.validUUID).Return(nil, nil).Once()
116+
117+
got, err = s.useCase.FindDownloadBackend(ctx, mapped)
118+
s.Nil(got)
119+
s.True(biz.IsNotFound(err))
120+
121+
s.resetMock()
122+
fallbackBackend.ValidationStatus = biz.CASBackendValidationFailed
123+
s.repo.On("FindDefaultBackend", mock.Anything, s.validUUID).Return(defaultBackend, nil).Once()
124+
s.repo.On("FindFallbackBackend", mock.Anything, s.validUUID).Return(fallbackBackend, nil).Once()
125+
126+
got, err = s.useCase.FindDownloadBackend(ctx, mapped)
127+
s.Nil(got)
128+
s.True(biz.IsErrValidation(err))
129+
}
130+
75131
func (s *casBackendTestSuite) TestSaveInvalidUUID() {
76132
repo, err := s.useCase.CreateOrUpdate(context.Background(), s.invalidUUID, "", "", "", backendType, true)
77133
assert.True(s.T(), biz.IsErrInvalidUUID(err))

0 commit comments

Comments
 (0)