Skip to content

Commit fffbbbe

Browse files
authored
feat(cas-backend): store and expose validation error (#2412)
Signed-off-by: Miguel Martinez <[email protected]>
1 parent 665d3c0 commit fffbbbe

27 files changed

+582
-140
lines changed

app/cli/cmd/casbackend_list.go

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// Copyright 2024 The Chainloop Authors.
2+
// Copyright 2024-2025 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.
@@ -17,6 +17,7 @@ package cmd
1717

1818
import (
1919
"fmt"
20+
"strings"
2021
"time"
2122

2223
"code.cloudfoundry.org/bytefmt"
@@ -56,9 +57,9 @@ func casBackendListTableOutput(backends []*action.CASBackendItem) error {
5657
}
5758

5859
t := newTableWriter()
59-
header := table.Row{"Name", "Location", "Provider", "Description", "Limits", "Default"}
60+
header := table.Row{"Name", "Location", "Provider", "Description", "Limits", "Default", "Status"}
6061
if full {
61-
header = append(header, "Validation Status", "Created At", "Validated At")
62+
header = append(header, "Created At", "Validated At")
6263
}
6364

6465
t.AppendHeader(header)
@@ -68,12 +69,14 @@ func casBackendListTableOutput(backends []*action.CASBackendItem) error {
6869
limits = fmt.Sprintf("MaxSize: %s", bytefmt.ByteSize(uint64(b.Limits.MaxBytes)))
6970
}
7071

71-
r := table.Row{b.Name, wrap.String(b.Location, 35), b.Provider, wrap.String(b.Description, 35), limits, b.Default}
72+
validationStatus := string(b.ValidationStatus)
73+
if b.ValidationError != nil && *b.ValidationError != "" {
74+
validationStatus = strings.Join([]string{validationStatus, wrap.String(*b.ValidationError, 50)}, "\n")
75+
}
76+
77+
r := table.Row{b.Name, wrap.String(b.Location, 35), b.Provider, wrap.String(b.Description, 35), limits, b.Default, validationStatus}
7278
if full {
73-
r = append(r, b.ValidationStatus,
74-
b.CreatedAt.Format(time.RFC822),
75-
b.ValidatedAt.Format(time.RFC822),
76-
)
79+
r = append(r, b.CreatedAt.Format(time.RFC822), b.ValidatedAt.Format(time.RFC822))
7780
}
7881

7982
t.AppendRow(r)

app/cli/internal/action/casbackend_list.go

Lines changed: 6 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-2025 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.
@@ -36,6 +36,7 @@ type CASBackendItem struct {
3636
Inline bool `json:"inline"`
3737
Limits *CASBackendLimits `json:"limits"`
3838
ValidationStatus ValidationStatus `json:"validationStatus"`
39+
ValidationError *string `json:"validationError,omitempty"`
3940

4041
CreatedAt *time.Time `json:"createdAt"`
4142
ValidatedAt *time.Time `json:"validatedAt"`
@@ -102,5 +103,9 @@ func pbCASBackendItemToAction(in *pb.CASBackendItem) *CASBackendItem {
102103
b.ValidationStatus = Invalid
103104
}
104105

106+
if in.ValidationError != nil {
107+
b.ValidationError = in.ValidationError
108+
}
109+
105110
return b
106111
}

app/controlplane/api/controlplane/v1/response_messages.pb.go

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

app/controlplane/api/controlplane/v1/response_messages.proto

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,8 @@ message CASBackendItem {
292292
// Is it an inline backend?
293293
// inline means that the content is stored in the attestation itself
294294
bool is_inline = 10;
295+
// Error message if validation failed
296+
optional string validation_error = 12;
295297

296298
message Limits {
297299
// Max number of bytes allowed to be stored in this backend

app/controlplane/api/gen/frontend/controlplane/v1/response_messages.ts

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

app/controlplane/api/gen/jsonschema/controlplane.v1.CASBackendItem.jsonschema.json

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

app/controlplane/api/gen/jsonschema/controlplane.v1.CASBackendItem.schema.json

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

app/controlplane/internal/service/casbackend.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,5 +176,9 @@ func bizCASBackendToPb(in *biz.CASBackend) *pb.CASBackendItem {
176176
r.ValidationStatus = pb.CASBackendItem_VALIDATION_STATUS_INVALID
177177
}
178178

179+
if in.ValidationError != nil {
180+
r.ValidationError = in.ValidationError
181+
}
182+
179183
return r
180184
}

app/controlplane/pkg/auditor/events/casbackend.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@ type CASBackendStatusChanged struct {
183183
*CASBackendBase
184184
PreviousStatus string `json:"previous_status,omitempty"`
185185
NewStatus string `json:"new_status,omitempty"`
186+
StatusError string `json:"status_error,omitempty"`
186187
IsRecovery bool `json:"is_recovery,omitempty"`
187188
}
188189

app/controlplane/pkg/biz/casbackend.go

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ type CASBackend struct {
5555
CreatedAt, ValidatedAt *time.Time
5656
OrganizationID uuid.UUID
5757
ValidationStatus CASBackendValidationStatus
58+
ValidationError *string
5859
// OCI, S3, ...
5960
Provider CASBackendProvider
6061
// Whether this is the default cas backend for the organization
@@ -77,6 +78,8 @@ type CASBackendOpts struct {
7778
Location, SecretName, Description string
7879
Provider CASBackendProvider
7980
Default bool
81+
ValidationStatus CASBackendValidationStatus
82+
ValidationError *string
8083
}
8184

8285
type CASBackendCreateOpts struct {
@@ -98,10 +101,10 @@ type CASBackendRepo interface {
98101
FindByIDInOrg(ctx context.Context, OrgID, ID uuid.UUID) (*CASBackend, error)
99102
FindByNameInOrg(ctx context.Context, OrgID uuid.UUID, name string) (*CASBackend, error)
100103
List(ctx context.Context, orgID uuid.UUID) ([]*CASBackend, error)
104+
UpdateValidationStatus(ctx context.Context, ID uuid.UUID, status CASBackendValidationStatus, validationError *string) error
101105
// ListBackends returns CAS backends across all organizations
102106
// If onlyDefaults is true, only default backends are returned
103107
ListBackends(ctx context.Context, onlyDefaults bool) ([]*CASBackend, error)
104-
UpdateValidationStatus(ctx context.Context, ID uuid.UUID, status CASBackendValidationStatus) error
105108
Create(context.Context, *CASBackendCreateOpts) (*CASBackend, error)
106109
Update(context.Context, *CASBackendUpdateOpts) (*CASBackend, error)
107110
Delete(ctx context.Context, ID uuid.UUID) error
@@ -345,6 +348,8 @@ func (uc *CASBackendUseCase) Update(ctx context.Context, orgID, id, description
345348
ID: uuid,
346349
CASBackendOpts: &CASBackendOpts{
347350
SecretName: secretName, Default: defaultB, Description: description, OrgID: orgUUID,
351+
ValidationStatus: CASBackendValidationOK,
352+
ValidationError: ToPtr(""),
348353
},
349354
})
350355
if err != nil {
@@ -530,6 +535,7 @@ func (CASBackendValidationStatus) Values() (kinds []string) {
530535
// Validate that the repository is valid and reachable
531536
func (uc *CASBackendUseCase) PerformValidation(ctx context.Context, id string) (err error) {
532537
validationStatus := CASBackendValidationFailed
538+
var validationError *string
533539

534540
backendUUID, err := uuid.Parse(id)
535541
if err != nil {
@@ -559,16 +565,16 @@ func (uc *CASBackendUseCase) PerformValidation(ctx context.Context, id string) (
559565
return
560566
}
561567

562-
// Store previous status for audit logging
563-
previousStatus := backend.ValidationStatus
564-
565-
// Update the validation status
566-
uc.logger.Infow("msg", "updating validation status", "ID", id, "status", validationStatus)
567-
if err := uc.repo.UpdateValidationStatus(ctx, backendUUID, validationStatus); err != nil {
568+
// Update the validation status and error
569+
uc.logger.Infow("msg", "updating validation status", "ID", id, "status", validationStatus, "error", validationError)
570+
if err := uc.repo.UpdateValidationStatus(ctx, backendUUID, validationStatus, validationError); err != nil {
568571
uc.logger.Errorw("msg", "updating validation status", "ID", id, "error", err)
569572
return
570573
}
571574

575+
// Store previous status for audit logging
576+
previousStatus := backend.ValidationStatus
577+
572578
// Log status change as an audit event if status has changed and auditor is available
573579
if uc.auditorUC != nil && previousStatus != validationStatus {
574580
uc.logger.Debugw("msg", "status changed, dispatching audit event",
@@ -579,6 +585,11 @@ func (uc *CASBackendUseCase) PerformValidation(ctx context.Context, id string) (
579585
// Check if this is a recovery event (going from failed to OK)
580586
isRecovery := previousStatus == CASBackendValidationFailed && validationStatus == CASBackendValidationOK
581587

588+
var validationErrorStr string
589+
if validationError != nil {
590+
validationErrorStr = *validationError
591+
}
592+
582593
// Create and send event for the status change
583594
uc.auditorUC.Dispatch(ctx, &events.CASBackendStatusChanged{
584595
CASBackendBase: &events.CASBackendBase{
@@ -590,6 +601,7 @@ func (uc *CASBackendUseCase) PerformValidation(ctx context.Context, id string) (
590601
},
591602
PreviousStatus: string(previousStatus),
592603
NewStatus: string(validationStatus),
604+
StatusError: validationErrorStr,
593605
IsRecovery: isRecovery,
594606
}, &backend.OrganizationID)
595607
}
@@ -598,25 +610,28 @@ func (uc *CASBackendUseCase) PerformValidation(ctx context.Context, id string) (
598610
// 1 - Retrieve the credentials from the external secrets manager
599611
var creds any
600612
if err := uc.credsRW.ReadCredentials(ctx, backend.SecretName, &creds); err != nil {
601-
uc.logger.Infow("msg", "credentials not found or invalid", "ID", id)
613+
uc.logger.Infow("msg", "credentials not found or invalid", "ID", id, "error", err)
602614
return nil
603615
}
604616

605617
credsJSON, err := json.Marshal(creds)
606618
if err != nil {
607-
uc.logger.Infow("msg", "credentials invalid", "ID", id)
619+
uc.logger.Infow("msg", "credentials invalid", "ID", id, "error", err)
608620
return nil
609621
}
610622

611623
// 2 - run validation
612624
_, err = provider.ValidateAndExtractCredentials(backend.Location, credsJSON)
613625
if err != nil {
614-
uc.logger.Infow("msg", "permissions validation failed", "ID", id)
626+
errMsg := err.Error()
627+
validationError = &errMsg
628+
uc.logger.Infow("msg", "permissions validation failed", "ID", id, "error", err)
615629
return nil
616630
}
617631

618632
// If everything went well, update the validation status to OK
619633
validationStatus = CASBackendValidationOK
634+
validationError = nil
620635
uc.logger.Infow("msg", "validation OK", "ID", id)
621636

622637
return nil

0 commit comments

Comments
 (0)