Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion internal/pkg/additional/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@ import (
)

type CollectorInterface interface {
AdditionalImagesCollector(ctx context.Context) ([]v2alpha1.CopyImageSchema, error)
AdditionalImagesCollector(ctx context.Context) (v2alpha1.CollectorSchema, error)
}
123 changes: 71 additions & 52 deletions internal/pkg/additional/local_stored_collector.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,76 +47,81 @@ func (o LocalStorageCollector) destinationRegistry() string {
// AdditionalImagesCollector - this looks into the additional images field
// taking into account the mode we are in (mirrorToDisk, diskToMirror)
// the image is downloaded in oci format
func (o LocalStorageCollector) AdditionalImagesCollector(ctx context.Context) ([]v2alpha1.CopyImageSchema, error) {

func (o LocalStorageCollector) AdditionalImagesCollector(ctx context.Context) (v2alpha1.CollectorSchema, error) {
var allImages []v2alpha1.CopyImageSchema
var allErrs []error
platformFilters := make(map[string][]string)

o.Log.Debug(collectorPrefix+"setting copy option o.Opts.MultiArch=%s when collecting releases image", o.Opts.MultiArch)
for _, img := range o.Config.ImageSetConfigurationSpec.Mirror.AdditionalImages {
var src, dest, tmpSrc, tmpDest string

origin := img.Name

imgSpec, err := image.ParseRef(img.Name)
src, dest, origin, err := o.resolveAdditionalImageSrcDest(img)
if err != nil {
// OCPBUGS-33081 - skip if parse error (i.e semver and other)
o.Log.Warn("%v : SKIPPING", err)
allErrs = append(allErrs, fmt.Errorf("parse image %q: %w", img.Name, err))
allErrs = append(allErrs, err)
continue
}
o.Log.Debug(collectorPrefix+"source %s", src)
o.Log.Debug(collectorPrefix+"destination %s", dest)

if img.TargetRepo != "" && !v2alpha1.IsValidPathComponent(img.TargetRepo) {
o.Log.Warn("invalid targetRepo %s for image %s : SKIPPING", img.TargetRepo, img.Name)
allErrs = append(allErrs, fmt.Errorf("invalid targetRepo %s for image %s", img.TargetRepo, img.Name))
continue
allImages = append(allImages, v2alpha1.CopyImageSchema{
Source: src,
Destination: dest,
Origin: origin,
Type: v2alpha1.TypeGeneric,
})
if platforms := v2alpha1.ConvertPlatformsToStringSlice(img.Platforms); len(platforms) > 0 {
platformFilters[origin] = platforms
}
}
cs := v2alpha1.CollectorSchema{AllImages: allImages}
if len(platformFilters) > 0 {
cs.PlatformFilters = platformFilters
}
return cs, errors.Join(allErrs...)
}

targetRepo := imgSpec.PathComponent
if img.TargetRepo != "" {
targetRepo = img.TargetRepo
}
// resolveAdditionalImageSrcDest validates an additional image entry and returns its
// resolved source, destination and origin references ready for mirroring.
func (o LocalStorageCollector) resolveAdditionalImageSrcDest(img v2alpha1.AdditionalImage) (src, dest, origin string, err error) {
origin = img.Name

targetTag := imgSpec.Tag
if img.TargetTag != "" {
targetTag = img.TargetTag
}
imgSpec, err := image.ParseRef(img.Name)
if err != nil {
// OCPBUGS-33081 - skip if parse error (i.e semver and other)
o.Log.Warn("%v : SKIPPING", err)
return "", "", "", fmt.Errorf("parse image %q: %w", img.Name, err)
}

switch {
case o.Opts.IsMirrorToDisk(), o.Opts.IsMirrorToMirror():
tmpSrc, tmpDest = o.buildMirrorToDiskPaths(img, imgSpec, targetRepo, targetTag)
case o.Opts.IsDiskToMirror(), o.Opts.IsDelete():
tmpSrc, tmpDest = o.buildDiskToMirrorPaths(img, imgSpec, targetRepo, targetTag)
}
if img.TargetRepo != "" && !v2alpha1.IsValidPathComponent(img.TargetRepo) {
o.Log.Warn("invalid targetRepo %s for image %s : SKIPPING", img.TargetRepo, img.Name)
return "", "", "", fmt.Errorf("invalid targetRepo %s for image %s", img.TargetRepo, img.Name)
}

if tmpSrc == "" || tmpDest == "" {
o.Log.Error(collectorPrefix+"unable to determine src %s or dst %s for %s", tmpSrc, tmpDest, img.Name)
allErrs = append(allErrs, fmt.Errorf("unable to determine src %s or dst %s for %s", tmpSrc, tmpDest, img.Name))
continue
}
targetRepo, targetTag := resolveTargetRepoTag(img, imgSpec)

srcSpec, err := image.ParseRef(tmpSrc) // makes sure this ref is valid, and adds transport if needed
if err != nil {
o.Log.Error(errMsg, err.Error())
allErrs = append(allErrs, fmt.Errorf("parse source %q: %w", tmpSrc, err))
continue
}
src = srcSpec.ReferenceWithTransport

destSpec, err := image.ParseRef(tmpDest) // makes sure this ref is valid, and adds transport if needed
if err != nil {
o.Log.Error(errMsg, err.Error())
allErrs = append(allErrs, fmt.Errorf("parse destination %q: %w", tmpDest, err))
continue
}
dest = destSpec.ReferenceWithTransport
var tmpSrc, tmpDest string
switch {
case o.Opts.IsMirrorToDisk(), o.Opts.IsMirrorToMirror():
tmpSrc, tmpDest = o.buildMirrorToDiskPaths(img, imgSpec, targetRepo, targetTag)
case o.Opts.IsDiskToMirror(), o.Opts.IsDelete():
tmpSrc, tmpDest = o.buildDiskToMirrorPaths(img, imgSpec, targetRepo, targetTag)
}

o.Log.Debug(collectorPrefix+"source %s", src)
o.Log.Debug(collectorPrefix+"destination %s", dest)
if tmpSrc == "" || tmpDest == "" {
o.Log.Error(collectorPrefix+"unable to determine src %s or dst %s for %s", tmpSrc, tmpDest, img.Name)
return "", "", "", fmt.Errorf("unable to determine src %s or dst %s for %s", tmpSrc, tmpDest, img.Name)
}

allImages = append(allImages, v2alpha1.CopyImageSchema{Source: src, Destination: dest, Origin: origin, Type: v2alpha1.TypeGeneric})
srcSpec, err := image.ParseRef(tmpSrc)
if err != nil {
o.Log.Error(errMsg, err.Error())
return "", "", "", fmt.Errorf("parse source %q: %w", tmpSrc, err)
}
destSpec, err := image.ParseRef(tmpDest)
if err != nil {
o.Log.Error(errMsg, err.Error())
return "", "", "", fmt.Errorf("parse destination %q: %w", tmpDest, err)
}
return allImages, errors.Join(allErrs...)
return srcSpec.ReferenceWithTransport, destSpec.ReferenceWithTransport, origin, nil
}

// buildMirrorToDiskPaths constructs source and destination paths for mirror-to-disk and mirror-to-mirror operations
Expand Down Expand Up @@ -188,3 +193,17 @@ func (o LocalStorageCollector) buildDiskToMirrorPaths(img v2alpha1.AdditionalIma

return tmpSrc, tmpDest
}

// resolveTargetRepoTag returns the effective target repository and tag for an additional image,
// applying any overrides from the image configuration.
func resolveTargetRepoTag(img v2alpha1.AdditionalImage, imgSpec image.ImageSpec) (string, string) {
targetRepo := imgSpec.PathComponent
if img.TargetRepo != "" {
targetRepo = img.TargetRepo
}
targetTag := imgSpec.Tag
if img.TargetTag != "" {
targetTag = img.TargetTag
}
return targetRepo, targetTag
}
12 changes: 6 additions & 6 deletions internal/pkg/additional/local_stored_collector_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ func TestAdditionalImageCollector(t *testing.T) {
log.Error(" %v ", err)
t.Fatalf("should not fail")
}
assert.ElementsMatch(t, expected, res)
assert.ElementsMatch(t, expected, res.AllImages)
})

// update opts
Expand Down Expand Up @@ -148,7 +148,7 @@ func TestAdditionalImageCollector(t *testing.T) {
log.Error(" %v ", err)
t.Fatalf("should not fail")
}
assert.ElementsMatch(t, expected, res)
assert.ElementsMatch(t, expected, res.AllImages)
})

t.Run("Testing AdditionalImagesCollector : diskToMirror with generateV1Tags should use latest for images by digest", func(t *testing.T) {
Expand Down Expand Up @@ -187,7 +187,7 @@ func TestAdditionalImageCollector(t *testing.T) {
log.Error(" %v ", err)
t.Fatalf("should not fail")
}
assert.ElementsMatch(t, expected, res)
assert.ElementsMatch(t, expected, res.AllImages)
})

// should error mirrorToDisk
Expand Down Expand Up @@ -219,7 +219,7 @@ func TestAdditionalImageCollector(t *testing.T) {
res, err := ex.AdditionalImagesCollector(ctx)
// Should return error for images that failed to parse
require.Error(t, err)
assert.ElementsMatch(t, expected, res)
assert.ElementsMatch(t, expected, res.AllImages)
})

t.Run("Testing AdditionalImagesCollector : diskToMirror should collect valid images and return parse errors", func(t *testing.T) {
Expand Down Expand Up @@ -250,7 +250,7 @@ func TestAdditionalImageCollector(t *testing.T) {
res, err := ex.AdditionalImagesCollector(ctx)
// Should return error for images that failed to parse
require.Error(t, err)
assert.ElementsMatch(t, expected, res)
assert.ElementsMatch(t, expected, res.AllImages)
})
}

Expand Down Expand Up @@ -586,7 +586,7 @@ func TestAdditionalImageCollectorWithTargetRepoAndTag(t *testing.T) {
} else {
require.NoError(t, err)
}
assert.ElementsMatch(t, c.expected, res)
assert.ElementsMatch(t, c.expected, res.AllImages)
})
}
}
Expand Down
26 changes: 26 additions & 0 deletions internal/pkg/api/v2alpha1/type_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,18 @@ type Platform struct {
// Architectures defines one or more architectures
// to mirror for the release image. This is defined at the
// platform level to enable cross-channel upgrades.
//
// Deprecated: Use Platforms instead for fine-grained OS/Architecture control.
//
// For backward compatibility, if Platforms is empty and Architectures is set,
// it will be interpreted as linux/<arch> for each architecture.
Architectures []string `json:"architectures,omitempty"`
// Platforms defines one or more OS/Architecture pairs to mirror
// for multi-architecture release images. If empty and Architectures is empty,
// defaults to linux/amd64. This is defined at the platform level to enable
// cross-channel upgrades.
// Example: [{OS: "linux", Architecture: "amd64"}, {OS: "linux", Architecture: "arm64"}]
Platforms []InstancePlatformFilter `json:"platforms,omitempty"`
// This new field will allow the diskToMirror functionality
// to copy from a release location on disk
Release string `json:"release,omitempty"`
Expand All @@ -115,6 +126,9 @@ func (p Platform) DeepCopy() Platform {
platformCopy.Architectures = make([]string, len(p.Architectures))
copy(platformCopy.Architectures, p.Architectures)

platformCopy.Platforms = make([]InstancePlatformFilter, len(p.Platforms))
copy(platformCopy.Platforms, p.Platforms)

return platformCopy
}

Expand Down Expand Up @@ -183,6 +197,10 @@ type Operator struct {
// path on disk for a template to use to complete catalogSource custom resource
// generated by oc-mirror
TargetCatalogSourceTemplate string `json:"targetCatalogSourceTemplate,omitempty"`
// Platforms defines one or more OS/Architecture pairs to mirror
// for multi-architecture catalog and operator images. If empty, mirrors all platforms.
// Example: [{OS: "linux", Architecture: "amd64"}, {OS: "linux", Architecture: "arm64"}]
Platforms []InstancePlatformFilter `json:"platforms,omitempty"`
}

// GetUniqueName determines the catalog name that will
Expand Down Expand Up @@ -266,6 +284,10 @@ type Chart struct {
// ImagePaths are custom JSON paths for images location
// in the helm manifest or templates
ImagePaths []string `json:"imagePaths,omitempty"`
// Platforms defines one or more OS/Architecture pairs to mirror
// for multi-architecture images referenced by this chart. If empty, mirrors all platforms.
// Example: [{OS: "linux", Architecture: "amd64"}, {OS: "linux", Architecture: "arm64"}]
Platforms []InstancePlatformFilter `json:"platforms,omitempty"`
}

// AdditionalImage contains image pull information for additional images,
Expand All @@ -291,6 +313,10 @@ type AdditionalImage struct {
// the image will be mirrored with the provided tag in the Name
// field or a tag calculated from the partial digest.
TargetTag string `json:"targetTag,omitempty"`
// Platforms defines one or more OS/Architecture pairs to mirror
// for multi-architecture images. If empty, mirrors all platforms.
// Example: [{OS: "linux", Architecture: "amd64"}, {OS: "linux", Architecture: "arm64"}]
Platforms []InstancePlatformFilter `json:"platforms,omitempty"`
}

// GetUniqueName determines the image name that will
Expand Down
3 changes: 3 additions & 0 deletions internal/pkg/api/v2alpha1/type_internal.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,9 @@ type CollectorSchema struct {
AllImages []CopyImageSchema
CopyImageSchemaMap CopyImageSchemaMap
CatalogToFBCMap map[string]CatalogFilterResult // key is the mirror.operator.catalog
// PlatformFilters maps image origin to its platform filter list ("os/arch" strings).
// Populated by collectors; consumed by the batch worker to set InstancePlatforms per image.
PlatformFilters map[string][]string
}

type CopyImageSchemaMap struct {
Expand Down
48 changes: 48 additions & 0 deletions internal/pkg/api/v2alpha1/type_platform.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package v2alpha1
import (
"encoding/json"
"errors"
"fmt"
)

// DefaultPlatformArchitecture defines the default
Expand Down Expand Up @@ -60,3 +61,50 @@ func (pt PlatformType) validate() error {
}
return nil
}

// InstancePlatformFilter defines OS and Architecture for filtering
// multi-architecture manifest lists. This allows selecting specific
// platforms (e.g., linux/amd64, linux/arm64) when mirroring images.
type InstancePlatformFilter struct {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if a user only sets the OS or the Architecture? Should we check that we always have both? Otherwise we might get linux/ or /amd64, no?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed at 8c26134

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please mark it as resolved in case there is no further suggestions for this thread.

// OS is the operating system (e.g., "linux", "windows")
OS string `json:"os"`
// Architecture is the CPU architecture (e.g., "amd64", "arm64", "ppc64le", "s390x")
Architecture string `json:"architecture"`
}

// String returns the platform in "os/architecture" format (e.g., "linux/amd64")
func (p InstancePlatformFilter) String() string {
return p.OS + "/" + p.Architecture
}

// Validate returns an error if OS or Architecture is empty.
func (p InstancePlatformFilter) Validate() error {
if p.OS == "" {
return fmt.Errorf("platform OS must not be empty (architecture: %q)", p.Architecture)
}
if p.Architecture == "" {
return fmt.Errorf("platform architecture must not be empty (OS: %q)", p.OS)
}
return nil
}

// ConvertPlatformsToStringSlice converts a slice of InstancePlatformFilter to a slice
// of platform strings in "os/architecture" format. Returns nil if the input is empty.
// Entries with an empty OS or Architecture are skipped.
func ConvertPlatformsToStringSlice(platforms []InstancePlatformFilter) []string {
if len(platforms) == 0 {
return nil
}

platformStrs := make([]string, 0, len(platforms))
for _, p := range platforms {
if p.Validate() != nil {
continue
}
platformStrs = append(platformStrs, p.String())
}
if len(platformStrs) == 0 {
return nil
}
return platformStrs
}
11 changes: 6 additions & 5 deletions internal/pkg/archive/archive.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ func NewMirrorArchive(opts *mirror.CopyOptions, destination, iscPath, workingDir
// * docker/v2/blobs/sha256 : blobs that haven't been mirrored (diff)
// * working-dir
// * image set config
func (o *MirrorArchive) BuildArchive(ctx context.Context, collectedImages []v2alpha1.CopyImageSchema) error {
func (o *MirrorArchive) BuildArchive(ctx context.Context, schema v2alpha1.CollectorSchema) error {
if err := o.createTarball(); err != nil {
return fmt.Errorf("unable to create the mirror archive: %w", err)
}
Expand Down Expand Up @@ -97,7 +97,7 @@ func (o *MirrorArchive) BuildArchive(ctx context.Context, collectedImages []v2al
}
// ignoring the error otherwise: continuing with an empty map in blobsInHistory

addedBlobs, err := o.addImagesDiff(ctx, collectedImages, blobsInHistory)
addedBlobs, err := o.addImagesDiff(ctx, schema, blobsInHistory)
if err != nil {
return fmt.Errorf("unable to add image blobs to the archive : %w", err)
}
Expand Down Expand Up @@ -136,10 +136,11 @@ func (o *MirrorArchive) createTarball() error {
return nil
}

func (o *MirrorArchive) addImagesDiff(ctx context.Context, collectedImages []v2alpha1.CopyImageSchema, historyBlobs sets.Set[string]) (sets.Set[string], error) {
func (o *MirrorArchive) addImagesDiff(ctx context.Context, schema v2alpha1.CollectorSchema, historyBlobs sets.Set[string]) (sets.Set[string], error) {
allAddedBlobs := sets.New[string]()
for _, img := range collectedImages {
imgBlobs, err := o.blobGatherer.GatherBlobs(ctx, img.Destination)
for _, img := range schema.AllImages {
allowedPlatforms := schema.PlatformFilters[img.Origin]
imgBlobs, err := o.blobGatherer.GatherBlobs(ctx, img.Destination, allowedPlatforms)
var sigErr *SignatureBlobGathererError
if err != nil && !errors.As(err, &sigErr) {
return nil, fmt.Errorf("unable to find blobs corresponding to %s: %w", img.Destination, err)
Expand Down
Loading