@@ -20,21 +20,29 @@ import (
20
20
"context"
21
21
"errors"
22
22
"fmt"
23
+ "os"
23
24
"runtime/trace"
25
+ "sort"
26
+ "strconv"
24
27
25
28
"github.com/google/go-containerregistry/pkg/name"
26
29
app "github.com/konflux-ci/application-api/api/v1alpha1"
27
30
log "github.com/sirupsen/logrus"
28
31
"github.com/spf13/afero"
29
32
"golang.org/x/exp/slices"
33
+ "golang.org/x/sync/errgroup"
30
34
"sigs.k8s.io/yaml"
31
35
32
36
"github.com/enterprise-contract/ec-cli/internal/kubernetes"
33
37
"github.com/enterprise-contract/ec-cli/internal/utils"
34
38
"github.com/enterprise-contract/ec-cli/internal/utils/oci"
35
39
)
36
40
37
- const unnamed = "Unnamed"
41
+ const (
42
+ unnamed = "Unnamed"
43
+ workersEnvVar = "IMAGE_INDEX_WORKERS"
44
+ defaultWorkers = 5
45
+ )
38
46
39
47
type Input struct {
40
48
File string // Deprecated: replaced by images
@@ -181,69 +189,114 @@ func readSnapshotSource(input []byte) (app.SnapshotSpec, error) {
181
189
return file , nil
182
190
}
183
191
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
+
184
247
func expandImageIndex (ctx context.Context , snap * app.SnapshotSpec ) {
185
248
if trace .IsEnabled () {
186
249
region := trace .StartRegion (ctx , "ec:expand-image-index" )
187
250
defer region .End ()
188
251
}
189
252
190
253
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
- }
204
254
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
+ }
210
266
211
- if ! desc .MediaType .IsIndex () {
212
- continue
213
- }
267
+ go func () {
268
+ _ = g .Wait ()
269
+ close (componentChan )
270
+ close (errorsChan )
271
+ }()
214
272
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
220
278
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
+ })
226
282
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 )
241
286
}
242
287
243
- snap .Components = components
244
-
245
288
if allErrors != nil {
246
289
log .Warnf ("Encountered error while checking for Image Index: %v" , allErrors )
247
290
}
248
291
log .Debugf ("Snap component after expanding the image index is %v" , snap .Components )
249
292
}
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
+ }
0 commit comments