Add Capacity Buffer controller logic
This commit is contained in:
parent
7b9cb8c8ba
commit
fe61e262fd
|
|
@ -97,9 +97,9 @@ type ResourceList map[ResourceName]resource.Quantity
|
||||||
// CapacityBufferSpec defines the desired state of CapacityBuffer.
|
// CapacityBufferSpec defines the desired state of CapacityBuffer.
|
||||||
type CapacityBufferSpec struct {
|
type CapacityBufferSpec struct {
|
||||||
// ProvisioningStrategy defines how the buffer is utilized.
|
// ProvisioningStrategy defines how the buffer is utilized.
|
||||||
// "active-capacity" is the default strategy, where the buffer actively scales up the cluster by creating placeholder pods.
|
// "buffer.x-k8s.io/active-capacity" is the default strategy, where the buffer actively scales up the cluster by creating placeholder pods.
|
||||||
// +kubebuilder:validation:Enum=active-capacity
|
// +kubebuilder:validation:Enum=buffer.x-k8s.io/active-capacity
|
||||||
// +kubebuilder:default="active-capacity"
|
// +kubebuilder:default="buffer.x-k8s.io/active-capacity"
|
||||||
// +optional
|
// +optional
|
||||||
ProvisioningStrategy *string `json:"provisioningStrategy,omitempty" protobuf:"bytes,1,opt,name=provisioningStrategy"`
|
ProvisioningStrategy *string `json:"provisioningStrategy,omitempty" protobuf:"bytes,1,opt,name=provisioningStrategy"`
|
||||||
|
|
||||||
|
|
@ -123,24 +123,18 @@ type CapacityBufferSpec struct {
|
||||||
// If neither `replicas` nor `percentage` is set, as many chunks as fit within
|
// If neither `replicas` nor `percentage` is set, as many chunks as fit within
|
||||||
// defined resource limits (if any) will be created. If both are set, the maximum
|
// defined resource limits (if any) will be created. If both are set, the maximum
|
||||||
// of the two will be used.
|
// of the two will be used.
|
||||||
// This field is mutually exclusive with `percentage` when `scalableRef` is set.
|
|
||||||
// +optional
|
// +optional
|
||||||
// +kubebuilder:validation:Minimum=0
|
// +kubebuilder:validation:Minimum=0
|
||||||
// +kubebuilder:validation:ExclusiveMinimum=false
|
// +kubebuilder:validation:ExclusiveMinimum=false
|
||||||
// +kubebuilder:validation:Xor=replicas,percentage
|
|
||||||
Replicas *int32 `json:"replicas,omitempty" protobuf:"varint,4,opt,name=replicas"`
|
Replicas *int32 `json:"replicas,omitempty" protobuf:"varint,4,opt,name=replicas"`
|
||||||
|
|
||||||
// Percentage defines the desired buffer capacity as a percentage of the
|
// Percentage defines the desired buffer capacity as a percentage of the
|
||||||
// `scalableRef`'s current replicas. This is only applicable if `scalableRef` is set.
|
// `scalableRef`'s current replicas. This is only applicable if `scalableRef` is set.
|
||||||
// The absolute number of replicas is calculated from the percentage by rounding up to a minimum of 1.
|
// The absolute number of replicas is calculated from the percentage by rounding up to a minimum of 1.
|
||||||
// For example, if `scalableRef` has 10 replicas and `percentage` is 20, 2 buffer chunks will be created.
|
// For example, if `scalableRef` has 10 replicas and `percentage` is 20, 2 buffer chunks will be created.
|
||||||
// This field is mutually exclusive with `replicas`.
|
|
||||||
// +optional
|
// +optional
|
||||||
// +kubebuilder:validation:Minimum=0
|
// +kubebuilder:validation:Minimum=0
|
||||||
// +kubebuilder:validation:Maximum=100
|
|
||||||
// +kubebuilder:validation:ExclusiveMaximum=false
|
|
||||||
// +kubebuilder:validation:ExclusiveMinimum=false
|
// +kubebuilder:validation:ExclusiveMinimum=false
|
||||||
// +kubebuilder:validation:Xor=replicas,percentage
|
|
||||||
Percentage *int32 `json:"percentage,omitempty" protobuf:"varint,5,opt,name=percentage"`
|
Percentage *int32 `json:"percentage,omitempty" protobuf:"varint,5,opt,name=percentage"`
|
||||||
|
|
||||||
// Limits, if specified, will limit the number of chunks created for this buffer
|
// Limits, if specified, will limit the number of chunks created for this buffer
|
||||||
|
|
|
||||||
|
|
@ -100,12 +100,12 @@ spec:
|
||||||
- name
|
- name
|
||||||
type: object
|
type: object
|
||||||
provisioningStrategy:
|
provisioningStrategy:
|
||||||
default: active-capacity
|
default: buffer.x-k8s.io/active-capacity
|
||||||
description: |-
|
description: |-
|
||||||
ProvisioningStrategy defines how the buffer is utilized.
|
ProvisioningStrategy defines how the buffer is utilized.
|
||||||
"active-capacity" is the default strategy, where the buffer actively scales up the cluster by creating placeholder pods.
|
"buffer.x-k8s.io/active-capacity" is the default strategy, where the buffer actively scales up the cluster by creating placeholder pods.
|
||||||
enum:
|
enum:
|
||||||
- active-capacity
|
- buffer.x-k8s.io/active-capacity
|
||||||
type: string
|
type: string
|
||||||
replicas:
|
replicas:
|
||||||
description: |-
|
description: |-
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
/*
|
||||||
|
Copyright 2025 The Kubernetes Authors.
|
||||||
|
|
||||||
|
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 common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
v1 "k8s.io/autoscaler/cluster-autoscaler/apis/capacitybuffer/autoscaling.x-k8s.io/v1"
|
||||||
|
client "k8s.io/autoscaler/cluster-autoscaler/apis/capacitybuffer/client/clientset/versioned"
|
||||||
|
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/client-go/kubernetes"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Constants to use in Capacity Buffers objects
|
||||||
|
const (
|
||||||
|
ActiveProvisioningStrategy = "buffer.x-k8s.io/active-capacity"
|
||||||
|
ReadyForProvisioningCondition = "ReadyForProvisioning"
|
||||||
|
ProvisioningCondition = "Provisioning"
|
||||||
|
ConditionTrue = "True"
|
||||||
|
ConditionFalse = "False"
|
||||||
|
DefaultNamespace = "default"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CreatePodTemplate creates a pod template object by calling API server
|
||||||
|
func CreatePodTemplate(client *kubernetes.Clientset, podTemplate *corev1.PodTemplate) (*corev1.PodTemplate, error) {
|
||||||
|
return client.CoreV1().PodTemplates(DefaultNamespace).Create(context.TODO(), podTemplate, metav1.CreateOptions{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateBufferStatus updates the passed buffer object with its defined status
|
||||||
|
func UpdateBufferStatus(buffersClient client.Interface, buffer *v1.CapacityBuffer) error {
|
||||||
|
_, err := buffersClient.AutoscalingV1().CapacityBuffers(DefaultNamespace).UpdateStatus(context.TODO(), buffer, metav1.UpdateOptions{})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,143 @@
|
||||||
|
/*
|
||||||
|
Copyright 2025 The Kubernetes Authors.
|
||||||
|
|
||||||
|
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 controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"k8s.io/klog/v2"
|
||||||
|
|
||||||
|
"k8s.io/apimachinery/pkg/labels"
|
||||||
|
buffersclient "k8s.io/autoscaler/cluster-autoscaler/apis/capacitybuffer/client/clientset/versioned"
|
||||||
|
|
||||||
|
v1 "k8s.io/autoscaler/cluster-autoscaler/apis/capacitybuffer/client/listers/autoscaling.x-k8s.io/v1"
|
||||||
|
"k8s.io/autoscaler/cluster-autoscaler/utils/kubernetes"
|
||||||
|
|
||||||
|
common "k8s.io/autoscaler/cluster-autoscaler/capacitybuffer/common"
|
||||||
|
filters "k8s.io/autoscaler/cluster-autoscaler/capacitybuffer/filters"
|
||||||
|
translators "k8s.io/autoscaler/cluster-autoscaler/capacitybuffer/translators"
|
||||||
|
updater "k8s.io/autoscaler/cluster-autoscaler/capacitybuffer/updater"
|
||||||
|
|
||||||
|
client "k8s.io/client-go/kubernetes"
|
||||||
|
)
|
||||||
|
|
||||||
|
const loopInterval = time.Second * 5
|
||||||
|
|
||||||
|
// BufferController performs updates on Buffers and convert them to pods to be injected
|
||||||
|
type BufferController interface {
|
||||||
|
// Run to run the reconciliation loop frequently every x seconds
|
||||||
|
Run(stopCh <-chan struct{})
|
||||||
|
}
|
||||||
|
|
||||||
|
type bufferController struct {
|
||||||
|
buffersLister v1.CapacityBufferLister
|
||||||
|
strategyFilter filters.Filter
|
||||||
|
statusFilter filters.Filter
|
||||||
|
translator translators.Translator
|
||||||
|
updater updater.StatusUpdater
|
||||||
|
loopInterval time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBufferController creates new bufferController object
|
||||||
|
func NewBufferController(
|
||||||
|
buffersLister v1.CapacityBufferLister,
|
||||||
|
strategyFilter filters.Filter,
|
||||||
|
statusFilter filters.Filter,
|
||||||
|
translator translators.Translator,
|
||||||
|
updater updater.StatusUpdater,
|
||||||
|
loopInterval time.Duration,
|
||||||
|
) BufferController {
|
||||||
|
return &bufferController{
|
||||||
|
buffersLister: buffersLister,
|
||||||
|
strategyFilter: strategyFilter,
|
||||||
|
statusFilter: statusFilter,
|
||||||
|
translator: translator,
|
||||||
|
updater: updater,
|
||||||
|
loopInterval: loopInterval,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDefaultBufferController creates bufferController with default configs
|
||||||
|
func NewDefaultBufferController(
|
||||||
|
listerRegistry kubernetes.ListerRegistry,
|
||||||
|
capacityBufferClinet buffersclient.Clientset,
|
||||||
|
nodeBufferListener v1.CapacityBufferLister,
|
||||||
|
kubeClient client.Clientset,
|
||||||
|
) BufferController {
|
||||||
|
return &bufferController{
|
||||||
|
buffersLister: nodeBufferListener,
|
||||||
|
// Accepting empty string as it represents nil value for ProvisioningStrategy
|
||||||
|
strategyFilter: filters.NewStrategyFilter([]string{common.ActiveProvisioningStrategy, ""}),
|
||||||
|
statusFilter: filters.NewStatusFilter(map[string]string{
|
||||||
|
common.ReadyForProvisioningCondition: common.ConditionTrue,
|
||||||
|
common.ProvisioningCondition: common.ConditionTrue,
|
||||||
|
}),
|
||||||
|
translator: translators.NewCombinedTranslator(
|
||||||
|
[]translators.Translator{
|
||||||
|
translators.NewPodTemplateBufferTranslator(),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
updater: *updater.NewStatusUpdater(&capacityBufferClinet),
|
||||||
|
loopInterval: loopInterval,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run to run the controller reconcile loop
|
||||||
|
func (c *bufferController) Run(stopCh <-chan struct{}) {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-stopCh:
|
||||||
|
return
|
||||||
|
case <-time.After(c.loopInterval):
|
||||||
|
c.reconcile()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reconcile represents single iteration in the main-loop of Updater
|
||||||
|
func (c *bufferController) reconcile() {
|
||||||
|
|
||||||
|
// List all capacity buffers objects
|
||||||
|
buffers, err := c.buffersLister.List(labels.Everything())
|
||||||
|
if err != nil {
|
||||||
|
klog.Errorf("Capacity buffer controller failed to list buffers with error: %v", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
klog.V(2).Infof("Capacity buffer controller listed [%v] buffers", len(buffers))
|
||||||
|
|
||||||
|
// Filter the desired provisioning strategy
|
||||||
|
filteredBuffers, _ := c.strategyFilter.Filter(buffers)
|
||||||
|
klog.V(2).Infof("Capacity buffer controller filtered %v buffers with buffers strategy filter", len(filteredBuffers))
|
||||||
|
|
||||||
|
// Filter the desired status
|
||||||
|
toBeTranslatedBuffers, _ := c.statusFilter.Filter(filteredBuffers)
|
||||||
|
klog.V(2).Infof("Capacity buffer controller filtered %v buffers with buffers status filter", len(filteredBuffers))
|
||||||
|
|
||||||
|
// Extract pod specs and number of replicas from filtered buffers
|
||||||
|
errors := c.translator.Translate(toBeTranslatedBuffers)
|
||||||
|
logErrors(errors)
|
||||||
|
|
||||||
|
// Update buffer status by calling API server
|
||||||
|
errors = c.updater.Update(toBeTranslatedBuffers)
|
||||||
|
logErrors(errors)
|
||||||
|
}
|
||||||
|
|
||||||
|
func logErrors(errors []error) {
|
||||||
|
for _, error := range errors {
|
||||||
|
klog.Errorf("Capacity buffer controller error: %v", error.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
/*
|
||||||
|
Copyright 2025 The Kubernetes Authors.
|
||||||
|
|
||||||
|
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 filter
|
||||||
|
|
||||||
|
import (
|
||||||
|
v1 "k8s.io/autoscaler/cluster-autoscaler/apis/capacitybuffer/autoscaling.x-k8s.io/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Filter filters CapacityBuffer based on some criteria.
|
||||||
|
type Filter interface {
|
||||||
|
Filter(buffers []*v1.CapacityBuffer) ([]*v1.CapacityBuffer, []*v1.CapacityBuffer)
|
||||||
|
CleanUp()
|
||||||
|
}
|
||||||
|
|
||||||
|
// combinedFilter is a list of Filter
|
||||||
|
type combinedFilter struct {
|
||||||
|
filters []Filter
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCombinedFilter construct combinedFilter.
|
||||||
|
func NewCombinedFilter(filters []Filter) *combinedFilter {
|
||||||
|
return &combinedFilter{filters}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddFilter append a filter to the list.
|
||||||
|
func (f *combinedFilter) AddFilter(filter Filter) {
|
||||||
|
f.filters = append(f.filters, filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter runs sub-filters sequentially
|
||||||
|
func (f *combinedFilter) Filter(buffers []*v1.CapacityBuffer) ([]*v1.CapacityBuffer, []*v1.CapacityBuffer) {
|
||||||
|
var totalFilteredOutBuffers []*v1.CapacityBuffer
|
||||||
|
for _, buffersFilter := range f.filters {
|
||||||
|
updatedBuffersList, filteredOutBuffers := buffersFilter.Filter(buffers)
|
||||||
|
buffers = updatedBuffersList
|
||||||
|
totalFilteredOutBuffers = append(totalFilteredOutBuffers, filteredOutBuffers...)
|
||||||
|
}
|
||||||
|
return buffers, totalFilteredOutBuffers
|
||||||
|
}
|
||||||
|
|
||||||
|
// CleanUp cleans up the filter's internal structures.
|
||||||
|
func (f *combinedFilter) CleanUp() {
|
||||||
|
for _, filter := range f.filters {
|
||||||
|
filter.CleanUp()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
/*
|
||||||
|
Copyright 2025 The Kubernetes Authors.
|
||||||
|
|
||||||
|
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 filter
|
||||||
|
|
||||||
|
import (
|
||||||
|
v1 "k8s.io/autoscaler/cluster-autoscaler/apis/capacitybuffer/autoscaling.x-k8s.io/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// statusFilter filters out buffers with the defined conditions
|
||||||
|
type statusFilter struct {
|
||||||
|
conditions map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewStatusFilter creates an instance of statusFilter that filters out the buffers with condition in passed conditions.
|
||||||
|
func NewStatusFilter(conditions map[string]string) *statusFilter {
|
||||||
|
return &statusFilter{
|
||||||
|
conditions: conditions,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter filters the passed buffers based on buffer status conditions
|
||||||
|
func (f *statusFilter) Filter(buffersToFilter []*v1.CapacityBuffer) ([]*v1.CapacityBuffer, []*v1.CapacityBuffer) {
|
||||||
|
var buffers []*v1.CapacityBuffer
|
||||||
|
var filteredOutBuffers []*v1.CapacityBuffer
|
||||||
|
|
||||||
|
for _, buffer := range buffersToFilter {
|
||||||
|
if !f.hasCondition(buffer) {
|
||||||
|
buffers = append(buffers, buffer)
|
||||||
|
} else {
|
||||||
|
filteredOutBuffers = append(filteredOutBuffers, buffer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return buffers, filteredOutBuffers
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *statusFilter) hasCondition(buffer *v1.CapacityBuffer) bool {
|
||||||
|
bufferConditions := buffer.Status.Conditions
|
||||||
|
for _, condition := range bufferConditions {
|
||||||
|
if val, found := f.conditions[condition.Type]; found && val == string(condition.Status) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// CleanUp cleans up the filter's internal structures.
|
||||||
|
func (f *statusFilter) CleanUp() {
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
/*
|
||||||
|
Copyright 2025 The Kubernetes Authors.
|
||||||
|
|
||||||
|
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 filter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
v1 "k8s.io/autoscaler/cluster-autoscaler/apis/capacitybuffer/autoscaling.x-k8s.io/v1"
|
||||||
|
"k8s.io/autoscaler/cluster-autoscaler/capacitybuffer/common"
|
||||||
|
"k8s.io/autoscaler/cluster-autoscaler/capacitybuffer/testutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestStatusFilter(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
conditions map[string]string
|
||||||
|
buffers []*v1.CapacityBuffer
|
||||||
|
expectedFilteredBuffers []*v1.CapacityBuffer
|
||||||
|
expectedFilteredOutBuffers []*v1.CapacityBuffer
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Empty conditions, filter none",
|
||||||
|
conditions: map[string]string{},
|
||||||
|
buffers: []*v1.CapacityBuffer{
|
||||||
|
testutil.GetPodTemplateRefBuffer(&v1.LocalObjectRef{Name: testutil.SomePodTemplateRefName}, nil),
|
||||||
|
},
|
||||||
|
expectedFilteredBuffers: []*v1.CapacityBuffer{
|
||||||
|
testutil.GetPodTemplateRefBuffer(&v1.LocalObjectRef{Name: testutil.SomePodTemplateRefName}, nil),
|
||||||
|
},
|
||||||
|
expectedFilteredOutBuffers: []*v1.CapacityBuffer{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Some condition, filter one",
|
||||||
|
conditions: map[string]string{common.ReadyForProvisioningCondition: common.ConditionTrue},
|
||||||
|
buffers: []*v1.CapacityBuffer{
|
||||||
|
testutil.GetBuffer(&testutil.ProvisioningStrategy, &v1.LocalObjectRef{Name: testutil.SomePodTemplateRefName}, nil, nil, nil, testutil.GetConditionReady()),
|
||||||
|
},
|
||||||
|
expectedFilteredBuffers: []*v1.CapacityBuffer{},
|
||||||
|
expectedFilteredOutBuffers: []*v1.CapacityBuffer{
|
||||||
|
testutil.GetBuffer(&testutil.ProvisioningStrategy, &v1.LocalObjectRef{Name: testutil.SomePodTemplateRefName}, nil, nil, nil, testutil.GetConditionReady()),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Some condition, filter one in and one out",
|
||||||
|
conditions: map[string]string{common.ReadyForProvisioningCondition: common.ConditionTrue},
|
||||||
|
buffers: []*v1.CapacityBuffer{
|
||||||
|
testutil.GetBuffer(&testutil.ProvisioningStrategy, &v1.LocalObjectRef{Name: testutil.SomePodTemplateRefName}, nil, nil, nil, testutil.GetConditionReady()),
|
||||||
|
testutil.GetBuffer(&testutil.ProvisioningStrategy, &v1.LocalObjectRef{Name: testutil.AnotherPodTemplateRefName}, nil, nil, nil, testutil.GetConditionNotReady()),
|
||||||
|
},
|
||||||
|
expectedFilteredBuffers: []*v1.CapacityBuffer{
|
||||||
|
testutil.GetBuffer(&testutil.ProvisioningStrategy, &v1.LocalObjectRef{Name: testutil.AnotherPodTemplateRefName}, nil, nil, nil, testutil.GetConditionNotReady()),
|
||||||
|
},
|
||||||
|
expectedFilteredOutBuffers: []*v1.CapacityBuffer{
|
||||||
|
testutil.GetBuffer(&testutil.ProvisioningStrategy, &v1.LocalObjectRef{Name: testutil.SomePodTemplateRefName}, nil, nil, nil, testutil.GetConditionReady()),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
statusFilter := NewStatusFilter(test.conditions)
|
||||||
|
filtered, filteredOut := statusFilter.Filter(test.buffers)
|
||||||
|
assert.ElementsMatch(t, test.expectedFilteredBuffers, filtered)
|
||||||
|
assert.ElementsMatch(t, test.expectedFilteredOutBuffers, filteredOut)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
/*
|
||||||
|
Copyright 2025 The Kubernetes Authors.
|
||||||
|
|
||||||
|
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 filter
|
||||||
|
|
||||||
|
import (
|
||||||
|
v1 "k8s.io/autoscaler/cluster-autoscaler/apis/capacitybuffer/autoscaling.x-k8s.io/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// strategyFilter filters out buffers with provisioning strategies not defined in strategiesToUse
|
||||||
|
// and defaults nil values of provisioningStrategy to empty string
|
||||||
|
type strategyFilter struct {
|
||||||
|
strategiesToUse map[string]bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewStrategyFilter creates an instance of strategyFilter.
|
||||||
|
func NewStrategyFilter(strategiesToUse []string) *strategyFilter {
|
||||||
|
strategiesToUseMap := map[string]bool{}
|
||||||
|
for _, strategy := range strategiesToUse {
|
||||||
|
strategiesToUseMap[strategy] = true
|
||||||
|
}
|
||||||
|
return &strategyFilter{
|
||||||
|
strategiesToUse: strategiesToUseMap,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter filters out buffers with provisioning strategies not defined in strategiesToUseMap
|
||||||
|
func (f *strategyFilter) Filter(buffers []*v1.CapacityBuffer) ([]*v1.CapacityBuffer, []*v1.CapacityBuffer) {
|
||||||
|
|
||||||
|
var filteredBuffers []*v1.CapacityBuffer
|
||||||
|
var filteredOutBuffers []*v1.CapacityBuffer
|
||||||
|
|
||||||
|
for _, buffer := range buffers {
|
||||||
|
if f.isAllowedProvisioningStrategy(buffer) {
|
||||||
|
filteredBuffers = append(filteredBuffers, buffer)
|
||||||
|
} else {
|
||||||
|
filteredOutBuffers = append(filteredOutBuffers, buffer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return filteredBuffers, filteredOutBuffers
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *strategyFilter) isAllowedProvisioningStrategy(buffer *v1.CapacityBuffer) bool {
|
||||||
|
provisioningStrategy := ""
|
||||||
|
if buffer.Spec.ProvisioningStrategy != nil {
|
||||||
|
provisioningStrategy = *buffer.Spec.ProvisioningStrategy
|
||||||
|
}
|
||||||
|
|
||||||
|
if useStrategy, found := f.strategiesToUse[provisioningStrategy]; found && useStrategy {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// CleanUp cleans up the filter's internal structures.
|
||||||
|
func (f *strategyFilter) CleanUp() {
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,94 @@
|
||||||
|
/*
|
||||||
|
Copyright 2025 The Kubernetes Authors.
|
||||||
|
|
||||||
|
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 filter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
v1 "k8s.io/autoscaler/cluster-autoscaler/apis/capacitybuffer/autoscaling.x-k8s.io/v1"
|
||||||
|
"k8s.io/autoscaler/cluster-autoscaler/capacitybuffer/testutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestStrategyFilter(t *testing.T) {
|
||||||
|
someRandomStrategy := "someStrategy"
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
buffers []*v1.CapacityBuffer
|
||||||
|
strategiesToConsider []string
|
||||||
|
expectedFilteredBuffers []*v1.CapacityBuffer
|
||||||
|
expectedFilteredOutBuffers []*v1.CapacityBuffer
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Single buffer with accepted strategy",
|
||||||
|
buffers: []*v1.CapacityBuffer{
|
||||||
|
testutil.GetBuffer(&testutil.ProvisioningStrategy, &v1.LocalObjectRef{Name: testutil.SomePodTemplateRefName}, nil, nil, nil, nil),
|
||||||
|
},
|
||||||
|
strategiesToConsider: []string{testutil.ProvisioningStrategy},
|
||||||
|
expectedFilteredBuffers: []*v1.CapacityBuffer{
|
||||||
|
testutil.GetBuffer(&testutil.ProvisioningStrategy, &v1.LocalObjectRef{Name: testutil.SomePodTemplateRefName}, nil, nil, nil, nil),
|
||||||
|
},
|
||||||
|
expectedFilteredOutBuffers: []*v1.CapacityBuffer{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Nil strategy defaulting to empty",
|
||||||
|
buffers: []*v1.CapacityBuffer{
|
||||||
|
testutil.GetBuffer(nil, &v1.LocalObjectRef{Name: testutil.SomePodTemplateRefName}, nil, nil, nil, nil),
|
||||||
|
},
|
||||||
|
strategiesToConsider: []string{""},
|
||||||
|
expectedFilteredBuffers: []*v1.CapacityBuffer{
|
||||||
|
testutil.GetBuffer(nil, &v1.LocalObjectRef{Name: testutil.SomePodTemplateRefName}, nil, nil, nil, nil),
|
||||||
|
},
|
||||||
|
expectedFilteredOutBuffers: []*v1.CapacityBuffer{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Single buffer with rejected strategy",
|
||||||
|
buffers: []*v1.CapacityBuffer{
|
||||||
|
testutil.GetBuffer(&someRandomStrategy, &v1.LocalObjectRef{Name: testutil.SomePodTemplateRefName}, nil, nil, nil, nil),
|
||||||
|
},
|
||||||
|
strategiesToConsider: []string{testutil.ProvisioningStrategy},
|
||||||
|
expectedFilteredBuffers: []*v1.CapacityBuffer{},
|
||||||
|
expectedFilteredOutBuffers: []*v1.CapacityBuffer{
|
||||||
|
testutil.GetBuffer(&someRandomStrategy, &v1.LocalObjectRef{Name: testutil.SomePodTemplateRefName}, nil, nil, nil, nil),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Multiple buffers different strategies",
|
||||||
|
buffers: []*v1.CapacityBuffer{
|
||||||
|
testutil.GetBuffer(&someRandomStrategy, &v1.LocalObjectRef{Name: testutil.SomePodTemplateRefName}, nil, nil, nil, nil),
|
||||||
|
testutil.GetBuffer(&testutil.ProvisioningStrategy, &v1.LocalObjectRef{Name: testutil.SomePodTemplateRefName}, nil, nil, nil, nil),
|
||||||
|
testutil.GetBuffer(nil, &v1.LocalObjectRef{Name: testutil.SomePodTemplateRefName}, nil, nil, nil, nil),
|
||||||
|
},
|
||||||
|
strategiesToConsider: []string{testutil.ProvisioningStrategy, ""},
|
||||||
|
expectedFilteredBuffers: []*v1.CapacityBuffer{
|
||||||
|
testutil.GetBuffer(&testutil.ProvisioningStrategy, &v1.LocalObjectRef{Name: testutil.SomePodTemplateRefName}, nil, nil, nil, nil),
|
||||||
|
testutil.GetBuffer(nil, &v1.LocalObjectRef{Name: testutil.SomePodTemplateRefName}, nil, nil, nil, nil),
|
||||||
|
},
|
||||||
|
expectedFilteredOutBuffers: []*v1.CapacityBuffer{
|
||||||
|
testutil.GetBuffer(&someRandomStrategy, &v1.LocalObjectRef{Name: testutil.SomePodTemplateRefName}, nil, nil, nil, nil),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
strategyFilter := NewStrategyFilter(test.strategiesToConsider)
|
||||||
|
filtered, filteredOut := strategyFilter.Filter(test.buffers)
|
||||||
|
assert.ElementsMatch(t, test.expectedFilteredBuffers, filtered)
|
||||||
|
assert.ElementsMatch(t, test.expectedFilteredOutBuffers, filteredOut)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,109 @@
|
||||||
|
/*
|
||||||
|
Copyright 2025 The Kubernetes Authors.
|
||||||
|
|
||||||
|
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 testutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
v1 "k8s.io/autoscaler/cluster-autoscaler/apis/capacitybuffer/autoscaling.x-k8s.io/v1"
|
||||||
|
"k8s.io/autoscaler/cluster-autoscaler/capacitybuffer/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
// To use their pointers in creating testing capacity buffer objects
|
||||||
|
var (
|
||||||
|
ProvisioningStrategy = common.ActiveProvisioningStrategy
|
||||||
|
SomeNumberOfReplicas = int32(3)
|
||||||
|
AnotherNumberOfReplicas = int32(5)
|
||||||
|
SomePodTemplateRefName = "some-pod-template"
|
||||||
|
AnotherPodTemplateRefName = "another-pod-template"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SanitizeBuffersStatus returns a list of the status objects of the passed buffers after sanitizing them for testing comparison
|
||||||
|
func SanitizeBuffersStatus(buffers []*v1.CapacityBuffer) []*v1.CapacityBufferStatus {
|
||||||
|
resultedStatus := []*v1.CapacityBufferStatus{}
|
||||||
|
for _, buffer := range buffers {
|
||||||
|
for i := range buffer.Status.Conditions {
|
||||||
|
buffer.Status.Conditions[i].LastTransitionTime = metav1.Time{}
|
||||||
|
buffer.Status.Conditions[i].Message = ""
|
||||||
|
}
|
||||||
|
resultedStatus = append(resultedStatus, &buffer.Status)
|
||||||
|
}
|
||||||
|
return resultedStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPodTemplateRefBuffer returns a buffer with podTemplateRef with the passed attributes and empty status, should be used for testing purposes only
|
||||||
|
func GetPodTemplateRefBuffer(podTemplateRef *v1.LocalObjectRef, replicas *int32) *v1.CapacityBuffer {
|
||||||
|
return &v1.CapacityBuffer{
|
||||||
|
Spec: v1.CapacityBufferSpec{
|
||||||
|
ProvisioningStrategy: &ProvisioningStrategy,
|
||||||
|
PodTemplateRef: podTemplateRef,
|
||||||
|
ScalableRef: nil,
|
||||||
|
Replicas: replicas,
|
||||||
|
Percentage: nil,
|
||||||
|
Limits: nil,
|
||||||
|
},
|
||||||
|
Status: *GetBufferStatus(nil, nil, nil),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBuffer returns a capacity buffer with the passed attributes, should be used for testing purposes only
|
||||||
|
func GetBuffer(strategy *string, podTemplateRef *v1.LocalObjectRef, replicas *int32, podTempRef *v1.LocalObjectRef, statusReplicas *int32, conditions []metav1.Condition) *v1.CapacityBuffer {
|
||||||
|
return &v1.CapacityBuffer{
|
||||||
|
Spec: v1.CapacityBufferSpec{
|
||||||
|
ProvisioningStrategy: strategy,
|
||||||
|
PodTemplateRef: podTemplateRef,
|
||||||
|
ScalableRef: nil,
|
||||||
|
Replicas: replicas,
|
||||||
|
Percentage: nil,
|
||||||
|
Limits: nil,
|
||||||
|
},
|
||||||
|
Status: *GetBufferStatus(podTempRef, statusReplicas, conditions),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBufferStatus returns a buffer status with the passed attributes, should be used for testing purposes only
|
||||||
|
func GetBufferStatus(podTempRef *v1.LocalObjectRef, replicas *int32, conditions []metav1.Condition) *v1.CapacityBufferStatus {
|
||||||
|
return &v1.CapacityBufferStatus{
|
||||||
|
PodTemplateRef: podTempRef,
|
||||||
|
Replicas: replicas,
|
||||||
|
PodTemplateGeneration: nil,
|
||||||
|
Conditions: conditions,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConditionReady returns a list of conditions with a condition ready and empty message, should be used for testing purposes only
|
||||||
|
func GetConditionReady() []metav1.Condition {
|
||||||
|
readyCondition := metav1.Condition{
|
||||||
|
Type: common.ReadyForProvisioningCondition,
|
||||||
|
Status: common.ConditionTrue,
|
||||||
|
Message: "",
|
||||||
|
Reason: "atrtibutesSetSuccessfully",
|
||||||
|
LastTransitionTime: metav1.Time{},
|
||||||
|
}
|
||||||
|
return []metav1.Condition{readyCondition}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConditionNotReady returns a list of conditions with a condition not ready and empty message, should be used for testing purposes only
|
||||||
|
func GetConditionNotReady() []metav1.Condition {
|
||||||
|
notReadyCondition := metav1.Condition{
|
||||||
|
Type: common.ReadyForProvisioningCondition,
|
||||||
|
Status: common.ConditionFalse,
|
||||||
|
Message: "",
|
||||||
|
Reason: "error",
|
||||||
|
LastTransitionTime: metav1.Time{},
|
||||||
|
}
|
||||||
|
return []metav1.Condition{notReadyCondition}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
/*
|
||||||
|
Copyright 2025 The Kubernetes Authors.
|
||||||
|
|
||||||
|
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 translator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
v1 "k8s.io/autoscaler/cluster-autoscaler/apis/capacitybuffer/autoscaling.x-k8s.io/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// podTemplateBufferTranslator translates podTemplateRef buffers specs to fill their status.
|
||||||
|
type podTemplateBufferTranslator struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPodTemplateBufferTranslator creates an instance of podTemplateBufferTranslator.
|
||||||
|
func NewPodTemplateBufferTranslator() *podTemplateBufferTranslator {
|
||||||
|
return &podTemplateBufferTranslator{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Translate translates buffers processors into pod capacity.
|
||||||
|
func (t *podTemplateBufferTranslator) Translate(buffers []*v1.CapacityBuffer) []error {
|
||||||
|
errors := []error{}
|
||||||
|
for _, buffer := range buffers {
|
||||||
|
if isPodTemplateBasedBuffer(buffer) {
|
||||||
|
podTemplateRef, numberOfPods, err := t.translate(buffer)
|
||||||
|
if err != nil {
|
||||||
|
setBufferAsNotReadyForProvisioning(buffer, err.Error())
|
||||||
|
errors = append(errors, err)
|
||||||
|
} else {
|
||||||
|
setBufferAsReadyForProvisioning(buffer, podTemplateRef.Name, numberOfPods)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return errors
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *podTemplateBufferTranslator) translate(buffer *v1.CapacityBuffer) (*v1.LocalObjectRef, int32, error) {
|
||||||
|
// Fixed Replicas will be used if both Replicas and Percent are defined
|
||||||
|
if buffer.Spec.Replicas != nil {
|
||||||
|
return buffer.Spec.PodTemplateRef, max(1, int32(*buffer.Spec.Replicas)), nil
|
||||||
|
}
|
||||||
|
return nil, 0, fmt.Errorf("Failed to translate buffer %v, Replicas should have a value when PodTemplateRef is set", buffer.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isPodTemplateBasedBuffer(buffer *v1.CapacityBuffer) bool {
|
||||||
|
return buffer.Spec.PodTemplateRef != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CleanUp cleans up the translator's internal structures.
|
||||||
|
func (t *podTemplateBufferTranslator) CleanUp() {
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,89 @@
|
||||||
|
/*
|
||||||
|
Copyright 2025 The Kubernetes Authors.
|
||||||
|
|
||||||
|
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 translator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
v1 "k8s.io/autoscaler/cluster-autoscaler/apis/capacitybuffer/autoscaling.x-k8s.io/v1"
|
||||||
|
"k8s.io/autoscaler/cluster-autoscaler/capacitybuffer/testutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPodTemplateBufferTranslator(t *testing.T) {
|
||||||
|
podTemplateBufferTranslator := NewPodTemplateBufferTranslator()
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
buffers []*v1.CapacityBuffer
|
||||||
|
expectedStatus []*v1.CapacityBufferStatus
|
||||||
|
expectedNumberOfErrors int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Test 1 buffer with pod template ref",
|
||||||
|
buffers: []*v1.CapacityBuffer{
|
||||||
|
testutil.GetPodTemplateRefBuffer(&v1.LocalObjectRef{Name: testutil.SomePodTemplateRefName}, &testutil.SomeNumberOfReplicas),
|
||||||
|
},
|
||||||
|
expectedStatus: []*v1.CapacityBufferStatus{
|
||||||
|
testutil.GetBufferStatus(&v1.LocalObjectRef{Name: testutil.SomePodTemplateRefName}, &testutil.SomeNumberOfReplicas, testutil.GetConditionReady()),
|
||||||
|
},
|
||||||
|
expectedNumberOfErrors: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Test 2 buffers with pod template ref",
|
||||||
|
buffers: []*v1.CapacityBuffer{
|
||||||
|
testutil.GetPodTemplateRefBuffer(&v1.LocalObjectRef{Name: testutil.SomePodTemplateRefName}, &testutil.SomeNumberOfReplicas),
|
||||||
|
testutil.GetPodTemplateRefBuffer(&v1.LocalObjectRef{Name: testutil.AnotherPodTemplateRefName}, &testutil.AnotherNumberOfReplicas),
|
||||||
|
},
|
||||||
|
expectedStatus: []*v1.CapacityBufferStatus{
|
||||||
|
testutil.GetBufferStatus(&v1.LocalObjectRef{Name: testutil.SomePodTemplateRefName}, &testutil.SomeNumberOfReplicas, testutil.GetConditionReady()),
|
||||||
|
testutil.GetBufferStatus(&v1.LocalObjectRef{Name: testutil.AnotherPodTemplateRefName}, &testutil.AnotherNumberOfReplicas, testutil.GetConditionReady()),
|
||||||
|
},
|
||||||
|
expectedNumberOfErrors: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Test 2 buffers, one with no replicas",
|
||||||
|
buffers: []*v1.CapacityBuffer{
|
||||||
|
testutil.GetPodTemplateRefBuffer(&v1.LocalObjectRef{Name: testutil.SomePodTemplateRefName}, &testutil.SomeNumberOfReplicas),
|
||||||
|
testutil.GetPodTemplateRefBuffer(&v1.LocalObjectRef{Name: testutil.AnotherPodTemplateRefName}, nil),
|
||||||
|
},
|
||||||
|
expectedStatus: []*v1.CapacityBufferStatus{
|
||||||
|
testutil.GetBufferStatus(&v1.LocalObjectRef{Name: testutil.SomePodTemplateRefName}, &testutil.SomeNumberOfReplicas, testutil.GetConditionReady()),
|
||||||
|
testutil.GetBufferStatus(nil, nil, testutil.GetConditionNotReady()),
|
||||||
|
},
|
||||||
|
expectedNumberOfErrors: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Test 2 buffers, one with no pod template ref",
|
||||||
|
buffers: []*v1.CapacityBuffer{
|
||||||
|
testutil.GetPodTemplateRefBuffer(&v1.LocalObjectRef{Name: testutil.SomePodTemplateRefName}, &testutil.SomeNumberOfReplicas),
|
||||||
|
testutil.GetPodTemplateRefBuffer(nil, &testutil.AnotherNumberOfReplicas),
|
||||||
|
},
|
||||||
|
expectedStatus: []*v1.CapacityBufferStatus{
|
||||||
|
testutil.GetBufferStatus(&v1.LocalObjectRef{Name: testutil.SomePodTemplateRefName}, &testutil.SomeNumberOfReplicas, testutil.GetConditionReady()),
|
||||||
|
testutil.GetBufferStatus(nil, nil, nil),
|
||||||
|
},
|
||||||
|
expectedNumberOfErrors: 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
errors := podTemplateBufferTranslator.Translate(test.buffers)
|
||||||
|
assert.Equal(t, len(errors), test.expectedNumberOfErrors)
|
||||||
|
assert.ElementsMatch(t, test.expectedStatus, testutil.SanitizeBuffersStatus(test.buffers))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,94 @@
|
||||||
|
/*
|
||||||
|
Copyright 2025 The Kubernetes Authors.
|
||||||
|
|
||||||
|
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 translator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
v1 "k8s.io/autoscaler/cluster-autoscaler/apis/capacitybuffer/autoscaling.x-k8s.io/v1"
|
||||||
|
"k8s.io/autoscaler/cluster-autoscaler/capacitybuffer/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Translator translates the passed buffers to pod template and number of replicas
|
||||||
|
type Translator interface {
|
||||||
|
Translate(buffers []*v1.CapacityBuffer) []error
|
||||||
|
CleanUp()
|
||||||
|
}
|
||||||
|
|
||||||
|
// combinedTranslator is a list of Translator
|
||||||
|
type combinedTranslator struct {
|
||||||
|
translators []Translator
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCombinedTranslator construct combinedTranslator.
|
||||||
|
func NewCombinedTranslator(Translators []Translator) *combinedTranslator {
|
||||||
|
return &combinedTranslator{Translators}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddTranslator append translator to the list.
|
||||||
|
func (b *combinedTranslator) AddTranslator(translator Translator) {
|
||||||
|
b.translators = append(b.translators, translator)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Translate runs sub-translate sequentially, in case more than one translator acted on same buffer
|
||||||
|
// last translator overrides the others
|
||||||
|
func (b *combinedTranslator) Translate(buffers []*v1.CapacityBuffer) []error {
|
||||||
|
var errors []error
|
||||||
|
for _, translator := range b.translators {
|
||||||
|
bufferErrors := translator.Translate(buffers)
|
||||||
|
errors = append(errors, bufferErrors...)
|
||||||
|
}
|
||||||
|
return errors
|
||||||
|
}
|
||||||
|
|
||||||
|
// CleanUp cleans up the translator's internal structures.
|
||||||
|
func (b *combinedTranslator) CleanUp() {
|
||||||
|
for _, translator := range b.translators {
|
||||||
|
translator.CleanUp()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setBufferAsReadyForProvisioning(buffer *v1.CapacityBuffer, podTemplateName string, replicas int32) {
|
||||||
|
buffer.Status.PodTemplateRef = &v1.LocalObjectRef{
|
||||||
|
Name: podTemplateName,
|
||||||
|
}
|
||||||
|
buffer.Status.Replicas = &replicas
|
||||||
|
buffer.Status.PodTemplateGeneration = nil
|
||||||
|
readyCondition := metav1.Condition{
|
||||||
|
Type: common.ReadyForProvisioningCondition,
|
||||||
|
Status: common.ConditionTrue,
|
||||||
|
Message: "ready",
|
||||||
|
Reason: "atrtibutesSetSuccessfully",
|
||||||
|
LastTransitionTime: metav1.Time{Time: time.Now()},
|
||||||
|
}
|
||||||
|
buffer.Status.Conditions = []metav1.Condition{readyCondition}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setBufferAsNotReadyForProvisioning(buffer *v1.CapacityBuffer, errorMessage string) {
|
||||||
|
buffer.Status.PodTemplateRef = nil
|
||||||
|
buffer.Status.Replicas = nil
|
||||||
|
buffer.Status.PodTemplateGeneration = nil
|
||||||
|
notReadyCondition := metav1.Condition{
|
||||||
|
Type: common.ReadyForProvisioningCondition,
|
||||||
|
Status: common.ConditionFalse,
|
||||||
|
Message: errorMessage,
|
||||||
|
Reason: "error",
|
||||||
|
LastTransitionTime: metav1.Time{Time: time.Now()},
|
||||||
|
}
|
||||||
|
buffer.Status.Conditions = []metav1.Condition{notReadyCondition}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
/*
|
||||||
|
Copyright 2025 The Kubernetes Authors.
|
||||||
|
|
||||||
|
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 updater
|
||||||
|
|
||||||
|
import (
|
||||||
|
v1 "k8s.io/autoscaler/cluster-autoscaler/apis/capacitybuffer/autoscaling.x-k8s.io/v1"
|
||||||
|
client "k8s.io/autoscaler/cluster-autoscaler/apis/capacitybuffer/client/clientset/versioned"
|
||||||
|
common "k8s.io/autoscaler/cluster-autoscaler/capacitybuffer/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StatusUpdater updates the buffer status bassed
|
||||||
|
type StatusUpdater struct {
|
||||||
|
client client.Interface
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewStatusUpdater creates an instance of StatusUpdater.
|
||||||
|
func NewStatusUpdater(client client.Interface) *StatusUpdater {
|
||||||
|
return &StatusUpdater{
|
||||||
|
client: client,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update updates the buffer status with pod capacity
|
||||||
|
func (u *StatusUpdater) Update(buffers []*v1.CapacityBuffer) []error {
|
||||||
|
var errors []error
|
||||||
|
for _, buffer := range buffers {
|
||||||
|
err := common.UpdateBufferStatus(u.client, buffer)
|
||||||
|
if err != nil {
|
||||||
|
errors = append(errors, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return errors
|
||||||
|
}
|
||||||
|
|
||||||
|
// CleanUp cleans up the updater's internal structures.
|
||||||
|
func (u *StatusUpdater) CleanUp() {
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,94 @@
|
||||||
|
/*
|
||||||
|
Copyright 2025 The Kubernetes Authors.
|
||||||
|
|
||||||
|
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 updater
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
ctesting "k8s.io/client-go/testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
v1 "k8s.io/autoscaler/cluster-autoscaler/apis/capacitybuffer/autoscaling.x-k8s.io/v1"
|
||||||
|
fakeclientset "k8s.io/autoscaler/cluster-autoscaler/apis/capacitybuffer/client/clientset/versioned/fake"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestStatusUpdater(t *testing.T) {
|
||||||
|
exitingBuffer := &v1.CapacityBuffer{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "buffer1",
|
||||||
|
Namespace: "default",
|
||||||
|
},
|
||||||
|
Spec: v1.CapacityBufferSpec{},
|
||||||
|
}
|
||||||
|
notExistingBuffer := &v1.CapacityBuffer{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "buffer2",
|
||||||
|
Namespace: "default",
|
||||||
|
},
|
||||||
|
Spec: v1.CapacityBufferSpec{},
|
||||||
|
}
|
||||||
|
fakeClient := fakeclientset.NewSimpleClientset(exitingBuffer)
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
buffers []*v1.CapacityBuffer
|
||||||
|
expectedNumberOfCalls int
|
||||||
|
expectedNumberOfErrors int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Update one buffer",
|
||||||
|
buffers: []*v1.CapacityBuffer{
|
||||||
|
exitingBuffer,
|
||||||
|
},
|
||||||
|
expectedNumberOfCalls: 1,
|
||||||
|
expectedNumberOfErrors: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Update one buffer not existing",
|
||||||
|
buffers: []*v1.CapacityBuffer{
|
||||||
|
notExistingBuffer,
|
||||||
|
},
|
||||||
|
expectedNumberOfCalls: 1,
|
||||||
|
expectedNumberOfErrors: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Update multiple buffers",
|
||||||
|
buffers: []*v1.CapacityBuffer{
|
||||||
|
exitingBuffer,
|
||||||
|
notExistingBuffer,
|
||||||
|
},
|
||||||
|
expectedNumberOfCalls: 2,
|
||||||
|
expectedNumberOfErrors: 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
updateCallsCount := 0
|
||||||
|
fakeClient.Fake.PrependReactor("update", "capacitybuffers",
|
||||||
|
func(action ctesting.Action) (handled bool, ret runtime.Object, err error) {
|
||||||
|
updateCallsCount++
|
||||||
|
return false, nil, nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
buffersUpdater := NewStatusUpdater(fakeClient)
|
||||||
|
errors := buffersUpdater.Update(test.buffers)
|
||||||
|
assert.Equal(t, test.expectedNumberOfErrors, len(errors))
|
||||||
|
assert.Equal(t, test.expectedNumberOfCalls, updateCallsCount)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue