Skip to content

Commit 58e27de

Browse files
committed
Add a full cycle CA test.
1 parent 1c02f08 commit 58e27de

File tree

7 files changed

+725
-28
lines changed

7 files changed

+725
-28
lines changed

cluster-autoscaler/builder/autoscaler_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,12 @@ func TestAutoscalerBuilderNoError(t *testing.T) {
4343
}
4444

4545
debuggingSnapshotter := debuggingsnapshot.NewDebuggingSnapshotter(false)
46-
kubeClient := fake.NewClientset()
46+
kubeClient := fake.NewClientset()
4747

4848
autoscaler, trigger, err := New(options).
4949
WithKubeClient(kubeClient).
5050
WithInformerFactory(informers.NewSharedInformerFactory(kubeClient, 0)).
51-
WithCloudProvider(test.NewCloudProvider()).
51+
WithCloudProvider(test.NewCloudProvider(nil)).
5252
WithPodObserver(&loop.UnschedulablePodObserver{}).
5353
Build(ctx, debuggingSnapshotter)
5454

cluster-autoscaler/cloudprovider/test/fake_cloud_provider.go

Lines changed: 293 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -17,81 +17,349 @@ limitations under the License.
1717
package test
1818

1919
import (
20+
"fmt"
2021
apiv1 "k8s.io/api/core/v1"
2122
"k8s.io/apimachinery/pkg/api/resource"
2223
"k8s.io/autoscaler/cluster-autoscaler/cloudprovider"
24+
"k8s.io/autoscaler/cluster-autoscaler/config"
25+
"k8s.io/autoscaler/cluster-autoscaler/simulator/framework"
2326
"k8s.io/autoscaler/cluster-autoscaler/utils/errors"
27+
fakek8s "k8s.io/autoscaler/cluster-autoscaler/utils/fake"
2428
"sync"
2529
)
2630

31+
const (
32+
defaultMinSize = 0
33+
defaultMaxSize = 1000
34+
)
35+
2736
// CloudProvider is a fake implementation of the cloudprovider interface for testing.
2837
type CloudProvider struct {
29-
sync.Mutex
38+
sync.RWMutex
39+
groups map[string]cloudprovider.NodeGroup
40+
minLimits map[string]int64
41+
maxLimits map[string]int64
42+
// nodeToGroup tracks which node name belongs to which group ID.
43+
nodeToGroup map[string]string
44+
k8s *fakek8s.Kubernetes
3045
}
3146

47+
// CloudProviderOption defines a function to configure the CloudProvider.
48+
type CloudProviderOption func(*CloudProvider)
49+
3250
// NewCloudProvider creates a new instance of the fake CloudProvider.
33-
func NewCloudProvider() *CloudProvider {
34-
return &CloudProvider{}
51+
func NewCloudProvider(k8s *fakek8s.Kubernetes) *CloudProvider {
52+
return &CloudProvider{
53+
groups: make(map[string]cloudprovider.NodeGroup),
54+
nodeToGroup: make(map[string]string),
55+
minLimits: map[string]int64{
56+
cloudprovider.ResourceNameCores: 0,
57+
cloudprovider.ResourceNameMemory: 0,
58+
},
59+
maxLimits: map[string]int64{
60+
// Set to a effectively infinite number for tests.
61+
cloudprovider.ResourceNameCores: 1000000,
62+
cloudprovider.ResourceNameMemory: 1000000,
63+
},
64+
k8s: k8s,
65+
}
3566
}
3667

3768
// NodeGroups returns all node groups configured in the fake CloudProvider.
3869
func (c *CloudProvider) NodeGroups() []cloudprovider.NodeGroup {
39-
panic("not implemented")
70+
c.Lock()
71+
defer c.Unlock()
72+
var res []cloudprovider.NodeGroup
73+
for _, g := range c.groups {
74+
res = append(res, g)
75+
}
76+
return res
4077
}
4178

4279
// NodeGroupForNode returns the node group that a given node belongs to.
4380
func (c *CloudProvider) NodeGroupForNode(node *apiv1.Node) (cloudprovider.NodeGroup, error) {
44-
panic("not implemented")
81+
c.Lock()
82+
defer c.Unlock()
83+
groupId, ok := c.nodeToGroup[node.Name]
84+
if !ok {
85+
return nil, nil
86+
}
87+
return c.groups[groupId], nil
4588
}
4689

4790
// HasInstance returns true if the given node is managed by this cloud provider.
4891
func (c *CloudProvider) HasInstance(node *apiv1.Node) (bool, error) {
49-
panic("not implemented")
92+
c.Lock()
93+
defer c.Unlock()
94+
_, found := c.nodeToGroup[node.Name]
95+
return found, nil
5096
}
5197

52-
// GetResourceLimiter generates a NEW limiter based on our current internal maps.
98+
// GetResourceLimiter generates a new limiter based on our current internal maps.
5399
func (c *CloudProvider) GetResourceLimiter() (*cloudprovider.ResourceLimiter, error) {
54-
panic("not implemented")
100+
c.Lock()
101+
defer c.Unlock()
102+
return cloudprovider.NewResourceLimiter(c.minLimits, c.maxLimits), nil
55103
}
56104

57105
// GPULabel returns the label used to identify GPU types in this provider.
58-
func (c *CloudProvider) GPULabel() string {
59-
panic("not implemented")
60-
}
106+
func (c *CloudProvider) GPULabel() string { return "gpu-label" }
61107

62108
// GetAvailableGPUTypes returns a map of all GPU types available in this provider.
63-
func (c *CloudProvider) GetAvailableGPUTypes() map[string]struct{} {
64-
panic("not implemented")
65-
}
109+
func (c *CloudProvider) GetAvailableGPUTypes() map[string]struct{} { return nil }
66110

67111
// GetNodeGpuConfig returns the GPU configuration for a specific node.
68-
func (c *CloudProvider) GetNodeGpuConfig(node *apiv1.Node) *cloudprovider.GpuConfig {
69-
panic("not implemented")
70-
}
112+
func (c *CloudProvider) GetNodeGpuConfig(node *apiv1.Node) *cloudprovider.GpuConfig { return nil }
71113

72114
// Cleanup performs any necessary teardown of the CloudProvider.
73-
func (c *CloudProvider) Cleanup() error {
74-
panic("not implemented")
75-
}
115+
func (c *CloudProvider) Cleanup() error { return nil }
76116

77117
// Refresh updates the internal state of the CloudProvider.
78-
func (c *CloudProvider) Refresh() error { panic("not implemented") }
118+
func (c *CloudProvider) Refresh() error { return nil }
79119

80120
// Name returns the name of the cloud provider.
81-
func (c *CloudProvider) Name() string { panic("not implemented") }
121+
func (c *CloudProvider) Name() string { return "Provider" }
82122

83123
// Pricing returns the pricing model associated with the provider.
84124
func (c *CloudProvider) Pricing() (cloudprovider.PricingModel, errors.AutoscalerError) {
85-
panic("not implemented")
125+
return nil, cloudprovider.ErrNotImplemented
86126
}
87127

88128
// GetAvailableMachineTypes returns the machine types supported by the provider.
89129
func (c *CloudProvider) GetAvailableMachineTypes() ([]string, error) {
90-
panic("not implemented")
130+
return nil, cloudprovider.ErrNotImplemented
91131
}
92132

93133
// NewNodeGroup creates a new node group based on the provided specifications.
94134
func (c *CloudProvider) NewNodeGroup(machineType string, labels map[string]string, systemLabels map[string]string,
95135
taints []apiv1.Taint, extraResources map[string]resource.Quantity) (cloudprovider.NodeGroup, error) {
96-
panic("not implemented")
136+
return nil, cloudprovider.ErrNotImplemented
97137
}
138+
139+
// NodeGroupOption is a function that configures a NodeGroup during creation.
140+
type NodeGroupOption func(*NodeGroup)
141+
142+
// WithNode adds a single initial node to the group and
143+
// automatically sets the group's template based on that node.
144+
func WithNode(node *apiv1.Node) NodeGroupOption {
145+
return func(n *NodeGroup) {
146+
n.provider.nodeToGroup[node.Name] = n.id
147+
n.instances[node.Name] = cloudprovider.InstanceRunning
148+
n.targetSize = 1
149+
n.template = framework.NewTestNodeInfo(node.DeepCopy())
150+
if n.provider.k8s != nil {
151+
n.provider.k8s.AddNode(node)
152+
}
153+
}
154+
}
155+
156+
// AddNodeGroup is a helper for tests to add a group with its template.
157+
func (c *CloudProvider) AddNodeGroup(id string, opts ...NodeGroupOption) {
158+
c.Lock()
159+
defer c.Unlock()
160+
161+
group := &NodeGroup{
162+
id: id,
163+
minSize: defaultMinSize,
164+
maxSize: defaultMaxSize,
165+
targetSize: 0,
166+
instances: make(map[string]cloudprovider.InstanceState),
167+
provider: c,
168+
}
169+
170+
for _, opt := range opts {
171+
opt(group)
172+
}
173+
c.groups[id] = group
174+
}
175+
176+
// GetNodeGroup is a helper for tests to get a node group.
177+
func (c *CloudProvider) GetNodeGroup(id string) cloudprovider.NodeGroup {
178+
c.Lock()
179+
defer c.Unlock()
180+
return c.groups[id]
181+
}
182+
183+
// AddNode connects a node name to a group ID.
184+
func (c *CloudProvider) AddNode(groupId string, node *apiv1.Node) {
185+
c.Lock()
186+
defer c.Unlock()
187+
c.nodeToGroup[node.Name] = groupId
188+
}
189+
190+
// SetResourceLimit allows the test to reach in and change the limits.
191+
func (c *CloudProvider) SetResourceLimit(resource string, min, max int64) {
192+
c.Lock()
193+
defer c.Unlock()
194+
c.minLimits[resource] = min
195+
c.maxLimits[resource] = max
196+
}
197+
198+
// NodeGroup is a fake implementation of the cloudprovider.NodeGroup interface for testing.
199+
type NodeGroup struct {
200+
sync.RWMutex
201+
id string
202+
minSize int
203+
maxSize int
204+
targetSize int
205+
template *framework.NodeInfo
206+
// instances maps instanceID -> state.
207+
instances map[string]cloudprovider.InstanceState
208+
provider *CloudProvider
209+
}
210+
211+
// MaxSize returns the maximum size of the node group.
212+
func (n *NodeGroup) MaxSize() int {
213+
return n.maxSize
214+
}
215+
216+
// MinSize returns the minimum size of the node group.
217+
func (n *NodeGroup) MinSize() int {
218+
return n.minSize
219+
}
220+
221+
// AtomicIncreaseSize is a version of IncreaseSize that increases the size of the node group atomically.
222+
func (n *NodeGroup) AtomicIncreaseSize(delta int) error {
223+
return n.IncreaseSize(delta)
224+
}
225+
226+
// DeleteNodes removes specific nodes from the node group and updates the internal mapping.
227+
func (n *NodeGroup) DeleteNodes(nodes []*apiv1.Node) error {
228+
n.Lock()
229+
defer n.Unlock()
230+
231+
n.provider.Lock()
232+
defer n.provider.Unlock()
233+
234+
deletedCount := 0
235+
for _, node := range nodes {
236+
if groupId, exists := n.provider.nodeToGroup[node.Name]; exists && groupId == n.id {
237+
delete(n.provider.nodeToGroup, node.Name)
238+
delete(n.instances, node.Name)
239+
if n.provider.k8s != nil {
240+
n.provider.k8s.DeleteNode(node.Name)
241+
}
242+
deletedCount++
243+
} else {
244+
fmt.Printf("Warning: node %s not found in group %s or already deleted.", node.Name, n.id)
245+
}
246+
}
247+
248+
if n.targetSize >= deletedCount {
249+
n.targetSize -= deletedCount
250+
} else {
251+
n.targetSize = 0
252+
}
253+
254+
return nil
255+
}
256+
257+
// ForceDeleteNodes deletes nodes without checking for specific conditions (fake implementation).
258+
func (n *NodeGroup) ForceDeleteNodes(nodes []*apiv1.Node) error {
259+
return n.DeleteNodes(nodes)
260+
}
261+
262+
// DecreaseTargetSize reduces the target size of the node group by the specified delta.
263+
func (n *NodeGroup) DecreaseTargetSize(delta int) error {
264+
n.Lock()
265+
defer n.Unlock()
266+
n.targetSize -= delta
267+
return nil
268+
}
269+
270+
// Id returns the unique identifier of the node group.
271+
func (n *NodeGroup) Id() string {
272+
return n.id
273+
}
274+
275+
// Debug returns a string representation of the node group's current state.
276+
func (n *NodeGroup) Debug() string {
277+
return fmt.Sprintf("NodeGroup{id: %s, targetSize: %d}", n.id, n.targetSize)
278+
}
279+
280+
// Nodes returns a list of all instances currently existing in this node group.
281+
func (n *NodeGroup) Nodes() ([]cloudprovider.Instance, error) {
282+
n.provider.Lock()
283+
defer n.provider.Unlock()
284+
285+
var instances []cloudprovider.Instance
286+
for id, state := range n.instances {
287+
instances = append(instances, cloudprovider.Instance{
288+
Id: id,
289+
Status: &cloudprovider.InstanceStatus{
290+
State: state,
291+
},
292+
})
293+
}
294+
return instances, nil
295+
}
296+
297+
// Exist returns true if the node group currently exists in the cloud provider.
298+
func (n *NodeGroup) Exist() bool {
299+
return true
300+
}
301+
302+
// Create creates the node group in the cloud provider (not implemented).
303+
func (n *NodeGroup) Create() (cloudprovider.NodeGroup, error) {
304+
return nil, cloudprovider.ErrNotImplemented
305+
}
306+
307+
// Delete deletes the node group from the cloud provider (not implemented).
308+
func (n *NodeGroup) Delete() error {
309+
return cloudprovider.ErrNotImplemented
310+
}
311+
312+
// Autoprovisioned returns true if the node group is autoprovisioned.
313+
func (n *NodeGroup) Autoprovisioned() bool {
314+
return false
315+
}
316+
317+
// GetOptions returns autoscaling options specific to this node group.
318+
func (n *NodeGroup) GetOptions(defaults config.NodeGroupAutoscalingOptions) (*config.NodeGroupAutoscalingOptions, error) {
319+
return nil, nil
320+
}
321+
322+
// TargetSize returns the current target size of the node group.
323+
func (n *NodeGroup) TargetSize() (int, error) { return n.targetSize, nil }
324+
325+
// IncreaseSize adds nodes to the node group and updates internal instance mapping.
326+
func (n *NodeGroup) IncreaseSize(delta int) error {
327+
n.Lock()
328+
defer n.Unlock()
329+
if n.targetSize+delta > n.maxSize {
330+
return fmt.Errorf("size too large")
331+
}
332+
333+
n.provider.Lock()
334+
defer n.provider.Unlock()
335+
336+
for i := 0; i < delta; i++ {
337+
instanceNum := n.targetSize + i
338+
instanceId := fmt.Sprintf("%s-node-%d", n.id, instanceNum)
339+
340+
if n.template == nil || n.template.Node() == nil {
341+
return fmt.Errorf("node group %s has no template to create new nodes", n.id)
342+
}
343+
newNode := n.template.Node().DeepCopy()
344+
newNode.Name = instanceId
345+
346+
n.instances[instanceId] = cloudprovider.InstanceRunning
347+
n.provider.nodeToGroup[instanceId] = n.id
348+
if n.provider.k8s != nil {
349+
n.provider.k8s.AddNode(newNode)
350+
}
351+
}
352+
n.targetSize += delta
353+
return nil
354+
}
355+
356+
// TemplateNodeInfo returns the template node information for this node group.
357+
func (n *NodeGroup) TemplateNodeInfo() (*framework.NodeInfo, error) {
358+
if n.template == nil {
359+
return nil, cloudprovider.ErrNotImplemented
360+
}
361+
return n.template, nil
362+
}
363+
364+
// GetTargetSize returns the target size as a raw integer (helper method).
365+
func (n *NodeGroup) GetTargetSize() int { return n.targetSize }

0 commit comments

Comments
 (0)