Skip to content

Commit 58a26d5

Browse files
authored
Merge pull request #2732 from joejstuart/only-one-arch
feat(vsa): generate digest-based VSAs for index and arch images
2 parents 89cdbac + 0d4acdc commit 58a26d5

11 files changed

Lines changed: 203 additions & 146 deletions

File tree

cmd/validate/__snapshots__/image_test.snap

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
[Test_ValidateImageCommandYAMLPolicyFile/spec - 1]
32
{
43
"components": [
@@ -36,7 +35,6 @@
3635
"success": true
3736
}
3837
---
39-
4038
[Test_ValidateImageCommandYAMLPolicyFile/ecp - 1]
4139
{
4240
"components": [

cmd/validate/image.go

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -70,14 +70,16 @@ func validateImageCmd(validate imageValidationFunc) *cobra.Command {
7070
rekorURL string
7171
snapshot string
7272
spec *app.SnapshotSpec
73-
strict bool
74-
images string
75-
noColor bool
76-
forceColor bool
77-
workers int
78-
vsaEnabled bool
79-
vsaSigningKey string
80-
vsaUpload []string
73+
// Only used to pass the expansion info to the report. Not a cli flag.
74+
expansion *applicationsnapshot.ExpansionInfo
75+
strict bool
76+
images string
77+
noColor bool
78+
forceColor bool
79+
workers int
80+
vsaEnabled bool
81+
vsaSigningKey string
82+
vsaUpload []string
8183
}{
8284
strict: true,
8385
workers: 5,
@@ -200,7 +202,7 @@ func validateImageCmd(validate imageValidationFunc) *cobra.Command {
200202
cmd.SetContext(ctx)
201203
}
202204

203-
if s, err := applicationsnapshot.DetermineInputSpec(ctx, applicationsnapshot.Input{
205+
if s, exp, err := applicationsnapshot.DetermineInputSpec(ctx, applicationsnapshot.Input{
204206
File: data.filePath,
205207
JSON: data.input,
206208
Image: data.imageRef,
@@ -210,6 +212,7 @@ func validateImageCmd(validate imageValidationFunc) *cobra.Command {
210212
allErrors = errors.Join(allErrors, err)
211213
} else {
212214
data.spec = s
215+
data.expansion = exp
213216
}
214217

215218
policyConfiguration, err := validate_utils.GetPolicyConfig(ctx, data.policyConfiguration)
@@ -450,7 +453,7 @@ func validateImageCmd(validate imageValidationFunc) *cobra.Command {
450453
data.output = append(data.output, fmt.Sprintf("%s=%s", applicationsnapshot.JSON, data.outputFile))
451454
}
452455

453-
report, err := applicationsnapshot.NewReport(data.snapshot, components, data.policy, manyPolicyInput, showSuccesses)
456+
report, err := applicationsnapshot.NewReport(data.snapshot, components, data.policy, manyPolicyInput, showSuccesses, data.expansion)
454457
if err != nil {
455458
return err
456459
}

cmd/validate/image_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -323,7 +323,7 @@ func Test_determineInputSpec(t *testing.T) {
323323
ctx := oci.WithClient(context.Background(), &client)
324324
for _, c := range cases {
325325
t.Run(c.name, func(t *testing.T) {
326-
s, err := applicationsnapshot.DetermineInputSpec(ctx, applicationsnapshot.Input{
326+
s, _, err := applicationsnapshot.DetermineInputSpec(ctx, applicationsnapshot.Input{
327327
File: c.arguments.filePath,
328328
JSON: c.arguments.input,
329329
Image: c.arguments.imageRef,
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Copyright The Conforma Contributors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
//
15+
// SPDX-License-Identifier: Apache-2.0
16+
17+
package applicationsnapshot
18+
19+
// ExpansionInfo tracks the relationships between image indexes and their child manifests
20+
// that are created when expanding multi-arch images.
21+
type ExpansionInfo struct {
22+
// ChildrenByIndex maps an image index digest to the list of child manifest digests
23+
ChildrenByIndex map[string][]string `json:"childrenByIndex,omitempty"`
24+
// ParentByChild maps a child manifest digest to its parent index digest
25+
ParentByChild map[string]string `json:"parentByChild,omitempty"`
26+
// IndexAliases maps image references to their pinned digest form
27+
IndexAliases map[string]string `json:"indexAliases,omitempty"`
28+
}
29+
30+
// NewExpansionInfo creates a new ExpansionInfo instance
31+
func NewExpansionInfo() *ExpansionInfo {
32+
return &ExpansionInfo{
33+
ChildrenByIndex: make(map[string][]string),
34+
ParentByChild: make(map[string]string),
35+
IndexAliases: make(map[string]string),
36+
}
37+
}

internal/applicationsnapshot/input.go

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ type Input struct {
5454

5555
type snapshot struct {
5656
app.SnapshotSpec
57+
Expansion *ExpansionInfo
5758
}
5859

5960
func (s *snapshot) merge(snap app.SnapshotSpec) {
@@ -89,7 +90,7 @@ func (s *snapshot) merge(snap app.SnapshotSpec) {
8990
}
9091
}
9192

92-
func DetermineInputSpec(ctx context.Context, input Input) (*app.SnapshotSpec, error) {
93+
func DetermineInputSpec(ctx context.Context, input Input) (*app.SnapshotSpec, *ExpansionInfo, error) {
9394
var snapshot snapshot
9495
provided := false
9596

@@ -106,7 +107,7 @@ func DetermineInputSpec(ctx context.Context, input Input) (*app.SnapshotSpec, er
106107

107108
file, err := readSnapshotSource(content)
108109
if err != nil {
109-
return nil, err
110+
return nil, nil, err
110111
}
111112
snapshot.merge(file)
112113
provided = true
@@ -117,11 +118,11 @@ func DetermineInputSpec(ctx context.Context, input Input) (*app.SnapshotSpec, er
117118
fs := utils.FS(ctx)
118119
content, err := afero.ReadFile(fs, input.File)
119120
if err != nil {
120-
return nil, err
121+
return nil, nil, err
121122
}
122123
file, err := readSnapshotSource(content)
123124
if err != nil {
124-
return nil, err
125+
return nil, nil, err
125126
}
126127
snapshot.merge(file)
127128
provided = true
@@ -131,7 +132,7 @@ func DetermineInputSpec(ctx context.Context, input Input) (*app.SnapshotSpec, er
131132
if input.JSON != "" {
132133
json, err := readSnapshotSource([]byte(input.JSON))
133134
if err != nil {
134-
return nil, err
135+
return nil, nil, err
135136
}
136137
snapshot.merge(json)
137138
provided = true
@@ -156,25 +157,29 @@ func DetermineInputSpec(ctx context.Context, input Input) (*app.SnapshotSpec, er
156157
client, err := kubernetes.NewClient(ctx)
157158
if err != nil {
158159
log.Debugf("Unable to initialize Kubernetes Client: %v", err)
159-
return nil, err
160+
return nil, nil, err
160161
}
161162

162163
cluster, err := client.FetchSnapshot(ctx, input.Snapshot)
163164
if err != nil {
164165
log.Debugf("Unable to fetch snapshot %s from Kubernetes cluster: %v", input.Snapshot, err)
165-
return nil, err
166+
return nil, nil, err
166167
}
167168
snapshot.merge(cluster.Spec)
168169
provided = true
169170
}
170171

171172
if !provided {
172173
log.Debug("No application snapshot available")
173-
return nil, errors.New("neither Snapshot nor image reference provided to validate")
174+
return nil, nil, errors.New("neither Snapshot nor image reference provided to validate")
174175
}
175-
expandImageIndex(ctx, &snapshot.SnapshotSpec)
176+
exp := expandImageIndex(ctx, &snapshot.SnapshotSpec)
176177

177-
return &snapshot.SnapshotSpec, nil
178+
// Store expansion info in the snapshot for later use
179+
// This will be used when building the Report
180+
snapshot.Expansion = exp
181+
182+
return &snapshot.SnapshotSpec, exp, nil
178183
}
179184

180185
func readSnapshotSource(input []byte) (app.SnapshotSpec, error) {
@@ -192,7 +197,7 @@ func readSnapshotSource(input []byte) (app.SnapshotSpec, error) {
192197
// For an image index, remove the original component and replace it with an expanded component with all its image manifests
193198
// Do not raise an error if the image is inaccessible, it will be handled as a violation when evaluated against the policy
194199
// This is to retain the original behavior of the `ec validate` command.
195-
func imageIndexWorker(client oci.Client, component app.SnapshotComponent, componentChan chan<- []app.SnapshotComponent, errorsChan chan<- error) {
200+
func imageIndexWorker(client oci.Client, component app.SnapshotComponent, componentChan chan<- []app.SnapshotComponent, errorsChan chan<- error, exp *ExpansionInfo) {
196201
var components []app.SnapshotComponent
197202
components = append(components, component)
198203
// to avoid adding to componentsChan before each return
@@ -228,6 +233,10 @@ func imageIndexWorker(client oci.Client, component app.SnapshotComponent, compon
228233
return
229234
}
230235

236+
// Track expansion metadata
237+
idxPinned := fmt.Sprintf("%s@%s", ref.Context().Name(), desc.Digest)
238+
exp.IndexAliases[ref.Name()] = idxPinned
239+
231240
// Add the platform-specific image references (Image Manifests) to the list of components so
232241
// each is validated as well as the multi-platform image reference (Image Index).
233242
for i, manifest := range indexManifest.Manifests {
@@ -241,15 +250,21 @@ func imageIndexWorker(client oci.Client, component app.SnapshotComponent, compon
241250
archComponent.Name = fmt.Sprintf("%s-%s-%s", component.Name, manifest.Digest, arch)
242251
archComponent.ContainerImage = fmt.Sprintf("%s@%s", ref.Context().Name(), manifest.Digest)
243252
components = append(components, archComponent)
253+
254+
// Track parent-child relationships
255+
childPinned := archComponent.ContainerImage
256+
exp.ChildrenByIndex[idxPinned] = append(exp.ChildrenByIndex[idxPinned], childPinned)
257+
exp.ParentByChild[childPinned] = idxPinned
244258
}
245259
}
246260

247-
func expandImageIndex(ctx context.Context, snap *app.SnapshotSpec) {
261+
func expandImageIndex(ctx context.Context, snap *app.SnapshotSpec) *ExpansionInfo {
248262
if trace.IsEnabled() {
249263
region := trace.StartRegion(ctx, "ec:expand-image-index")
250264
defer region.End()
251265
}
252266

267+
exp := NewExpansionInfo()
253268
client := oci.NewClient(ctx)
254269

255270
componentChan := make(chan []app.SnapshotComponent, len(snap.Components))
@@ -259,7 +274,7 @@ func expandImageIndex(ctx context.Context, snap *app.SnapshotSpec) {
259274
for _, component := range snap.Components {
260275
// fetch manifests concurrently
261276
g.Go(func() error {
262-
imageIndexWorker(client, component, componentChan, errorsChan)
277+
imageIndexWorker(client, component, componentChan, errorsChan, exp)
263278
return nil
264279
})
265280
}
@@ -289,6 +304,8 @@ func expandImageIndex(ctx context.Context, snap *app.SnapshotSpec) {
289304
log.Warnf("Encountered error while checking for Image Index: %v", allErrors)
290305
}
291306
log.Debugf("Snap component after expanding the image index is %v", snap.Components)
307+
308+
return exp
292309
}
293310

294311
func imageWorkers() int {

internal/applicationsnapshot/input_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ func Test_DetermineInputSpec(t *testing.T) {
177177
panic(err)
178178
}
179179
}
180-
got, err := DetermineInputSpec(ctx, tc.input)
180+
got, _, err := DetermineInputSpec(ctx, tc.input)
181181
// expect an error so check for nil
182182
if tc.want != nil {
183183
assert.NoError(t, err)

internal/applicationsnapshot/report.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ type Report struct {
6161
EffectiveTime time.Time `json:"effective-time"`
6262
PolicyInput [][]byte `json:"-"`
6363
ShowSuccesses bool `json:"-"`
64+
Expansion *ExpansionInfo `json:"-"`
6465
}
6566

6667
type summary struct {
@@ -126,7 +127,7 @@ var OutputFormats = []string{
126127

127128
// WriteReport returns a new instance of Report representing the state of
128129
// components from the snapshot.
129-
func NewReport(snapshot string, components []Component, policy policy.Policy, policyInput [][]byte, showSuccesses bool) (Report, error) {
130+
func NewReport(snapshot string, components []Component, policy policy.Policy, policyInput [][]byte, showSuccesses bool, expansion *ExpansionInfo) (Report, error) {
130131
success := true
131132

132133
// Set the report success, remains true if all components are successful
@@ -157,6 +158,7 @@ func NewReport(snapshot string, components []Component, policy policy.Policy, po
157158
PolicyInput: policyInput,
158159
EffectiveTime: policy.EffectiveTime().UTC(),
159160
ShowSuccesses: showSuccesses,
161+
Expansion: expansion,
160162
}, nil
161163
}
162164

internal/applicationsnapshot/report_test.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ func Test_ReportJson(t *testing.T) {
5252

5353
ctx := context.Background()
5454
testPolicy := createTestPolicy(t, ctx)
55-
report, err := NewReport("snappy", components, testPolicy, nil, true)
55+
report, err := NewReport("snappy", components, testPolicy, nil, true, nil)
5656
assert.NoError(t, err)
5757

5858
testEffectiveTime := testPolicy.EffectiveTime().UTC().Format(time.RFC3339Nano)
@@ -110,7 +110,7 @@ func Test_ReportYaml(t *testing.T) {
110110

111111
ctx := context.Background()
112112
testPolicy := createTestPolicy(t, ctx)
113-
report, err := NewReport("snappy", components, testPolicy, nil, true)
113+
report, err := NewReport("snappy", components, testPolicy, nil, true, nil)
114114
assert.NoError(t, err)
115115

116116
testEffectiveTime := testPolicy.EffectiveTime().UTC().Format(time.RFC3339Nano)
@@ -257,7 +257,7 @@ func Test_GenerateMarkdownSummary(t *testing.T) {
257257
for _, c := range cases {
258258
t.Run(c.name, func(t *testing.T) {
259259
ctx := context.Background()
260-
report, err := NewReport(c.snapshot, c.components, createTestPolicy(t, ctx), nil, true)
260+
report, err := NewReport(c.snapshot, c.components, createTestPolicy(t, ctx), nil, true, nil)
261261
assert.NoError(t, err)
262262
report.created = time.Unix(0, 0).UTC()
263263

@@ -504,7 +504,7 @@ func Test_ReportSummary(t *testing.T) {
504504
for _, tc := range tests {
505505
t.Run(fmt.Sprintf("NewReport=%s", tc.name), func(t *testing.T) {
506506
ctx := context.Background()
507-
report, err := NewReport(tc.snapshot, []Component{tc.input}, createTestPolicy(t, ctx), nil, true)
507+
report, err := NewReport(tc.snapshot, []Component{tc.input}, createTestPolicy(t, ctx), nil, true, nil)
508508
assert.NoError(t, err)
509509
assert.Equal(t, tc.want, report.toSummary())
510510
})
@@ -641,7 +641,7 @@ func Test_ReportAppstudio(t *testing.T) {
641641
assert.NoError(t, err)
642642

643643
ctx := context.Background()
644-
report, err := NewReport(c.snapshot, c.components, createTestPolicy(t, ctx), nil, true)
644+
report, err := NewReport(c.snapshot, c.components, createTestPolicy(t, ctx), nil, true, nil)
645645
assert.NoError(t, err)
646646
assert.False(t, report.created.IsZero())
647647
assert.Equal(t, c.success, report.Success)
@@ -789,7 +789,7 @@ func Test_ReportHACBS(t *testing.T) {
789789
assert.NoError(t, err)
790790

791791
ctx := context.Background()
792-
report, err := NewReport(c.snapshot, c.components, createTestPolicy(t, ctx), nil, true)
792+
report, err := NewReport(c.snapshot, c.components, createTestPolicy(t, ctx), nil, true, nil)
793793
assert.NoError(t, err)
794794
assert.False(t, report.created.IsZero())
795795
assert.Equal(t, c.success, report.Success)
@@ -821,7 +821,7 @@ func Test_ReportPolicyInput(t *testing.T) {
821821
}
822822

823823
ctx := context.Background()
824-
report, err := NewReport("snapshot", nil, createTestPolicy(t, ctx), policyInput, true)
824+
report, err := NewReport("snapshot", nil, createTestPolicy(t, ctx), policyInput, true, nil)
825825
require.NoError(t, err)
826826

827827
p := format.NewTargetParser(JSON, format.Options{}, defaultWriter, fs)

internal/input/report_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ func Test_ReportSummary(t *testing.T) {
233233
t.Run(fmt.Sprintf("NewReport=%s", tc.name), func(t *testing.T) {
234234
ctx := context.Background()
235235
report, err := NewReport(tc.input, createTestPolicy(t, ctx), nil)
236-
// report, err := NewReport(tc.snapshot, []Component{tc.input}, createTestPolicy(t, ctx), nil)
236+
// report, err := NewReport(tc.snapshot, []Component{tc.input}, createTestPolicy(t, ctx), nil, nil)
237237
assert.NoError(t, err)
238238
fmt.Println("\n\nExpected:\n", tc.want, "\n\nActual:\n", report.toSummary())
239239
assert.Equal(t, tc.want, report.toSummary())

0 commit comments

Comments
 (0)