Skip to content

Commit dc1eb2e

Browse files
authored
feat: add support for optimistic concurrency check (#1133)
Signed-off-by: Miguel <[email protected]>
1 parent 103742f commit dc1eb2e

25 files changed

+867
-146
lines changed

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

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

app/controlplane/api/controlplane/v1/attestation_state.proto

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,19 @@ syntax = "proto3";
1717

1818
package controlplane.v1;
1919

20-
option go_package = "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1;v1";
21-
22-
import "buf/validate/validate.proto";
2320
import "attestation/v1/crafting_state.proto";
21+
import "buf/validate/validate.proto";
22+
import "errors/errors.proto";
23+
24+
option go_package = "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1;v1";
2425

2526
// API to remotely store and retrieve attestation state
2627
// using the attestation crafting process
2728
service AttestationStateService {
28-
rpc Initialized (AttestationStateServiceInitializedRequest) returns (AttestationStateServiceInitializedResponse);
29-
rpc Save (AttestationStateServiceSaveRequest) returns (AttestationStateServiceSaveResponse);
30-
rpc Read (AttestationStateServiceReadRequest) returns (AttestationStateServiceReadResponse);
31-
rpc Reset (AttestationStateServiceResetRequest) returns (AttestationStateServiceResetResponse);
29+
rpc Initialized(AttestationStateServiceInitializedRequest) returns (AttestationStateServiceInitializedResponse);
30+
rpc Save(AttestationStateServiceSaveRequest) returns (AttestationStateServiceSaveResponse);
31+
rpc Read(AttestationStateServiceReadRequest) returns (AttestationStateServiceReadResponse);
32+
rpc Reset(AttestationStateServiceResetRequest) returns (AttestationStateServiceResetResponse);
3233
}
3334

3435
message AttestationStateServiceInitializedRequest {
@@ -37,7 +38,7 @@ message AttestationStateServiceInitializedRequest {
3738

3839
message AttestationStateServiceInitializedResponse {
3940
Result result = 1;
40-
41+
4142
message Result {
4243
bool initialized = 1;
4344
}
@@ -46,6 +47,9 @@ message AttestationStateServiceInitializedResponse {
4647
message AttestationStateServiceSaveRequest {
4748
string workflow_run_id = 1 [(buf.validate.field).string = {min_len: 1}];
4849
attestation.v1.CraftingState attestation_state = 2 [(buf.validate.field).required = true];
50+
// digest of the attestation state this update was performed on top of
51+
// The digest might be empty the first time
52+
string base_digest = 3;
4953
}
5054

5155
message AttestationStateServiceSaveResponse {}
@@ -56,9 +60,11 @@ message AttestationStateServiceReadRequest {
5660

5761
message AttestationStateServiceReadResponse {
5862
Result result = 1;
59-
63+
6064
message Result {
6165
attestation.v1.CraftingState attestation_state = 2;
66+
// digest of the attestation state to implement Optimistic Concurrency Control
67+
string digest = 3;
6268
}
6369
}
6470

@@ -68,3 +74,8 @@ message AttestationStateServiceResetRequest {
6874

6975
message AttestationStateServiceResetResponse {}
7076

77+
enum AttestationStateError {
78+
ATTESTATION_STATE_ERROR_UNSPECIFIED = 0;
79+
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/409
80+
ATTESTATION_STATE_ERROR_CONFLICT = 1 [(errors.code) = 409];
81+
}

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

Lines changed: 26 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/frontend/controlplane/v1/attestation_state.ts

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

app/controlplane/internal/service/attestationstate.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,12 @@ func (s *AttestationStateService) Save(ctx context.Context, req *cpAPI.Attestati
9393
return nil, errors.Forbidden("forbidden", "failed to authenticate request")
9494
}
9595

96-
if err := s.attestationStateUseCase.Save(ctx, wf.ID.String(), req.WorkflowRunId, req.AttestationState, encryptionPassphrase); err != nil {
96+
err = s.attestationStateUseCase.Save(ctx, wf.ID.String(), req.WorkflowRunId, req.AttestationState, encryptionPassphrase, biz.WithAttStateBaseDigest(req.GetBaseDigest()))
97+
if err != nil {
98+
if biz.IsErrAttestationStateConflict(err) {
99+
return nil, cpAPI.ErrorAttestationStateErrorConflict(err.Error())
100+
}
101+
97102
return nil, handleUseCaseErr(err, s.log)
98103
}
99104

@@ -124,6 +129,7 @@ func (s *AttestationStateService) Read(ctx context.Context, req *cpAPI.Attestati
124129
return &cpAPI.AttestationStateServiceReadResponse{
125130
Result: &cpAPI.AttestationStateServiceReadResponse_Result{
126131
AttestationState: state.State,
132+
Digest: state.Digest,
127133
},
128134
}, nil
129135
}

app/controlplane/pkg/biz/attestationstate.go

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,14 @@ import (
3333

3434
type AttestationState struct {
3535
State *v1.CraftingState
36+
// Digest will be used for optimistic concurrency control
37+
Digest string
3638
}
3739

3840
type AttestationStateRepo interface {
3941
Initialized(ctx context.Context, workflowRunID uuid.UUID) (bool, error)
40-
Save(ctx context.Context, workflowRunID uuid.UUID, state []byte) error
41-
Read(ctx context.Context, workflowRunID uuid.UUID) ([]byte, error)
42+
Save(ctx context.Context, workflowRunID uuid.UUID, state []byte, baseDigest string) error
43+
Read(ctx context.Context, workflowRunID uuid.UUID) ([]byte, string, error)
4244
Reset(ctx context.Context, workflowRunID uuid.UUID) error
4345
}
4446

@@ -65,7 +67,25 @@ func (uc *AttestationStateUseCase) Initialized(ctx context.Context, workflowID,
6567
return initialized, nil
6668
}
6769

68-
func (uc *AttestationStateUseCase) Save(ctx context.Context, workflowID, runID string, state *v1.CraftingState, passphrase string) error {
70+
type AttestationStateSaveOpts struct {
71+
BaseDigest string
72+
}
73+
74+
type SaveOption func(*AttestationStateSaveOpts)
75+
76+
func WithAttStateBaseDigest(digest string) SaveOption {
77+
return func(o *AttestationStateSaveOpts) {
78+
o.BaseDigest = digest
79+
}
80+
}
81+
82+
func (uc *AttestationStateUseCase) Save(ctx context.Context, workflowID, runID string, state *v1.CraftingState, passphrase string, opts ...SaveOption) error {
83+
opt := &AttestationStateSaveOpts{}
84+
85+
for _, o := range opts {
86+
o(opt)
87+
}
88+
6989
runUUID, err := uc.checkWorkflowRunInWorkflow(ctx, workflowID, runID)
7090
if err != nil {
7191
return fmt.Errorf("failed to check workflow run: %w", err)
@@ -76,12 +96,16 @@ func (uc *AttestationStateUseCase) Save(ctx context.Context, workflowID, runID s
7696
return fmt.Errorf("failed to marshal attestation state: %w", err)
7797
}
7898

99+
if passphrase == "" {
100+
return NewErrValidationStr("passphrase is required")
101+
}
102+
79103
encryptedState, err := encrypt(rawState, passphrase)
80104
if err != nil {
81105
return fmt.Errorf("failed to encrypt attestation state: %w", err)
82106
}
83107

84-
if err := uc.repo.Save(ctx, *runUUID, encryptedState); err != nil {
108+
if err := uc.repo.Save(ctx, *runUUID, encryptedState, opt.BaseDigest); err != nil {
85109
return fmt.Errorf("failed to save attestation state: %w", err)
86110
}
87111

@@ -94,7 +118,7 @@ func (uc *AttestationStateUseCase) Read(ctx context.Context, workflowID, runID,
94118
return nil, fmt.Errorf("failed to check workflow run: %w", err)
95119
}
96120

97-
res, err := uc.repo.Read(ctx, *runUUID)
121+
res, digest, err := uc.repo.Read(ctx, *runUUID)
98122
if err != nil {
99123
return nil, fmt.Errorf("failed to read attestation state: %w", err)
100124
}
@@ -109,7 +133,7 @@ func (uc *AttestationStateUseCase) Read(ctx context.Context, workflowID, runID,
109133
return nil, fmt.Errorf("failed to unmarshal attestation state: %w", err)
110134
}
111135

112-
return &AttestationState{State: state}, nil
136+
return &AttestationState{State: state, Digest: digest}, nil
113137
}
114138

115139
func (uc *AttestationStateUseCase) Reset(ctx context.Context, workflowID, runID string) error {

0 commit comments

Comments
 (0)