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.
|
||||
type CapacityBufferSpec struct {
|
||||
// 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.
|
||||
// +kubebuilder:validation:Enum=active-capacity
|
||||
// +kubebuilder:default="active-capacity"
|
||||
// "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=buffer.x-k8s.io/active-capacity
|
||||
// +kubebuilder:default="buffer.x-k8s.io/active-capacity"
|
||||
// +optional
|
||||
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
|
||||
// defined resource limits (if any) will be created. If both are set, the maximum
|
||||
// of the two will be used.
|
||||
// This field is mutually exclusive with `percentage` when `scalableRef` is set.
|
||||
// +optional
|
||||
// +kubebuilder:validation:Minimum=0
|
||||
// +kubebuilder:validation:ExclusiveMinimum=false
|
||||
// +kubebuilder:validation:Xor=replicas,percentage
|
||||
Replicas *int32 `json:"replicas,omitempty" protobuf:"varint,4,opt,name=replicas"`
|
||||
|
||||
// Percentage defines the desired buffer capacity as a percentage of the
|
||||
// `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.
|
||||
// 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
|
||||
// +kubebuilder:validation:Minimum=0
|
||||
// +kubebuilder:validation:Maximum=100
|
||||
// +kubebuilder:validation:ExclusiveMaximum=false
|
||||
// +kubebuilder:validation:ExclusiveMinimum=false
|
||||
// +kubebuilder:validation:Xor=replicas,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
|
||||
|
|
|
|||
|
|
@ -100,12 +100,12 @@ spec:
|
|||
- name
|
||||
type: object
|
||||
provisioningStrategy:
|
||||
default: active-capacity
|
||||
default: buffer.x-k8s.io/active-capacity
|
||||
description: |-
|
||||
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:
|
||||
- active-capacity
|
||||
- buffer.x-k8s.io/active-capacity
|
||||
type: string
|
||||
replicas:
|
||||
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