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
55 changes: 55 additions & 0 deletions cloudprofilesync/ossync/os_image_updater.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// ArchitectureCapability is the well-known Gardener capability key for CPU architecture.
// It is read from the "architecture" OCI annotation and excluded from the user-configured
// capabilityKeys since it is always populated automatically by the OCI source.
const ArchitectureCapability = "architecture"

type SourceImage struct {
// Version is the full tag from the registry (used as version key for legacy images).
Version string
Expand Down Expand Up @@ -158,6 +163,51 @@ func inPlaceUpdates(supported bool) *gardenerv1beta1.InPlaceUpdates {
return &gardenerv1beta1.InPlaceUpdates{Supported: true}
}

// allowedCapabilityValues builds a per-key set of allowed values from the
// MachineCapabilities declared in the CloudProfile spec. Only values present
// here will be written into capabilityFlavors.
func allowedCapabilityValues(caps []gardenerv1beta1.CapabilityDefinition) map[string]map[string]struct{} {
allowed := make(map[string]map[string]struct{}, len(caps))
for _, cap := range caps {
vals := make(map[string]struct{}, len(cap.Values))
for _, v := range cap.Values {
vals[v] = struct{}{}
}
allowed[cap.Name] = vals
}
return allowed
}

// filterCapabilities returns a copy of caps filtered to only the key/value pairs
// declared in allowed. Keys or values absent from allowed are dropped.
// Returns nil if allowed is empty — MachineCapabilities not configured means
// no capabilities should be written to the CloudProfile.
func filterCapabilities(caps gardenerv1beta1.Capabilities, allowed map[string]map[string]struct{}) gardenerv1beta1.Capabilities {
if len(allowed) == 0 {
return nil
}
result := make(gardenerv1beta1.Capabilities, len(caps))
for key, values := range caps {
allowedVals, ok := allowed[key]
if !ok {
continue
}
var filtered []string
for _, v := range values {
if _, ok := allowedVals[v]; ok {
filtered = append(filtered, v)
}
}
if len(filtered) > 0 {
result[key] = filtered
}
}
if len(result) == 0 {
return nil
}
return result
}

func (iu *ImageUpdater) Update(ctx context.Context, cpSpec *gardenerv1beta1.CloudProfileSpec) error {
sourceImages, err := iu.Source.GetVersions(ctx)
if err != nil {
Expand All @@ -173,6 +223,11 @@ func (iu *ImageUpdater) Update(ctx context.Context, cpSpec *gardenerv1beta1.Clou
}
return cmp.Compare(a.Version, b.Version)
})

allowed := allowedCapabilityValues(cpSpec.MachineCapabilities)
for i := range sourceImages {
sourceImages[i].Capabilities = filterCapabilities(sourceImages[i].Capabilities, allowed)
}
imageIndex := slices.IndexFunc(cpSpec.MachineImages, func(img gardenerv1beta1.MachineImage) bool {
return img.Name == iu.ImageName
})
Expand Down
176 changes: 156 additions & 20 deletions cloudprofilesync/ossync/os_image_updater_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ var _ = Describe("filterImages", func() {
Version: "1877.9.2.0-metal-sci-pxe-amd64",
CleanVersion: "1877.9.2",
Architectures: []string{"amd64"},
Capabilities: gardencorev1beta1.Capabilities{"architecture": {"amd64"}, "feature": {"sci", "_pxe"}},
Capabilities: gardencorev1beta1.Capabilities{"architecture": {"amd64"}, "feature_set": {"sci", "pxe"}},
},
})
Expect(result).To(HaveLen(1))
Expand All @@ -67,7 +67,7 @@ var _ = Describe("filterImages", func() {
Version: "2254.0.0-baremetal-sci-usi-amd64",
CleanVersion: "2254.0.0",
Architectures: []string{"amd64"},
Capabilities: gardencorev1beta1.Capabilities{"architecture": {"amd64"}, "feature": {"sci", "_usi"}},
Capabilities: gardencorev1beta1.Capabilities{"architecture": {"amd64"}, "feature_set": {"sci", "usi"}},
},
})
Expect(result).To(HaveLen(2))
Expand All @@ -89,7 +89,7 @@ var _ = Describe("filterImages", func() {
Version: "1921.0.0-metal-sci-usi-amd64",
CleanVersion: "1921.0",
Architectures: []string{"amd64"},
Capabilities: gardencorev1beta1.Capabilities{"architecture": {"amd64"}, "feature": {"sci", "_usi"}},
Capabilities: gardencorev1beta1.Capabilities{"architecture": {"amd64"}, "feature_set": {"sci", "usi"}},
},
})
Expect(result).To(HaveLen(2))
Expand Down Expand Up @@ -197,7 +197,7 @@ var _ = Describe("ImageUpdater", func() {
Version: "2254.0.0-baremetal-sci-usi-amd64",
CleanVersion: "2254.0.0",
Architectures: []string{"amd64"},
Capabilities: gardencorev1beta1.Capabilities{"architecture": {"amd64"}, "feature": {"sci", "_usi"}},
Capabilities: gardencorev1beta1.Capabilities{"architecture": {"amd64"}, "feature_set": {"sci", "usi"}},
},
}
updater := ossync.ImageUpdater{
Expand Down Expand Up @@ -248,13 +248,25 @@ var _ = Describe("ImageUpdater", func() {
})

Describe("flag ON (dual-write clean version)", func() {
// cpSpecWithCaps returns a CloudProfileSpec with MachineCapabilities covering
// the keys used by the test source images. Required because filterCapabilities
// returns nil when MachineCapabilities is empty.
cpSpecWithCaps := func() gardencorev1beta1.CloudProfileSpec {
return gardencorev1beta1.CloudProfileSpec{
MachineCapabilities: []gardencorev1beta1.CapabilityDefinition{
{Name: "architecture", Values: []string{"amd64", "arm64"}},
{Name: "feature_set", Values: []string{"sci", "usi", "pxe", "scibase"}},
},
}
}

It("sets CapabilityFlavors when CleanVersion equals Version (semver tag with matching annotation)", func(ctx SpecContext) {
mockSource.images = []ossync.SourceImage{
{
Version: "2254.0.0",
CleanVersion: "2254.0.0",
Architectures: []string{"amd64"},
Capabilities: gardencorev1beta1.Capabilities{"architecture": {"amd64"}, "feature": {"sci", "_usi"}},
Capabilities: gardencorev1beta1.Capabilities{"architecture": {"amd64"}, "feature_set": {"sci", "usi"}},
},
}
updater := ossync.ImageUpdater{
Expand All @@ -263,15 +275,15 @@ var _ = Describe("ImageUpdater", func() {
ImageName: "test",
EnableCapabilities: true,
}
var cpSpec gardencorev1beta1.CloudProfileSpec
cpSpec := cpSpecWithCaps()
Expect(updater.Update(ctx, &cpSpec)).To(Succeed())

Expect(cpSpec.MachineImages[0].Versions).To(HaveLen(1))
v := cpSpec.MachineImages[0].Versions[0]
Expect(v.Version).To(Equal("2254.0.0"))
Expect(v.CapabilityFlavors).To(HaveLen(1))
Expect(v.CapabilityFlavors[0].Capabilities).To(Equal(
gardencorev1beta1.Capabilities{"architecture": {"amd64"}, "feature": {"sci", "_usi"}},
gardencorev1beta1.Capabilities{"architecture": {"amd64"}, "feature_set": {"sci", "usi"}},
))
})

Expand All @@ -281,7 +293,7 @@ var _ = Describe("ImageUpdater", func() {
Version: "2254.0.0-baremetal-sci-usi-amd64",
CleanVersion: "2254.0.0",
Architectures: []string{"amd64"},
Capabilities: gardencorev1beta1.Capabilities{"architecture": {"amd64"}, "feature": {"sci", "_usi"}},
Capabilities: gardencorev1beta1.Capabilities{"architecture": {"amd64"}, "feature_set": {"sci", "usi"}},
},
}
updater := ossync.ImageUpdater{
Expand All @@ -290,7 +302,7 @@ var _ = Describe("ImageUpdater", func() {
ImageName: "test",
EnableCapabilities: true,
}
var cpSpec gardencorev1beta1.CloudProfileSpec
cpSpec := cpSpecWithCaps()
Expect(updater.Update(ctx, &cpSpec)).To(Succeed())

versions := cpSpec.MachineImages[0].Versions
Expand All @@ -304,7 +316,7 @@ var _ = Describe("ImageUpdater", func() {
Expect(cleanEntry).NotTo(BeNil())
Expect(cleanEntry.CapabilityFlavors).To(HaveLen(1))
Expect(cleanEntry.CapabilityFlavors[0].Capabilities).To(Equal(
gardencorev1beta1.Capabilities{"architecture": {"amd64"}, "feature": {"sci", "_usi"}},
gardencorev1beta1.Capabilities{"architecture": {"amd64"}, "feature_set": {"sci", "usi"}},
))
})

Expand All @@ -314,13 +326,13 @@ var _ = Describe("ImageUpdater", func() {
Version: "2254.0.0-baremetal-sci-usi-amd64",
CleanVersion: "2254.0.0",
Architectures: []string{"amd64"},
Capabilities: gardencorev1beta1.Capabilities{"architecture": {"amd64"}, "feature": {"sci", "_usi"}},
Capabilities: gardencorev1beta1.Capabilities{"architecture": {"amd64"}, "feature_set": {"sci", "usi"}},
},
{
Version: "2254.0.0-baremetal-sci-pxe-amd64",
CleanVersion: "2254.0.0",
Architectures: []string{"amd64"},
Capabilities: gardencorev1beta1.Capabilities{"architecture": {"amd64"}, "feature": {"sci", "_pxe"}},
Capabilities: gardencorev1beta1.Capabilities{"architecture": {"amd64"}, "feature_set": {"sci", "pxe"}},
},
}
updater := ossync.ImageUpdater{
Expand All @@ -329,7 +341,7 @@ var _ = Describe("ImageUpdater", func() {
ImageName: "test",
EnableCapabilities: true,
}
var cpSpec gardencorev1beta1.CloudProfileSpec
cpSpec := cpSpecWithCaps()
Expect(updater.Update(ctx, &cpSpec)).To(Succeed())

versions := cpSpec.MachineImages[0].Versions
Expand All @@ -347,8 +359,8 @@ var _ = Describe("ImageUpdater", func() {
cleanEntry.CapabilityFlavors[1].Capabilities,
}
Expect(flavors).To(ConsistOf(
gardencorev1beta1.Capabilities{"architecture": {"amd64"}, "feature": {"sci", "_usi"}},
gardencorev1beta1.Capabilities{"architecture": {"amd64"}, "feature": {"sci", "_pxe"}},
gardencorev1beta1.Capabilities{"architecture": {"amd64"}, "feature_set": {"sci", "usi"}},
gardencorev1beta1.Capabilities{"architecture": {"amd64"}, "feature_set": {"sci", "pxe"}},
))
})

Expand All @@ -358,7 +370,7 @@ var _ = Describe("ImageUpdater", func() {
Version: "2254.0.0-baremetal-sci-usi-amd64",
CleanVersion: "2254.0.0",
Architectures: []string{"amd64"},
Capabilities: gardencorev1beta1.Capabilities{"architecture": {"amd64"}, "feature": {"sci", "_usi"}},
Capabilities: gardencorev1beta1.Capabilities{"architecture": {"amd64"}, "feature_set": {"sci", "usi"}},
},
}
updater := ossync.ImageUpdater{
Expand All @@ -367,7 +379,7 @@ var _ = Describe("ImageUpdater", func() {
ImageName: "test",
EnableCapabilities: true,
}
var cpSpec gardencorev1beta1.CloudProfileSpec
cpSpec := cpSpecWithCaps()
Expect(updater.Update(ctx, &cpSpec)).To(Succeed())
Expect(updater.Update(ctx, &cpSpec)).To(Succeed())

Expand Down Expand Up @@ -418,7 +430,7 @@ var _ = Describe("ImageUpdater", func() {
Version: "2254.0.0-baremetal-sci-usi-amd64",
CleanVersion: "2254.0.0",
Architectures: []string{"amd64"},
Capabilities: gardencorev1beta1.Capabilities{"architecture": {"amd64"}, "feature": {"sci", "_usi"}},
Capabilities: gardencorev1beta1.Capabilities{"architecture": {"amd64"}, "feature_set": {"sci", "usi"}},
SupportInPlaceUpdate: true,
},
}
Expand All @@ -444,7 +456,7 @@ var _ = Describe("ImageUpdater", func() {
Version: "2254.0.0-baremetal-sci-usi-amd64",
CleanVersion: "2254.0.0",
Architectures: []string{"amd64"},
Capabilities: gardencorev1beta1.Capabilities{"architecture": {"amd64"}, "feature": {"sci", "_usi"}},
Capabilities: gardencorev1beta1.Capabilities{"architecture": {"amd64"}, "feature_set": {"sci", "usi"}},
},
}
updater := ossync.ImageUpdater{
Expand All @@ -465,7 +477,7 @@ var _ = Describe("ImageUpdater", func() {
Version: "1877.9.2.0-metal-sci-pxe-amd64-1877-9-2-6bb2b442",
CleanVersion: "1877.9.2",
Architectures: []string{"amd64"},
Capabilities: gardencorev1beta1.Capabilities{"architecture": {"amd64"}, "feature": {"sci", "_pxe"}},
Capabilities: gardencorev1beta1.Capabilities{"architecture": {"amd64"}, "feature_set": {"sci", "pxe"}},
},
}
updater := ossync.ImageUpdater{
Expand Down Expand Up @@ -531,6 +543,130 @@ var _ = Describe("ImageUpdater", func() {
})
})

Describe("capability filtering via MachineCapabilities", func() {
It("writes no CapabilityFlavors when MachineCapabilities is empty", func(ctx SpecContext) {
mockSource.images = []ossync.SourceImage{{
Version: "2254.0.0",
CleanVersion: "2254.0.0",
Architectures: []string{"amd64"},
Capabilities: gardencorev1beta1.Capabilities{
"architecture": {"amd64"},
"feature_set": {"sci", "usi"},
"hypervisor": {"kvm"},
},
}}
updater := ossync.ImageUpdater{
Log: GinkgoLogr,
Source: &mockSource,
ImageName: "test",
EnableCapabilities: true,
}
var cpSpec gardencorev1beta1.CloudProfileSpec
Expect(updater.Update(ctx, &cpSpec)).To(Succeed())
v := cpSpec.MachineImages[0].Versions[0]
Expect(v.CapabilityFlavors).To(BeEmpty())
})

It("drops capability values absent from MachineCapabilities", func(ctx SpecContext) {
mockSource.images = []ossync.SourceImage{{
Version: "2254.0.0",
CleanVersion: "2254.0.0",
Architectures: []string{"amd64"},
Capabilities: gardencorev1beta1.Capabilities{
"architecture": {"amd64"},
"feature_set": {"sci", "usi", "rescue"},
},
}}
updater := ossync.ImageUpdater{
Log: GinkgoLogr,
Source: &mockSource,
ImageName: "test",
EnableCapabilities: true,
}
cpSpec := gardencorev1beta1.CloudProfileSpec{
MachineCapabilities: []gardencorev1beta1.CapabilityDefinition{
{Name: "architecture", Values: []string{"amd64", "arm64"}},
{Name: "feature_set", Values: []string{"sci", "usi"}},
},
}
Expect(updater.Update(ctx, &cpSpec)).To(Succeed())
v := cpSpec.MachineImages[0].Versions[0]
Expect(v.CapabilityFlavors).To(HaveLen(1))
Expect(v.CapabilityFlavors[0].Capabilities).To(Equal(gardencorev1beta1.Capabilities{
"architecture": {"amd64"},
"feature_set": {"sci", "usi"}, // "rescue" dropped — not in MachineCapabilities
}))
})

It("populates all declared capability keys into capabilityFlavors", func(ctx SpecContext) {
mockSource.images = []ossync.SourceImage{{
Version: "2254.0.0",
CleanVersion: "2254.0.0",
Architectures: []string{"amd64"},
Capabilities: gardencorev1beta1.Capabilities{
"architecture": {"amd64"},
"feature_set": {"sci", "usi"},
"hypervisor": {"kvm"},
},
}}
updater := ossync.ImageUpdater{
Log: GinkgoLogr,
Source: &mockSource,
ImageName: "test",
EnableCapabilities: true,
}
cpSpec := gardencorev1beta1.CloudProfileSpec{
MachineCapabilities: []gardencorev1beta1.CapabilityDefinition{
{Name: "architecture", Values: []string{"amd64", "arm64"}},
{Name: "feature_set", Values: []string{"sci", "usi", "scibase"}},
{Name: "hypervisor", Values: []string{"kvm", "xen"}},
},
}
Expect(updater.Update(ctx, &cpSpec)).To(Succeed())
v := cpSpec.MachineImages[0].Versions[0]
Expect(v.CapabilityFlavors).To(HaveLen(1))
Expect(v.CapabilityFlavors[0].Capabilities).To(Equal(gardencorev1beta1.Capabilities{
"architecture": {"amd64"},
"feature_set": {"sci", "usi"},
"hypervisor": {"kvm"},
}))
})

It("drops a capability key entirely when none of its values are in MachineCapabilities", func(ctx SpecContext) {
mockSource.images = []ossync.SourceImage{{
Version: "2254.0.0",
CleanVersion: "2254.0.0",
Architectures: []string{"amd64"},
Capabilities: gardencorev1beta1.Capabilities{
"architecture": {"amd64"},
"feature_set": {"sci", "usi"},
"hypervisor": {"kvm"},
},
}}
updater := ossync.ImageUpdater{
Log: GinkgoLogr,
Source: &mockSource,
ImageName: "test",
EnableCapabilities: true,
}
cpSpec := gardencorev1beta1.CloudProfileSpec{
MachineCapabilities: []gardencorev1beta1.CapabilityDefinition{
{Name: "architecture", Values: []string{"amd64", "arm64"}},
{Name: "feature_set", Values: []string{"sci", "usi"}},
// hypervisor not declared → key dropped entirely
},
}
Expect(updater.Update(ctx, &cpSpec)).To(Succeed())
v := cpSpec.MachineImages[0].Versions[0]
Expect(v.CapabilityFlavors).To(HaveLen(1))
Expect(v.CapabilityFlavors[0].Capabilities).To(Equal(gardencorev1beta1.Capabilities{
"architecture": {"amd64"},
"feature_set": {"sci", "usi"},
}))
Expect(v.CapabilityFlavors[0].Capabilities).NotTo(HaveKey("hypervisor"))
})
})

Describe("expiration", func() {
deprecated := gardencorev1beta1.ClassificationDeprecated

Expand Down
Loading