Skip to content
Open
21 changes: 17 additions & 4 deletions app/controlplane/api/controlplane/v1/workflow_contract.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions app/controlplane/api/controlplane/v1/workflow_contract.proto
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,11 @@ message WorkflowContractServiceApplyRequest {
repeated string batch_policy_names = 3;
// Same as batch_policy_names, for policy groups created/updated in the same batch.
repeated string batch_policy_group_names = 4;
// Raw representation (json or yaml) of the policies created/updated in the same batch apply.
// Only used on dry runs, ignored on a real apply (which persists batch resources first).
// When present, the specs of batch-local references are used to validate their inputs and
// whether the attachment can apply to a material, as a real apply does.
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Resolved
repeated bytes batch_policies = 5;
}

message WorkflowContractServiceApplyResponse {
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion app/controlplane/internal/service/attestation.go
Original file line number Diff line number Diff line change
Expand Up @@ -809,7 +809,7 @@ func (s *AttestationService) FindOrCreateWorkflow(ctx context.Context, req *cpAP

// contract validation
if req.GetContractBytes() != nil {
if err = s.workflowContractUseCase.ValidateContractPolicies(ctx, req.GetContractBytes(), token, nil, nil); err != nil {
if err = s.workflowContractUseCase.ValidateContractPolicies(ctx, req.GetContractBytes(), token, nil, nil, nil); err != nil {
return nil, handleUseCaseErr(ctx, err, s.log)
}
}
Expand Down
39 changes: 36 additions & 3 deletions app/controlplane/internal/service/workflowcontract.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ func (s *WorkflowContractService) Create(ctx context.Context, req *pb.WorkflowCo
}

if len(req.RawContract) != 0 {
if err = s.contractUseCase.ValidateContractPolicies(ctx, req.RawContract, token, nil, nil); err != nil {
if err = s.contractUseCase.ValidateContractPolicies(ctx, req.RawContract, token, nil, nil, nil); err != nil {
return nil, handleUseCaseErr(ctx, err, s.log)
}
}
Expand Down Expand Up @@ -214,7 +214,7 @@ func (s *WorkflowContractService) Update(ctx context.Context, req *pb.WorkflowCo

// Validate the contract policies if the raw contract is provided
if len(req.RawContract) != 0 {
if err = s.contractUseCase.ValidateContractPolicies(ctx, req.RawContract, token, nil, nil); err != nil {
if err = s.contractUseCase.ValidateContractPolicies(ctx, req.RawContract, token, nil, nil, nil); err != nil {
return nil, handleUseCaseErr(ctx, err, s.log)
}
}
Expand Down Expand Up @@ -259,12 +259,17 @@ func (s *WorkflowContractService) Apply(ctx context.Context, req *pb.WorkflowCon
// the resources may not be persisted yet. A real apply persists batch resources before the
// contract, so it must always validate fully and never trust the client-supplied batch lists.
var batchPolicyNames, batchPolicyGroupNames []string
var batchPolicies map[string]*schemav1.Policy
if dryRun {
batchPolicyNames = req.GetBatchPolicyNames()
batchPolicyGroupNames = req.GetBatchPolicyGroupNames()

if batchPolicies, err = parseBatchPolicies(req.GetBatchPolicies()); err != nil {
return nil, err
}
}

if err = s.contractUseCase.ValidateContractPolicies(ctx, req.RawSchema, token, batchPolicyNames, batchPolicyGroupNames); err != nil {
if err = s.contractUseCase.ValidateContractPolicies(ctx, req.RawSchema, token, batchPolicyNames, batchPolicyGroupNames, batchPolicies); err != nil {
return nil, handleUseCaseErr(ctx, err, s.log)
}

Expand Down Expand Up @@ -526,6 +531,34 @@ func validateAndExtractMetadata(rawContract []byte, explicitName, explicitDesc s
return name, description, nil
}

// parseBatchPolicies parses the raw policies sent with a batch apply, indexed by policy name
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Resolved
func parseBatchPolicies(raws [][]byte) (map[string]*schemav1.Policy, error) {
if len(raws) == 0 {
return nil, nil
}

parsed := make(map[string]*schemav1.Policy, len(raws))
for _, raw := range raws {
format, err := unmarshal.IdentifyFormat(raw)
if err != nil {
return nil, errors.BadRequest("invalid", fmt.Sprintf("invalid batch policy: %s", err))
}

policy := &schemav1.Policy{}
if err := unmarshal.FromRaw(raw, format, policy, true); err != nil {
return nil, errors.BadRequest("invalid", fmt.Sprintf("invalid batch policy: %s", err))
}

if policy.GetMetadata().GetName() == "" {
return nil, errors.BadRequest("invalid", "invalid batch policy: missing metadata name")
}

parsed[policy.GetMetadata().GetName()] = policy
}

return parsed, nil
}

// extractMetadata attempts to extract the name from metadata.name in v2 contracts
func extractMetadata(rawContract []byte) (*schemav1.Metadata, error) {
if len(rawContract) == 0 {
Expand Down
56 changes: 56 additions & 0 deletions app/controlplane/internal/service/workflowcontract_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,62 @@ import (
"github.com/stretchr/testify/require"
)

const batchPolName = "batch-pol"

func TestParseBatchPolicies(t *testing.T) {
t.Parallel()

tests := []struct {
name string
raws [][]byte
wantNames []string
wantErr bool
}{
{name: "no policies"},
{
name: "valid kind-less policy",
raws: [][]byte{[]byte(`apiVersion: chainloop.dev/v1
kind: Policy
metadata:
name: batch-pol
spec:
policies:
- embedded: |
package main
`)},
wantNames: []string{batchPolName},
},
{
name: "only a metadata name is not a valid policy",
raws: [][]byte{[]byte("metadata:\n name: batch-pol\n")},
wantErr: true,
},
{
name: "missing metadata name",
raws: [][]byte{[]byte("apiVersion: chainloop.dev/v1\nkind: Policy\nspec:\n policies:\n - embedded: package main\n")},
wantErr: true,
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()

got, err := parseBatchPolicies(tc.raws)
if tc.wantErr {
require.Error(t, err)
return
}

require.NoError(t, err)
assert.Len(t, got, len(tc.wantNames))
for _, n := range tc.wantNames {
assert.Contains(t, got, n)
}
})
}
}

func TestExtractMetadata(t *testing.T) {
t.Parallel()

Expand Down
Loading
Loading