@@ -14,12 +14,13 @@ import (
1414 "strings"
1515
1616 "github.com/google/renameio"
17- "github.com/openshift/assisted-image-service/internal/common"
18- "github.com/openshift/assisted-image-service/pkg/isoeditor"
1917 "github.com/pkg/errors"
2018 log "github.com/sirupsen/logrus"
2119 "github.com/thoas/go-funk"
2220 "golang.org/x/sync/errgroup"
21+
22+ "github.com/openshift/assisted-image-service/internal/common"
23+ "github.com/openshift/assisted-image-service/pkg/isoeditor"
2324)
2425
2526var DefaultVersions = []map [string ]string {
@@ -279,12 +280,12 @@ func (s *rhcosStore) Populate(ctx context.Context) error {
279280 return err
280281 }
281282
283+ versions := deduplicateVersions (s .versions )
282284 errs , _ := errgroup .WithContext (ctx )
283285
284- for i := range s . versions {
285- imageInfo := s . versions [i ]
286+ for i := range versions {
287+ imageInfo := versions [i ]
286288 errs .Go (func () error {
287- openshiftVersion := imageInfo ["openshift_version" ]
288289 imageVersion := imageInfo ["version" ]
289290 arch := imageInfo ["cpu_architecture" ]
290291
@@ -293,7 +294,7 @@ func (s *rhcosStore) Populate(ctx context.Context) error {
293294 return fmt .Errorf ("failed to get image type: %v" , err )
294295 }
295296
296- fullPath := filepath .Join (s .dataDir , isoFileName (imageType , openshiftVersion , imageVersion , arch ))
297+ fullPath := filepath .Join (s .dataDir , isoFileName (imageType , imageVersion , arch ))
297298 if _ , err := os .Stat (fullPath ); os .IsNotExist (err ) {
298299 url := imageInfo ["url" ]
299300 log .Infof ("Downloading iso from %s to %s" , url , fullPath )
@@ -302,7 +303,7 @@ func (s *rhcosStore) Populate(ctx context.Context) error {
302303 if err != nil {
303304 return fmt .Errorf ("failed to download %s: %v" , url , err )
304305 }
305- log .Infof ("Finished downloading for %s-%s (%s) " , openshiftVersion , arch , imageVersion )
306+ log .Infof ("Finished downloading for %s-%s" , imageVersion , arch )
306307 if err := validateISOID (fullPath ); err != nil {
307308 message := fmt .Sprintf ("failed to validate %s: %v" , fullPath , err )
308309 if err = os .Remove (fullPath ); err != nil {
@@ -333,8 +334,8 @@ func (s *rhcosStore) Populate(ctx context.Context) error {
333334 return err
334335 }
335336
336- for i := range s . versions {
337- imageInfo := s . versions [i ]
337+ for i := range versions {
338+ imageInfo := versions [i ]
338339 openshiftVersion := imageInfo ["openshift_version" ]
339340 imageVersion := imageInfo ["version" ]
340341 arch := imageInfo ["cpu_architecture" ]
@@ -350,26 +351,26 @@ func (s *rhcosStore) Populate(ctx context.Context) error {
350351 }
351352
352353 // Extract and cache nmstatectl for all architectures
353- if err := s .extractAndCacheNmstatectl (openshiftVersion , arch ); err != nil {
354- return fmt .Errorf ("failed to extract and cache nmstatectl for %s-%s: %v" , openshiftVersion , arch , err )
354+ if err := s .extractAndCacheNmstatectl (imageInfo ); err != nil {
355+ return fmt .Errorf ("failed to extract and cache nmstatectl for %s-%s: %v" , imageVersion , arch , err )
355356 }
356357
357358 // Don't attempt to create a minimal ISO for s390x because there's no easy way to edit the kernel parameters
358359 // This means that the rootfs URL can't be added which makes it impossible for us to create a minimal ISO
359360 if arch == "s390x" {
360361 continue
361362 }
362- minimalPath := filepath .Join (s .dataDir , isoFileName (ImageTypeMinimal , openshiftVersion , imageVersion , arch ))
363+ minimalPath := filepath .Join (s .dataDir , isoFileName (ImageTypeMinimal , imageVersion , arch ))
363364 if _ , err := os .Stat (minimalPath ); os .IsNotExist (err ) {
364- log .Infof ("Creating minimal iso for %s-%s-%s" , openshiftVersion , imageVersion , arch )
365+ log .Infof ("Creating minimal iso for %s-%s" , imageVersion , arch )
365366
366- fullPath := filepath .Join (s .dataDir , isoFileName (ImageTypeFull , openshiftVersion , imageVersion , arch ))
367- rootfsURL , err := buildRootfsURL (s .imageServiceBaseURL , arch , openshiftVersion )
367+ fullPath := filepath .Join (s .dataDir , isoFileName (ImageTypeFull , imageVersion , arch ))
368+ rootfsURL , err := buildRootfsURL (s .imageServiceBaseURL , arch , imageVersion )
368369 if err != nil {
369370 return fmt .Errorf ("failed to build rootfs URL: %v" , err )
370371 }
371372
372- nmstatectlPath , err := s .NmstatectlPathForParams (openshiftVersion , arch )
373+ nmstatectlPath , err := s .NmstatectlPathForParams (imageVersion , arch )
373374 if err != nil {
374375 return err
375376 }
@@ -378,13 +379,55 @@ func (s *rhcosStore) Populate(ctx context.Context) error {
378379 return fmt .Errorf ("failed to create minimal iso template for version %s: %v" , imageInfo , err )
379380 }
380381
381- log .Infof ("Finished creating minimal iso for %s-%s (%s) " , openshiftVersion , arch , imageVersion )
382+ log .Infof ("Finished creating minimal iso for %s-%s" , imageVersion , arch )
382383 }
383384 }
384385
385386 return nil
386387}
387388
389+ // deduplicateVersions keeps a single entry per RHCOS version and CPU architecture pair.
390+ // When multiple pairs share the same RHCOS image, the entry with the highest openshift_version is kept.
391+ // It's important to keep that entry because the openshift_version is used to decide if we want the nmstatectl for that entry.
392+ func deduplicateVersions (versions []map [string ]string ) []map [string ]string {
393+ m := make (map [string ]map [string ]string , len (versions ))
394+
395+ for _ , entry := range versions {
396+ key := entry ["version" ] + "@" + entry ["cpu_architecture" ]
397+ existing , ok := m [key ]
398+ if ! ok {
399+ m [key ] = entry
400+ continue
401+ }
402+ greater , err := common .VersionGreaterOrEqual (entry ["openshift_version" ], existing ["openshift_version" ])
403+ if err != nil {
404+ log .Debugf (
405+ "Couldn't check if OS image entry for RHCOS version %s and architecture %s with openshift_version %s has a greater openshift version, keeping %s, error: %v" ,
406+ entry ["version" ], entry ["cpu_architecture" ], entry ["openshift_version" ], existing ["openshift_version" ], err ,
407+ )
408+ continue
409+ }
410+ if greater {
411+ log .Debugf (
412+ "Replacing duplicate OS image entry for RHCOS version %s and architecture %s with openshift_version %s (was %s)" ,
413+ entry ["version" ], entry ["cpu_architecture" ], entry ["openshift_version" ], existing ["openshift_version" ],
414+ )
415+ m [key ] = entry
416+ } else {
417+ log .Debugf (
418+ "Skipping duplicate OS image entry for RHCOS version %s and architecture %s (openshift_version %s, keeping %s)" ,
419+ entry ["version" ], entry ["cpu_architecture" ], entry ["openshift_version" ], existing ["openshift_version" ],
420+ )
421+ }
422+ }
423+
424+ result := make ([]map [string ]string , 0 , len (m ))
425+ for _ , entry := range m {
426+ result = append (result , entry )
427+ }
428+ return result
429+ }
430+
388431func (* rhcosStore ) getImageType (imageInfo map [string ]string ) (string , error ) {
389432 validTypes := map [string ]bool {
390433 ImageTypeFull : true ,
@@ -405,32 +448,36 @@ func (*rhcosStore) getImageType(imageInfo map[string]string) (string, error) {
405448
406449// extractAndCacheNmstatectl extracts nmstatectl from the rootfs and caches it for later use
407450// This ensures nmstatectl is available for both minimal ISO creation and PXE provisioning
408- func (s * rhcosStore ) extractAndCacheNmstatectl (openshiftVersion , arch string ) error {
451+ func (s * rhcosStore ) extractAndCacheNmstatectl (imageInfo map [string ]string ) error {
452+ openshiftVersion := imageInfo ["openshift_version" ]
453+ imageVersion := imageInfo ["version" ]
454+ arch := imageInfo ["cpu_architecture" ]
455+
409456 // Check if version supports nmstatectl
410457 versionOK , err := common .VersionGreaterOrEqual (openshiftVersion , isoeditor .MinimalVersionForNmstatectl )
411458 if err != nil {
412459 return err
413460 }
414461 if ! versionOK {
415- log .Debugf ("Skipping nmstatectl extraction for %s-%s (version does not support nmstatectl)" , openshiftVersion , arch )
462+ log .Debugf ("Skipping nmstatectl extraction for %s-%s (version does not support nmstatectl)" , imageVersion , arch )
416463 return nil
417464 }
418465
419- nmstatectlPath , err := s .NmstatectlPathForParams (openshiftVersion , arch )
466+ nmstatectlPath , err := s .NmstatectlPathForParams (imageVersion , arch )
420467 if err != nil {
421468 return err
422469 }
423470
424471 // Check if nmstatectl is already cached
425472 if _ , err = os .Stat (nmstatectlPath ); err == nil {
426- log .Debugf ("nmstatectl already cached for %s-%s" , openshiftVersion , arch )
473+ log .Debugf ("nmstatectl already cached for %s-%s" , imageVersion , arch )
427474 return nil
428475 }
429476
430- log .Infof ("Extracting and caching nmstatectl for %s-%s" , openshiftVersion , arch )
477+ log .Infof ("Extracting and caching nmstatectl for %s-%s" , imageVersion , arch )
431478
432479 // Get the path to the full ISO
433- fullPath := s .PathForParams (ImageTypeFull , openshiftVersion , arch )
480+ fullPath := s .PathForParams (ImageTypeFull , imageVersion , arch )
434481
435482 // Read rootfs directly from ISO
436483 rootfsFile , err := isoeditor .GetFileFromISO (fullPath , isoeditor .RootfsImagePath )
@@ -475,22 +522,41 @@ func (s *rhcosStore) extractAndCacheNmstatectl(openshiftVersion, arch string) er
475522 return fmt .Errorf ("failed to cache nmstatectl: %v" , err )
476523 }
477524
478- log .Infof ("Successfully cached nmstatectl for %s-%s" , openshiftVersion , arch )
525+ log .Infof ("Successfully cached nmstatectl for %s-%s" , imageVersion , arch )
479526 return nil
480527}
481528
482- func (s * rhcosStore ) PathForParams (imageType , openshiftVersion , arch string ) string {
483- var version string
529+ func (s * rhcosStore ) findVersionEntry (versionKey , arch string ) map [string ]string {
484530 for _ , entry := range s .versions {
485- if entry ["openshift_version" ] == openshiftVersion && entry ["cpu_architecture" ] == arch {
486- version = entry ["version" ]
531+ if entry ["cpu_architecture" ] != arch {
532+ continue
533+ }
534+ if entry ["version" ] == versionKey {
535+ return entry
487536 }
488537 }
489- return filepath .Join (s .dataDir , isoFileName (imageType , openshiftVersion , version , arch ))
538+ for _ , entry := range s .versions {
539+ if entry ["cpu_architecture" ] != arch {
540+ continue
541+ }
542+ if entry ["openshift_version" ] == versionKey {
543+ return entry
544+ }
545+ }
546+ return nil
490547}
491548
492- func isoFileName (imageType , openshiftVersion , version , arch string ) string {
493- return fmt .Sprintf ("rhcos-%s-%s-%s-%s.iso" , imageType , openshiftVersion , version , arch )
549+ func (s * rhcosStore ) PathForParams (imageType , versionKey , arch string ) string {
550+ rhcosVersion := versionKey
551+ entry := s .findVersionEntry (versionKey , arch )
552+ if entry != nil {
553+ rhcosVersion = entry ["version" ]
554+ }
555+ return filepath .Join (s .dataDir , isoFileName (imageType , rhcosVersion , arch ))
556+ }
557+
558+ func isoFileName (imageType , version , arch string ) string {
559+ return fmt .Sprintf ("rhcos-%s-%s-%s.iso" , imageType , version , arch )
494560}
495561
496562func buildRootfsURL (baseURL , arch , version string ) (string , error ) {
@@ -515,9 +581,9 @@ func (s *rhcosStore) cleanDataDir() error {
515581 var expectedFiles []string
516582 for _ , version := range s .versions {
517583 // Only add full isos here as we want to regenerate the minimal image on each deploy
518- expectedFiles = append (expectedFiles , isoFileName (ImageTypeFull , version ["openshift_version" ], version [ " version" ], version ["cpu_architecture" ]))
584+ expectedFiles = append (expectedFiles , isoFileName (ImageTypeFull , version ["version" ], version ["cpu_architecture" ]))
519585 // Keep nmstatectl cached files for all architectures (including s390x)
520- expectedFiles = append (expectedFiles , nmstatectlFileName (version ["openshift_version" ], version [ " version" ], version ["cpu_architecture" ]))
586+ expectedFiles = append (expectedFiles , nmstatectlFileName (version ["version" ], version ["cpu_architecture" ]))
521587 }
522588
523589 dataDirFiles , err := os .ReadDir (s .dataDir )
@@ -539,24 +605,16 @@ func (s *rhcosStore) cleanDataDir() error {
539605}
540606
541607func (s * rhcosStore ) HaveVersion (version , arch string ) bool {
542- for _ , entry := range s .versions {
543- v , versionPresent := entry ["openshift_version" ]
544- a , archPresent := entry ["cpu_architecture" ]
545- if versionPresent && v == version && archPresent && a == arch {
546- return true
547- }
548- }
549- return false
608+ return s .findVersionEntry (version , arch ) != nil
550609}
551610
552- func (s * rhcosStore ) NmstatectlPathForParams (openshiftVersion , arch string ) (string , error ) {
553- var version string
554- for _ , entry := range s .versions {
555- if entry ["openshift_version" ] == openshiftVersion && entry ["cpu_architecture" ] == arch {
556- version = entry ["version" ]
557- }
611+ func (s * rhcosStore ) NmstatectlPathForParams (versionKey , arch string ) (string , error ) {
612+ rhcosVersion := versionKey
613+ entry := s .findVersionEntry (versionKey , arch )
614+ if entry != nil {
615+ rhcosVersion = entry ["version" ]
558616 }
559- nmstatectlPath := filepath .Join (s .dataDir , nmstatectlFileName (openshiftVersion , version , arch ))
617+ nmstatectlPath := filepath .Join (s .dataDir , nmstatectlFileName (rhcosVersion , arch ))
560618
561619 // Safety check: ensures nmstatectlPath stays within the expected dataDir,
562620 // preventing crafted inputs from escaping the intended directory
@@ -566,6 +624,6 @@ func (s *rhcosStore) NmstatectlPathForParams(openshiftVersion, arch string) (str
566624 return nmstatectlPath , nil
567625}
568626
569- func nmstatectlFileName (openshiftVersion , version , arch string ) string {
570- return fmt .Sprintf ("nmstatectl-%s-%s-%s" , openshiftVersion , version , arch )
627+ func nmstatectlFileName (version , arch string ) string {
628+ return fmt .Sprintf ("nmstatectl-%s-%s" , version , arch )
571629}
0 commit comments