Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: MULTIARCH-4974: Cluster wide architecture weighted affinity #452

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
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
51 changes: 51 additions & 0 deletions apis/multiarch/common/plugins/base_plugin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
Copyright 2025 Red Hat, Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package plugins

// +k8s:deepcopy-gen=package

// Plugins represents the plugins configuration.
type Plugins struct {
NodeAffinityScoring *NodeAffinityScoring `json:"nodeAffinityScoring,omitempty"`
// Future plugins can be added here.
}

// IBasePlugin defines a basic interface for plugins.
// +k8s:deepcopy-gen=false
type IBasePlugin interface {
// Enabled is a required boolean field.
IsEnabled() bool
// PluginName returns the name of the plugin.
Name() string
}

// BasePlugin
type BasePlugin struct {
// Enabled indicates whether the plugin is enabled.
// +kubebuilder:"validation:Required"
Enabled bool `json:"enabled" protobuf:"varint,1,opt,name=enabled" kubebuilder:"validation:Required"`
}

// Name returns the name of the BasePlugin.
func (b *BasePlugin) Name() string {
return "BasePlugin"
}

// Enabled returns the value of the Enabled field.
func (b *BasePlugin) IsEnabled() bool {
return b.Enabled
}
254 changes: 254 additions & 0 deletions apis/multiarch/common/plugins/base_plugins_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
package plugins_test

import (
"testing"

"github.com/openshift/multiarch-tuning-operator/apis/multiarch/common/plugins"
"github.com/openshift/multiarch-tuning-operator/apis/multiarch/v1alpha1"
"github.com/openshift/multiarch-tuning-operator/apis/multiarch/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func TestV1Alpha1ToV1Beta1Conversion(t *testing.T) {
// Create a v1alpha1 object
v1alpha1Obj := &v1alpha1.ClusterPodPlacementConfig{
ObjectMeta: metav1.ObjectMeta{
Name: "test-cppc",
Namespace: "default",
},
Spec: v1alpha1.ClusterPodPlacementConfigSpec{
LogVerbosity: "Normal",
NamespaceSelector: &metav1.LabelSelector{MatchLabels: map[string]string{"env": "test"}},
Plugins: plugins.Plugins{
NodeAffinityScoring: &plugins.NodeAffinityScoring{
BasePlugin: plugins.BasePlugin{
Enabled: true,
},
Platforms: []plugins.NodeAffinityScoringPlatformTerm{
{Architecture: "ppc64le", Weight: 50},
},
},
},
},
}

// Convert to v1beta1
v1beta1Obj := &v1beta1.ClusterPodPlacementConfig{}
err := v1alpha1Obj.ConvertTo(v1beta1Obj)
if err != nil {
t.Fatalf("Failed to convert v1alpha1 to v1beta1: %v", err)
}

// Validate the conversion
if v1beta1Obj.Name != v1alpha1Obj.Name {
t.Errorf("Name mismatch: expected %s, got %s", v1alpha1Obj.Name, v1beta1Obj.Name)
}

if v1beta1Obj.Spec.Plugins.NodeAffinityScoring == nil {
t.Fatalf("NodeAffinityScoring plugin is nil in v1beta1 object")
}

if v1beta1Obj.Spec.Plugins.NodeAffinityScoring.Platforms[0].Architecture != "ppc64le" {
t.Errorf("Architecture mismatch: expected %s, got %s",
"ppc64le", v1beta1Obj.Spec.Plugins.NodeAffinityScoring.Platforms[0].Architecture)
}

}

func TestV1Alpha1WithNoPluginsField(t *testing.T) {
// Create a v1alpha1 object with no plugins field
v1alpha1Obj := &v1alpha1.ClusterPodPlacementConfig{
ObjectMeta: metav1.ObjectMeta{
Name: "test-cppc-no-plugins",
Namespace: "default",
},
Spec: v1alpha1.ClusterPodPlacementConfigSpec{
LogVerbosity: "Normal",
NamespaceSelector: &metav1.LabelSelector{MatchLabels: map[string]string{"env": "test"}},
},
}

// Convert to v1beta1
v1beta1Obj := &v1beta1.ClusterPodPlacementConfig{}
err := v1alpha1Obj.ConvertTo(v1beta1Obj)
if err != nil {
t.Fatalf("Failed to convert v1beta1 to v1alpha1: %v", err)
}

// Convert v1beta1 to itself and ensure it works without modification
v1beta1ObjClone := &v1beta1.ClusterPodPlacementConfig{}
err = v1alpha1Obj.ConvertTo(v1beta1ObjClone)
if err != nil {
t.Fatalf("Failed to convert v1beta1 to v1beta1: %v", err)
}

// Validate the conversion back to v1beta1
if v1beta1ObjClone.Name != v1beta1Obj.Name {
t.Errorf("Name mismatch in v1beta1 conversion: expected %s, got %s", v1beta1Obj.Name, v1beta1ObjClone.Name)
}

if v1beta1ObjClone.Spec.Plugins != v1beta1Obj.Spec.Plugins {
t.Errorf("Expected nil plugins in v1beta1, got %v", v1beta1ObjClone.Spec.Plugins)
}
}

func TestV1Alpha1WithNodeAffinityScoringPluginDisabled(t *testing.T) {
// Create a v1alpha1 object with NodeAffinityScoring plugin set, enabled = false, no other keys
v1alpha1Obj := &v1alpha1.ClusterPodPlacementConfig{
ObjectMeta: metav1.ObjectMeta{
Name: "test-cppc-na-disabled",
Namespace: "default",
},
Spec: v1alpha1.ClusterPodPlacementConfigSpec{
LogVerbosity: "Normal",
Plugins: plugins.Plugins{
NodeAffinityScoring: &plugins.NodeAffinityScoring{
BasePlugin: plugins.BasePlugin{
Enabled: false,
},
Platforms: nil, // No additional configuration
},
},
},
}

// Convert to v1beta1
v1beta1Obj := &v1beta1.ClusterPodPlacementConfig{}
err := v1alpha1Obj.ConvertTo(v1beta1Obj)
if err != nil {
t.Fatalf("Failed to convert v1alpha1 to v1beta1: %v", err)
}

// Validate the conversion for v1beta1
if v1beta1Obj.Spec.Plugins.NodeAffinityScoring == nil {
t.Fatalf("NodeAffinityScoring plugin should not be nil in v1alpha1")
}
if v1beta1Obj.Spec.Plugins.NodeAffinityScoring.Enabled {
t.Errorf("Expected NodeAffinityScoring plugin to be disabled in v1alpha1, but got enabled")
}
if v1beta1Obj.Spec.Plugins.NodeAffinityScoring.Platforms != nil {
t.Errorf("Expected nil Platforms in v1alpha1, but got %v", v1alpha1Obj.Spec.Plugins.NodeAffinityScoring.Platforms)
}

// Convert back to v1beta1
v1beta1ObjClone := &v1beta1.ClusterPodPlacementConfig{}
err = v1alpha1Obj.ConvertTo(v1beta1ObjClone)
if err != nil {
t.Fatalf("Failed to convert v1alpha1 to v1beta1: %v", err)
}

// Validate the conversion back to v1beta1
if v1beta1ObjClone.Spec.Plugins.NodeAffinityScoring == nil {
t.Fatalf("NodeAffinityScoring plugin should not be nil in v1beta1")
}
if v1beta1ObjClone.Spec.Plugins.NodeAffinityScoring.Enabled {
t.Errorf("Expected NodeAffinityScoring plugin to be disabled in v1beta1, but got enabled")
}
if v1beta1ObjClone.Spec.Plugins.NodeAffinityScoring.Platforms != nil {
t.Errorf("Expected nil Platforms in v1beta1, but got %v", v1beta1ObjClone.Spec.Plugins.NodeAffinityScoring.Platforms)
}
}

func TestV1Alpha1WithEmptyNodeAffinityScoringPlatforms(t *testing.T) {
// Create a v1alpha1 object with empty Platforms in NodeAffinityScoring plugin
v1alpha1Obj := &v1alpha1.ClusterPodPlacementConfig{
ObjectMeta: metav1.ObjectMeta{
Name: "test-cppc-empty-platforms",
Namespace: "default",
},
Spec: v1alpha1.ClusterPodPlacementConfigSpec{
LogVerbosity: "Normal",
Plugins: plugins.Plugins{
NodeAffinityScoring: &plugins.NodeAffinityScoring{
BasePlugin: plugins.BasePlugin{
Enabled: true,
},
Platforms: []plugins.NodeAffinityScoringPlatformTerm{},
},
},
},
}

// Convert to v1beta1
v1beta1Obj := &v1beta1.ClusterPodPlacementConfig{}
err := v1alpha1Obj.ConvertTo(v1beta1Obj)
if err != nil {
t.Fatalf("Failed to convert v1alpha1 to v1beta1: %v", err)
}

// Validate the conversion for v1beta1
if v1beta1Obj.Spec.Plugins.NodeAffinityScoring == nil {
t.Fatalf("NodeAffinityScoring plugin should not be nil in v1beta1")
}
if len(v1beta1Obj.Spec.Plugins.NodeAffinityScoring.Platforms) != 0 {
t.Errorf("Expected empty Platforms in v1beta1, but got %v", v1beta1Obj.Spec.Plugins.NodeAffinityScoring.Platforms)
}

// Convert back to v1beta1cone
v1alpha1Clone := &v1alpha1.ClusterPodPlacementConfig{}
err = v1alpha1Clone.ConvertFrom(v1beta1Obj)
if err != nil {
t.Fatalf("Failed to convert v1alpha1 to v1beta1: %v", err)
}

// Validate the conversion back to v1beta1
if v1alpha1Clone.Spec.Plugins.NodeAffinityScoring == nil {
t.Fatalf("NodeAffinityScoring plugin should not be nil in v1beta1")
}
if len(v1alpha1Clone.Spec.Plugins.NodeAffinityScoring.Platforms) != 0 {
t.Errorf("Expected empty Platforms in v1beta1, but got %v", v1alpha1Clone.Spec.Plugins.NodeAffinityScoring.Platforms)
}
}

func TestV1Alpha1WithNonEmptyNodeAffinityScoringPlatforms(t *testing.T) {
// Create a v1alpha1 object with non-empty Platforms in NodeAffinityScoring plugin
v1alpha1Obj := &v1alpha1.ClusterPodPlacementConfig{
ObjectMeta: metav1.ObjectMeta{
Name: "test-cppc-nonempty-platforms",
Namespace: "default",
},
Spec: v1alpha1.ClusterPodPlacementConfigSpec{
LogVerbosity: "Normal",
Plugins: plugins.Plugins{
NodeAffinityScoring: &plugins.NodeAffinityScoring{
BasePlugin: plugins.BasePlugin{
Enabled: true,
},
Platforms: []plugins.NodeAffinityScoringPlatformTerm{
{
Architecture: "ppc64le",
Weight: 10,
},
{
Architecture: "amd64",
Weight: 20,
},
},
},
},
},
}

// Convert to v1beta1
v1beta1Obj := &v1beta1.ClusterPodPlacementConfig{}
err := v1alpha1Obj.ConvertTo(v1beta1Obj)
if err != nil {
t.Fatalf("Failed to convert v1alpha1 to v1beta1: %v", err)
}

// Validate the conversion for v1beta1
if v1beta1Obj.Spec.Plugins.NodeAffinityScoring == nil {
t.Fatalf("NodeAffinityScoring plugin should not be nil in v1beta1")
}
if len(v1beta1Obj.Spec.Plugins.NodeAffinityScoring.Platforms) != 2 {
t.Errorf("Expected 2 Platforms in v1beta1, but got %v", len(v1beta1Obj.Spec.Plugins.NodeAffinityScoring.Platforms))
}
if v1beta1Obj.Spec.Plugins.NodeAffinityScoring.Platforms[0].Architecture != "ppc64le" ||
v1beta1Obj.Spec.Plugins.NodeAffinityScoring.Platforms[0].Weight != 10 {
t.Errorf("First platform in v1beta1 does not match expected values")
}
if v1beta1Obj.Spec.Plugins.NodeAffinityScoring.Platforms[1].Architecture != "amd64" ||
v1beta1Obj.Spec.Plugins.NodeAffinityScoring.Platforms[1].Weight != 20 {
t.Errorf("Second platform in v1beta1 does not match expected values")
}
}
54 changes: 54 additions & 0 deletions apis/multiarch/common/plugins/nodeaffinityscoring_plugin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
Copyright 2025 Red Hat, Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package plugins

const (
// PluginName for NodeAffinityScoring.
NodeAffinityScoringPluginName = "NodeAffinityScoring"
)

// NodeAffinityScoring is the plugin that implements the ScorePlugin interface.
type NodeAffinityScoring struct {
BasePlugin `json:",inline"`

// Platforms is a required field and must contain at least one entry.
// +kubebuilder:"validation:Required"
Platforms []NodeAffinityScoringPlatformTerm `json:"platforms" protobuf:"bytes,2,opt,name=platforms" kubebuilder:"validation:Required"`
}

// PlatformConfig holds configuration for specific platforms, with required fields validated.
type NodeAffinityScoringPlatformTerm struct {
// Architecture must be a list of non-empty string of arch names.
// +kubebuilder:"validation:Required"
// +kubebuilder:validation:Enum=arm64;amd64;ppc64le;s390x
Architecture string `json:"architecture" protobuf:"bytes,1,rep,name=architecture" kubebuilder:"validation:Required"`

// Operating system is an optional string field.
// +optional"
// Os string `json:"os,omitempty" protobuf:"bytes,2,rep,name=os"`

// weight associated with matching the corresponding NodeAffinityScoringPlatformTerm,
// in the range 0-100.
// +kubebuilder:"validation:Required
// +kubebuilder:validation:Minimum:=0
Weight int32 `json:"weight" protobuf:"bytes,3,rep,name=weight" kubebuilder:"validation:Required,validation:Minimum:=0"`
}

// Name returns the name of the NodeAffinityScoringPluginName.
func (b *NodeAffinityScoring) Name() string {
return NodeAffinityScoringPluginName
}
Loading