Skip to content

Commit 562c17d

Browse files
authored
Merge pull request #2128 from joejstuart/EC-975
Resolve ImageManifests concurrently
2 parents b408abf + eb0fefa commit 562c17d

File tree

3 files changed

+128
-74
lines changed

3 files changed

+128
-74
lines changed

cmd/validate/image_test.go

+18-17
Original file line numberDiff line numberDiff line change
@@ -173,13 +173,17 @@ func Test_determineInputSpec(t *testing.T) {
173173
{
174174
"name": "single-container-app",
175175
"containerImage": "quay.io/hacbs-contract-demo/single-container-app:62c06bf"
176-
}
176+
},
177177
]
178178
}`,
179179
},
180180
spec: &app.SnapshotSpec{
181181
Application: "app1",
182182
Components: []app.SnapshotComponent{
183+
{
184+
Name: "single-container-app",
185+
ContainerImage: "quay.io/hacbs-contract-demo/single-container-app:62c06bf",
186+
},
183187
{
184188
Name: "nodejs",
185189
ContainerImage: "quay.io/hacbs-contract-demo/single-nodejs-app:877418e",
@@ -188,10 +192,6 @@ func Test_determineInputSpec(t *testing.T) {
188192
Name: "petclinic",
189193
ContainerImage: "quay.io/hacbs-contract-demo/spring-petclinic:dc80a7f",
190194
},
191-
{
192-
Name: "single-container-app",
193-
ContainerImage: "quay.io/hacbs-contract-demo/single-container-app:62c06bf",
194-
},
195195
},
196196
},
197197
},
@@ -213,6 +213,10 @@ func Test_determineInputSpec(t *testing.T) {
213213
spec: &app.SnapshotSpec{
214214
Application: "app1",
215215
Components: []app.SnapshotComponent{
216+
{
217+
Name: "single-container-app",
218+
ContainerImage: "quay.io/hacbs-contract-demo/single-container-app:62c06bf",
219+
},
216220
{
217221
Name: "nodejs",
218222
ContainerImage: "quay.io/hacbs-contract-demo/single-nodejs-app:877418e",
@@ -221,10 +225,6 @@ func Test_determineInputSpec(t *testing.T) {
221225
Name: "petclinic",
222226
ContainerImage: "quay.io/hacbs-contract-demo/spring-petclinic:dc80a7f",
223227
},
224-
{
225-
Name: "single-container-app",
226-
ContainerImage: "quay.io/hacbs-contract-demo/single-container-app:62c06bf",
227-
},
228228
},
229229
},
230230
},
@@ -236,6 +236,10 @@ func Test_determineInputSpec(t *testing.T) {
236236
spec: &app.SnapshotSpec{
237237
Application: "app1",
238238
Components: []app.SnapshotComponent{
239+
{
240+
Name: "single-container-app",
241+
ContainerImage: "quay.io/hacbs-contract-demo/single-container-app:62c06bf",
242+
},
239243
{
240244
Name: "nodejs",
241245
ContainerImage: "quay.io/hacbs-contract-demo/single-nodejs-app:877418e",
@@ -244,10 +248,6 @@ func Test_determineInputSpec(t *testing.T) {
244248
Name: "petclinic",
245249
ContainerImage: "quay.io/hacbs-contract-demo/spring-petclinic:dc80a7f",
246250
},
247-
{
248-
Name: "single-container-app",
249-
ContainerImage: "quay.io/hacbs-contract-demo/single-container-app:62c06bf",
250-
},
251251
},
252252
},
253253
},
@@ -259,6 +259,10 @@ func Test_determineInputSpec(t *testing.T) {
259259
spec: &app.SnapshotSpec{
260260
Application: "app1",
261261
Components: []app.SnapshotComponent{
262+
{
263+
Name: "single-container-app",
264+
ContainerImage: "quay.io/hacbs-contract-demo/single-container-app:62c06bf",
265+
},
262266
{
263267
Name: "nodejs",
264268
ContainerImage: "quay.io/hacbs-contract-demo/single-nodejs-app:877418e",
@@ -267,10 +271,6 @@ func Test_determineInputSpec(t *testing.T) {
267271
Name: "petclinic",
268272
ContainerImage: "quay.io/hacbs-contract-demo/spring-petclinic:dc80a7f",
269273
},
270-
{
271-
Name: "single-container-app",
272-
ContainerImage: "quay.io/hacbs-contract-demo/single-container-app:62c06bf",
273-
},
274274
},
275275
},
276276
},
@@ -291,6 +291,7 @@ func Test_determineInputSpec(t *testing.T) {
291291
} else {
292292
assert.NoError(t, err)
293293
}
294+
294295
assert.Equal(t, c.spec, s)
295296
})
296297
}

internal/applicationsnapshot/input.go

+101-48
Original file line numberDiff line numberDiff line change
@@ -20,21 +20,29 @@ import (
2020
"context"
2121
"errors"
2222
"fmt"
23+
"os"
2324
"runtime/trace"
25+
"sort"
26+
"strconv"
2427

2528
"github.com/google/go-containerregistry/pkg/name"
2629
app "github.com/konflux-ci/application-api/api/v1alpha1"
2730
log "github.com/sirupsen/logrus"
2831
"github.com/spf13/afero"
2932
"golang.org/x/exp/slices"
33+
"golang.org/x/sync/errgroup"
3034
"sigs.k8s.io/yaml"
3135

3236
"github.com/enterprise-contract/ec-cli/internal/kubernetes"
3337
"github.com/enterprise-contract/ec-cli/internal/utils"
3438
"github.com/enterprise-contract/ec-cli/internal/utils/oci"
3539
)
3640

37-
const unnamed = "Unnamed"
41+
const (
42+
unnamed = "Unnamed"
43+
workersEnvVar = "IMAGE_INDEX_WORKERS"
44+
defaultWorkers = 5
45+
)
3846

3947
type Input struct {
4048
File string // Deprecated: replaced by images
@@ -181,69 +189,114 @@ func readSnapshotSource(input []byte) (app.SnapshotSpec, error) {
181189
return file, nil
182190
}
183191

192+
// For an image index, remove the original component and replace it with an expanded component with all its image manifests
193+
// Do not raise an error if the image is inaccessible, it will be handled as a violation when evaluated against the policy
194+
// 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) {
196+
var components []app.SnapshotComponent
197+
components = append(components, component)
198+
// to avoid adding to componentsChan before each return
199+
defer func() {
200+
componentChan <- components
201+
}()
202+
203+
ref, err := name.ParseReference(component.ContainerImage)
204+
if err != nil {
205+
errorsChan <- fmt.Errorf("unable to parse container image %s: %w", component.ContainerImage, err)
206+
return
207+
}
208+
209+
desc, err := client.Head(ref)
210+
if err != nil {
211+
errorsChan <- fmt.Errorf("unable to fetch descriptior for container image %s: %w", ref, err)
212+
return
213+
}
214+
215+
if !desc.MediaType.IsIndex() {
216+
return
217+
}
218+
219+
index, err := client.Index(ref)
220+
if err != nil {
221+
errorsChan <- fmt.Errorf("unable to fetch index for container image %s: %w", component.ContainerImage, err)
222+
return
223+
}
224+
225+
indexManifest, err := index.IndexManifest()
226+
if err != nil {
227+
errorsChan <- fmt.Errorf("unable to fetch index manifest for container image %s: %w", component.ContainerImage, err)
228+
return
229+
}
230+
231+
// Add the platform-specific image references (Image Manifests) to the list of components so
232+
// each is validated as well as the multi-platform image reference (Image Index).
233+
for i, manifest := range indexManifest.Manifests {
234+
var arch string
235+
if manifest.Platform != nil && manifest.Platform.Architecture != "" {
236+
arch = manifest.Platform.Architecture
237+
} else {
238+
arch = fmt.Sprintf("noarch-%d", i)
239+
}
240+
archComponent := component
241+
archComponent.Name = fmt.Sprintf("%s-%s-%s", component.Name, manifest.Digest, arch)
242+
archComponent.ContainerImage = fmt.Sprintf("%s@%s", ref.Context().Name(), manifest.Digest)
243+
components = append(components, archComponent)
244+
}
245+
}
246+
184247
func expandImageIndex(ctx context.Context, snap *app.SnapshotSpec) {
185248
if trace.IsEnabled() {
186249
region := trace.StartRegion(ctx, "ec:expand-image-index")
187250
defer region.End()
188251
}
189252

190253
client := oci.NewClient(ctx)
191-
// For an image index, remove the original component and replace it with an expanded component with all its image manifests
192-
var components []app.SnapshotComponent
193-
// Do not raise an error if the image is inaccessible, it will be handled as a violation when evaluated against the policy
194-
// This is to retain the original behavior of the `ec validate` command.
195-
var allErrors error = nil
196-
for _, component := range snap.Components {
197-
// Assume the image is not an image index or it isn't accessible
198-
components = append(components, component)
199-
ref, err := name.ParseReference(component.ContainerImage)
200-
if err != nil {
201-
allErrors = errors.Join(allErrors, fmt.Errorf("unable to parse container image %s: %w", component.ContainerImage, err))
202-
continue
203-
}
204254

205-
desc, err := client.Head(ref)
206-
if err != nil {
207-
allErrors = errors.Join(allErrors, fmt.Errorf("unable to fetch descriptior for container image %s: %w", ref, err))
208-
continue
209-
}
255+
componentChan := make(chan []app.SnapshotComponent, len(snap.Components))
256+
errorsChan := make(chan error, len(snap.Components))
257+
g, _ := errgroup.WithContext(ctx)
258+
g.SetLimit(imageWorkers())
259+
for _, component := range snap.Components {
260+
// fetch manifests concurrently
261+
g.Go(func() error {
262+
imageIndexWorker(client, component, componentChan, errorsChan)
263+
return nil
264+
})
265+
}
210266

211-
if !desc.MediaType.IsIndex() {
212-
continue
213-
}
267+
go func() {
268+
_ = g.Wait()
269+
close(componentChan)
270+
close(errorsChan)
271+
}()
214272

215-
index, err := client.Index(ref)
216-
if err != nil {
217-
allErrors = errors.Join(allErrors, fmt.Errorf("unable to fetch index for container image %s: %w", component.ContainerImage, err))
218-
continue
219-
}
273+
var components []app.SnapshotComponent
274+
for component := range componentChan {
275+
components = append(components, component...)
276+
}
277+
snap.Components = components
220278

221-
indexManifest, err := index.IndexManifest()
222-
if err != nil {
223-
allErrors = errors.Join(allErrors, fmt.Errorf("unable to fetch index manifest for container image %s: %w", component.ContainerImage, err))
224-
continue
225-
}
279+
sort.Slice(snap.Components, func(i, j int) bool {
280+
return snap.Components[i].ContainerImage < snap.Components[j].ContainerImage
281+
})
226282

227-
// Add the platform-specific image references (Image Manifests) to the list of components so
228-
// each is validated as well as the multi-platform image reference (Image Index).
229-
for i, manifest := range indexManifest.Manifests {
230-
var arch string
231-
if manifest.Platform != nil && manifest.Platform.Architecture != "" {
232-
arch = manifest.Platform.Architecture
233-
} else {
234-
arch = fmt.Sprintf("noarch-%d", i)
235-
}
236-
archComponent := component
237-
archComponent.Name = fmt.Sprintf("%s-%s-%s", component.Name, manifest.Digest, arch)
238-
archComponent.ContainerImage = fmt.Sprintf("%s@%s", ref.Context().Name(), manifest.Digest)
239-
components = append(components, archComponent)
240-
}
283+
var allErrors error = nil
284+
for err := range errorsChan {
285+
allErrors = errors.Join(allErrors, err)
241286
}
242287

243-
snap.Components = components
244-
245288
if allErrors != nil {
246289
log.Warnf("Encountered error while checking for Image Index: %v", allErrors)
247290
}
248291
log.Debugf("Snap component after expanding the image index is %v", snap.Components)
249292
}
293+
294+
func imageWorkers() int {
295+
workers := defaultWorkers
296+
if value, exists := os.LookupEnv(workersEnvVar); exists {
297+
if parsed, err := strconv.Atoi(value); err == nil {
298+
workers = parsed
299+
}
300+
}
301+
return workers
302+
}

internal/applicationsnapshot/input_test.go

+9-9
Original file line numberDiff line numberDiff line change
@@ -119,15 +119,15 @@ func Test_DetermineInputSpec(t *testing.T) {
119119
},
120120
want: &app.SnapshotSpec{
121121
Components: []app.SnapshotComponent{
122-
snapshot.Components[0],
123-
{
124-
Name: "Named",
125-
ContainerImage: "registry.io/repository/image:different",
126-
},
127122
{
128123
Name: "Unnamed",
129124
ContainerImage: "registry.io/repository/image:another",
130125
},
126+
{
127+
Name: "Named",
128+
ContainerImage: "registry.io/repository/image:different",
129+
},
130+
snapshot.Components[0],
131131
},
132132
},
133133
},
@@ -140,14 +140,14 @@ func Test_DetermineInputSpec(t *testing.T) {
140140
},
141141
want: &app.SnapshotSpec{
142142
Components: []app.SnapshotComponent{
143-
{
144-
Name: "Named",
145-
ContainerImage: imageRef,
146-
},
147143
{
148144
Name: "Set name",
149145
ContainerImage: "registry.io/repository/image:another",
150146
},
147+
{
148+
Name: "Named",
149+
ContainerImage: imageRef,
150+
},
151151
},
152152
},
153153
},

0 commit comments

Comments
 (0)