Skip to content
Merged
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
20 changes: 20 additions & 0 deletions api/v1alpha1/managedcloudprofile.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,15 @@ type MachineImageUpdate struct {
ImageName string `json:"imageName"`
}

// ImageFilter defines admission criteria for source images.
type ImageFilter struct {
// RequiredFeatureSetValues lists exact values that must all be present in the
// image's feature_set OCI annotation (e.g. "_usi", "scibase"). Images missing
// any of these values are excluded from the CloudProfile entirely.
// +optional
RequiredFeatureSetValues []string `json:"requiredFeatureSetValues,omitempty"`
}

type GarbageCollectionConfig struct {
// Enabled toggles garbage collection for this image.
// +optional
Expand Down Expand Up @@ -182,6 +191,17 @@ type OCI struct {
// Insecure disables TLS
// +optional
Insecure bool `json:"insecure,omitempty"`
// ImageFilter defines criteria for filtering source images before they are
// written into the CloudProfile. Only applies when this OCI source is used
// for machine image updates (not for Kubernetes version sources).
// +optional
ImageFilter *ImageFilter `json:"imageFilter,omitempty"`
// FeatureToCapabilityMap maps raw feature_set annotation values (e.g. "_usidev")
// to boolean CloudProfile capability names (e.g. "usidev"). For each entry,
// presence of the key in the annotation produces CapabilityName: [true],
// absence produces CapabilityName: [false].
// +optional
FeatureToCapabilityMap map[string]string `json:"featureToCapabilityMap,omitempty"`
}

type MachineImageUpdateProvider struct {
Expand Down
38 changes: 35 additions & 3 deletions api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 6 additions & 2 deletions cloudprofilesync/ossync/os_image_updater.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ import (
// capabilityKeys since it is always populated automatically by the OCI source.
const ArchitectureCapability = "architecture"

// FeatureSetAnnotation is the gardenlinux OCI annotation key that carries the image's
// feature set as a comma-separated list (e.g. "sci,_usi,vhost").
const FeatureSetAnnotation = "feature_set"

type SourceImage struct {
// Version is the full tag from the registry (used as version key for legacy images).
Version string
Expand Down Expand Up @@ -67,7 +71,7 @@ type Provider interface {
Configure(cloudProfile *gardenerv1beta1.CloudProfileSpec, versions []SourceImage) error
}

func filterImages(log logr.Logger, versions []SourceImage) []SourceImage {
func validateImageVersions(log logr.Logger, versions []SourceImage) []SourceImage {
filtered := make([]SourceImage, 0, len(versions))
for _, version := range versions {
if len(version.Architectures) == 0 {
Expand Down Expand Up @@ -200,7 +204,7 @@ func (iu *ImageUpdater) Update(ctx context.Context, cpSpec *gardenerv1beta1.Clou
if err != nil {
return fmt.Errorf("failed to retrieve image versions from OCI registry: %w", err)
}
sourceImages = filterImages(iu.Log, sourceImages)
sourceImages = validateImageVersions(iu.Log, sourceImages)
Comment thread
anton-paulovich marked this conversation as resolved.
// Images from a source arrive in no guaranteed order. A changed order
// in the source images may lead to a changed order in the CloudProfile,
// causing unnecesscary reconciliations.
Comment thread
anton-paulovich marked this conversation as resolved.
Expand Down
98 changes: 98 additions & 0 deletions cloudprofilesync/ossync/source/oci/oci_internal_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company
// SPDX-License-Identifier: Apache-2.0

package oci

import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"

"github.com/cobaltcore-dev/cloud-profile-sync/api/v1alpha1"
)

var _ = Describe("splitAnnotationRaw", func() {
It("returns empty map for empty string", func() {
Expect(splitAnnotationRaw("")).To(BeEmpty())
})

It("returns empty map for whitespace-only string", func() {
Expect(splitAnnotationRaw(" , , ")).To(BeEmpty())
})

It("preserves tokens including leading underscores", func() {
result := splitAnnotationRaw("scibase,_usi,vhost")
Expect(result).To(HaveKey("scibase"))
Expect(result).To(HaveKey("_usi"))
Expect(result).To(HaveKey("vhost"))
Expect(result).To(HaveLen(3))
})

It("trims whitespace around tokens", func() {
result := splitAnnotationRaw(" scibase , _usi ")
Expect(result).To(HaveKey("scibase"))
Expect(result).To(HaveKey("_usi"))
})

It("deduplicates repeated tokens", func() {
result := splitAnnotationRaw("sci,sci,_usi")
Expect(result).To(HaveLen(2))
Expect(result).To(HaveKey("sci"))
Expect(result).To(HaveKey("_usi"))
})
})

var _ = Describe("supportsInPlaceUpdate", func() {
It("returns false when feature_set annotation is absent", func() {
Expect(supportsInPlaceUpdate(map[string]string{"architecture": "amd64"})).To(BeFalse())
})

It("returns false when feature_set annotation is present but empty", func() {
Expect(supportsInPlaceUpdate(map[string]string{"architecture": "amd64", "feature_set": ""})).To(BeFalse())
})

It("returns false when _usi is absent from feature_set", func() {
Expect(supportsInPlaceUpdate(map[string]string{"feature_set": "scibase,vhost"})).To(BeFalse())
})

It("returns false when only the normalized form 'usi' is present (not '_usi')", func() {
Expect(supportsInPlaceUpdate(map[string]string{"feature_set": "scibase,usi"})).To(BeFalse())
})

It("returns true when _usi is present in feature_set", func() {
Expect(supportsInPlaceUpdate(map[string]string{"feature_set": "scibase,_usi,vhost"})).To(BeTrue())
})
})

var _ = Describe("passesImageFilter", func() {
It("passes when filter is nil", func() {
Expect(passesImageFilter(map[string]struct{}{"sci": {}}, nil)).To(BeTrue())
})

It("passes when RequiredFeatureSetValues is empty", func() {
filter := &v1alpha1.ImageFilter{}
Expect(passesImageFilter(map[string]struct{}{"sci": {}}, filter)).To(BeTrue())
})

It("passes when all required values are present", func() {
filter := &v1alpha1.ImageFilter{RequiredFeatureSetValues: []string{"scibase", "_usi"}}
tokens := map[string]struct{}{"scibase": {}, "_usi": {}, "vhost": {}}
Expect(passesImageFilter(tokens, filter)).To(BeTrue())
})

It("fails when a required value is absent", func() {
filter := &v1alpha1.ImageFilter{RequiredFeatureSetValues: []string{"scibase", "_usi"}}
tokens := map[string]struct{}{"scibase": {}}
Expect(passesImageFilter(tokens, filter)).To(BeFalse())
})

It("fails when normalized form is present but raw form is required", func() {
filter := &v1alpha1.ImageFilter{RequiredFeatureSetValues: []string{"_usi"}}
tokens := map[string]struct{}{"usi": {}}
Expect(passesImageFilter(tokens, filter)).To(BeFalse())
})

It("fails when token set is empty", func() {
filter := &v1alpha1.ImageFilter{RequiredFeatureSetValues: []string{"scibase"}}
Expect(passesImageFilter(map[string]struct{}{}, filter)).To(BeFalse())
})
})
Loading