Skip to content

Commit 3b3eb49

Browse files
authored
feat(policies): materials in policy groups (#1455)
Signed-off-by: Jose I. Paris <jiparis@chainloop.dev>
1 parent 4bcbec9 commit 3b3eb49

17 files changed

Lines changed: 608 additions & 250 deletions

app/cli/internal/action/attestation_init.go

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,11 @@ import (
2222
"strconv"
2323

2424
pb "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1"
25+
v1 "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1"
2526
"github.com/chainloop-dev/chainloop/pkg/attestation/crafter"
2627
clientAPI "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/api/attestation/v1"
28+
"github.com/chainloop-dev/chainloop/pkg/policies"
29+
"github.com/rs/zerolog"
2730
)
2831

2932
type AttestationInitOpts struct {
@@ -126,6 +129,12 @@ func (action *AttestationInit) Run(ctx context.Context, opts *AttestationInitRun
126129

127130
action.Logger.Debug().Msg("workflow contract and metadata retrieved from the control plane")
128131

132+
// 3. enrich contract with group materials and policies
133+
err = enrichContractMaterials(ctx, contractVersion.GetV1(), client, &action.Logger)
134+
if err != nil {
135+
return "", fmt.Errorf("failed to apply materials from policy groups: %w", err)
136+
}
137+
129138
// Auto discover the runner context and enforce against the one in the contract if needed
130139
discoveredRunner, err := crafter.DiscoverAndEnforceRunner(contractVersion.GetV1().GetRunner().GetType(), action.dryRun, action.Logger)
131140
if err != nil {
@@ -164,7 +173,8 @@ func (action *AttestationInit) Run(ctx context.Context, opts *AttestationInitRun
164173
// NOTE: important to run this initialization here since workflowMeta is populated
165174
// with the workflowRunId that comes from the control plane
166175
initOpts := &crafter.InitOpts{
167-
WfInfo: workflowMeta, SchemaV1: contractVersion.GetV1(),
176+
WfInfo: workflowMeta,
177+
SchemaV1: contractVersion.GetV1(),
168178
DryRun: action.dryRun,
169179
AttestationID: attestationID,
170180
Runner: discoveredRunner,
@@ -186,3 +196,44 @@ func (action *AttestationInit) Run(ctx context.Context, opts *AttestationInitRun
186196

187197
return attestationID, nil
188198
}
199+
200+
func enrichContractMaterials(ctx context.Context, schema *v1.CraftingSchema, client pb.AttestationServiceClient, logger *zerolog.Logger) error {
201+
contractMaterials := schema.GetMaterials()
202+
for _, pgAtt := range schema.GetPolicyGroups() {
203+
group, _, err := policies.LoadPolicyGroup(ctx, pgAtt, &policies.LoadPolicyGroupOptions{
204+
Client: client,
205+
Logger: logger,
206+
})
207+
if err != nil {
208+
return fmt.Errorf("failed to load policy group: %w", err)
209+
}
210+
logger.Debug().Msgf("adding materials from policy group '%s'", group.GetMetadata().GetName())
211+
212+
toAdd := getGroupMaterialsToAdd(group, contractMaterials, logger)
213+
contractMaterials = append(contractMaterials, toAdd...)
214+
}
215+
216+
schema.Materials = contractMaterials
217+
218+
return nil
219+
}
220+
221+
// merge existing materials with group ones, taking the contract's one in case of conflict
222+
func getGroupMaterialsToAdd(group *v1.PolicyGroup, fromContract []*v1.CraftingSchema_Material, logger *zerolog.Logger) []*v1.CraftingSchema_Material {
223+
toAdd := make([]*v1.CraftingSchema_Material, 0)
224+
for _, groupMaterial := range group.GetSpec().GetPolicies().GetMaterials() {
225+
// check if material already exists in the contract and skip it in that case
226+
ignore := false
227+
for _, mat := range fromContract {
228+
if mat.GetName() == groupMaterial.GetName() {
229+
logger.Warn().Msgf("material '%s' from policy group '%s' is also present in the contract and will be ignored", mat.GetName(), group.GetMetadata().GetName())
230+
ignore = true
231+
}
232+
}
233+
if !ignore {
234+
toAdd = append(toAdd, groupMaterial)
235+
}
236+
}
237+
238+
return toAdd
239+
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
//
2+
// Copyright 2024 The Chainloop Authors.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
package action
17+
18+
import (
19+
"context"
20+
"slices"
21+
"testing"
22+
23+
v1 "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1"
24+
"github.com/rs/zerolog"
25+
"github.com/stretchr/testify/assert"
26+
"github.com/stretchr/testify/require"
27+
)
28+
29+
func TestEnrichMaterials(t *testing.T) {
30+
cases := []struct {
31+
name string
32+
materials []*v1.CraftingSchema_Material
33+
policyGroup string
34+
expectErr bool
35+
nMaterials int
36+
nPolicies int
37+
}{
38+
{
39+
name: "existing material",
40+
materials: []*v1.CraftingSchema_Material{
41+
{
42+
Type: v1.CraftingSchema_Material_SBOM_SPDX_JSON,
43+
Name: "sbom",
44+
},
45+
},
46+
policyGroup: "file://testdata/policy_group.yaml",
47+
nMaterials: 2,
48+
nPolicies: 0,
49+
},
50+
{
51+
name: "new materials",
52+
materials: []*v1.CraftingSchema_Material{
53+
{
54+
Type: v1.CraftingSchema_Material_SBOM_CYCLONEDX_JSON,
55+
Name: "another-sbom",
56+
},
57+
},
58+
policyGroup: "file://testdata/policy_group.yaml",
59+
nMaterials: 3,
60+
nPolicies: 1,
61+
},
62+
{
63+
name: "empty materials in schema",
64+
materials: []*v1.CraftingSchema_Material{},
65+
policyGroup: "file://testdata/policy_group.yaml",
66+
nMaterials: 2,
67+
nPolicies: 1,
68+
},
69+
{
70+
name: "wrong policy group",
71+
materials: []*v1.CraftingSchema_Material{},
72+
policyGroup: "file://testdata/idontexist.yaml",
73+
expectErr: true,
74+
},
75+
}
76+
77+
l := zerolog.Nop()
78+
for _, tc := range cases {
79+
t.Run(tc.name, func(t *testing.T) {
80+
schema := v1.CraftingSchema{
81+
Materials: tc.materials,
82+
PolicyGroups: []*v1.PolicyGroupAttachment{
83+
{
84+
Ref: tc.policyGroup,
85+
},
86+
},
87+
}
88+
err := enrichContractMaterials(context.TODO(), &schema, nil, &l)
89+
if tc.expectErr {
90+
assert.Error(t, err)
91+
return
92+
}
93+
require.NoError(t, err)
94+
assert.Len(t, schema.Materials, tc.nMaterials)
95+
// find "sbom" material and check it has proper policies
96+
assert.True(t, slices.ContainsFunc(schema.Materials, func(m *v1.CraftingSchema_Material) bool {
97+
return m.Name == "sbom" && len(m.Policies) == tc.nPolicies
98+
}))
99+
})
100+
}
101+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
apiVersion: workflowcontract.chainloop.dev/v1
2+
kind: PolicyGroup
3+
metadata:
4+
name: sbom-quality
5+
description: This policy group applies a number of SBOM-related policies
6+
annotations:
7+
category: SBOM
8+
spec:
9+
policies:
10+
attestation:
11+
- ref: file://testdata/with_arguments.yaml
12+
materials:
13+
- name: sbom
14+
type: SBOM_SPDX_JSON
15+
policies:
16+
- ref: file://testdata/multi-kind.yaml
17+
- name: container
18+
type: CONTAINER_IMAGE

app/controlplane/api/gen/frontend/workflowcontract/v1/crafting_schema.ts

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

app/controlplane/api/gen/jsonschema/workflowcontract.v1.CraftingSchema.Material.jsonschema.json

Lines changed: 7 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/workflowcontract.v1.CraftingSchema.Material.schema.json

Lines changed: 7 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/workflowcontract.v1.PolicyGroup.GroupPolicies.jsonschema.json

Lines changed: 21 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/workflowcontract.v1.PolicyGroup.GroupPolicies.schema.json

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

0 commit comments

Comments
 (0)