e0e362d700
SDK Tests / changes (push) Successful in 2m29s
Real E2E Tests / changes (push) Successful in 2m29s
Deploy Docs Pages / build (push) Has been cancelled
Deploy Docs Pages / deploy (push) Has been cancelled
Real E2E Tests / JavaScript E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Python E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Java E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / C# E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Go E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Real E2E CI (push) Has been cancelled
SDK Tests / SDK CI (push) Has been cancelled
SDK Tests / CLI Tests (push) Has been cancelled
SDK Tests / Python SDK Quality (code-interpreter) (push) Has been cancelled
SDK Tests / Python SDK Quality (sandbox) (push) Has been cancelled
SDK Tests / Python SDK Tests (code-interpreter) (push) Has been cancelled
SDK Tests / JavaScript SDK Quality And Tests (code-interpreter) (push) Has been cancelled
SDK Tests / JavaScript SDK Quality And Tests (sandbox) (push) Has been cancelled
SDK Tests / Python SDK Tests (sandbox) (push) Has been cancelled
SDK Tests / CLI Quality (push) Has been cancelled
SDK Tests / Kotlin SDK Quality And Tests (sandbox) (push) Has been cancelled
SDK Tests / Kotlin SDK Quality And Tests (code-interpreter) (push) Has been cancelled
SDK Tests / C# SDK Quality And Tests (code-interpreter) (push) Has been cancelled
SDK Tests / C# SDK Quality And Tests (sandbox) (push) Has been cancelled
SDK Tests / Go SDK Quality And Tests (push) Has been cancelled
244 lines
8.1 KiB
Go
244 lines
8.1 KiB
Go
// Copyright 2025 Alibaba Group Holding Ltd.
|
|
//
|
|
// 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 (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
|
|
corev1 "k8s.io/api/core/v1"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
"k8s.io/apimachinery/pkg/types"
|
|
"k8s.io/client-go/tools/record"
|
|
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
|
|
|
sandboxv1alpha1 "github.com/alibaba/OpenSandbox/sandbox-k8s/apis/sandbox/v1alpha1"
|
|
"github.com/alibaba/OpenSandbox/sandbox-k8s/internal/controller/algorithm"
|
|
"github.com/alibaba/OpenSandbox/sandbox-k8s/internal/controller/eviction"
|
|
)
|
|
|
|
type stubAllocator struct {
|
|
podAllocation map[string]string
|
|
}
|
|
|
|
func (a *stubAllocator) Schedule(_ context.Context, _ *AllocSpec) (*algorithm.AllocAction, error) {
|
|
return nil, nil
|
|
}
|
|
func (a *stubAllocator) GetPoolAllocation(_ context.Context, _ *sandboxv1alpha1.Pool) (map[string]string, error) {
|
|
return a.podAllocation, nil
|
|
}
|
|
func (a *stubAllocator) ClearPoolAllocation(_ context.Context, _ string, _ string) error {
|
|
return nil
|
|
}
|
|
func (a *stubAllocator) SyncSandboxAllocation(_ context.Context, _ *sandboxv1alpha1.BatchSandbox, _ []string) error {
|
|
return nil
|
|
}
|
|
func (a *stubAllocator) SyncSandboxReleased(_ context.Context, _ *sandboxv1alpha1.BatchSandbox, _ []string) error {
|
|
return nil
|
|
}
|
|
func (a *stubAllocator) GetSandboxAllocation(_ context.Context, _ *sandboxv1alpha1.BatchSandbox) ([]string, error) {
|
|
return nil, nil
|
|
}
|
|
func (a *stubAllocator) GetSandboxReleased(_ context.Context, _ *sandboxv1alpha1.BatchSandbox) ([]string, error) {
|
|
return nil, nil
|
|
}
|
|
func (a *stubAllocator) ReleasePodsAllocation(_ context.Context, _ string, _ string, _ []string) {}
|
|
|
|
func newEvictionTestPod(name string, labels map[string]string, deleting bool) *corev1.Pod {
|
|
pod := &corev1.Pod{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: name,
|
|
Namespace: "default",
|
|
Labels: labels,
|
|
},
|
|
}
|
|
if deleting {
|
|
now := metav1.NewTime(time.Now())
|
|
pod.DeletionTimestamp = &now
|
|
pod.Finalizers = []string{"test-finalizer"}
|
|
}
|
|
return pod
|
|
}
|
|
|
|
func newEvictionTestReconciler(podAllocation map[string]string, objs ...runtime.Object) *PoolReconciler {
|
|
scheme := runtime.NewScheme()
|
|
_ = corev1.AddToScheme(scheme)
|
|
_ = sandboxv1alpha1.AddToScheme(scheme)
|
|
|
|
builder := fake.NewClientBuilder().WithScheme(scheme)
|
|
for _, o := range objs {
|
|
if pod, ok := o.(*corev1.Pod); ok {
|
|
p := *pod
|
|
builder = builder.WithObjects(&p)
|
|
}
|
|
}
|
|
c := builder.Build()
|
|
|
|
return &PoolReconciler{
|
|
Client: c,
|
|
Scheme: scheme,
|
|
Recorder: record.NewFakeRecorder(10),
|
|
Allocator: &stubAllocator{podAllocation: podAllocation},
|
|
}
|
|
}
|
|
|
|
func TestHandleEviction(t *testing.T) {
|
|
pool := &sandboxv1alpha1.Pool{
|
|
ObjectMeta: metav1.ObjectMeta{Name: "test-pool", Namespace: "default"},
|
|
}
|
|
ctx := context.Background()
|
|
|
|
evictLabel := map[string]string{eviction.LabelEvict: ""}
|
|
normalLabel := map[string]string{"app": "test"}
|
|
|
|
t.Run("no eviction labels keeps all pods", func(t *testing.T) {
|
|
pods := []*corev1.Pod{
|
|
newEvictionTestPod("pod-1", normalLabel, false),
|
|
newEvictionTestPod("pod-2", normalLabel, false),
|
|
}
|
|
r := newEvictionTestReconciler(map[string]string{}, pods[0], pods[1])
|
|
|
|
got, err := r.handleEviction(ctx, pool, pods)
|
|
if err != nil {
|
|
t.Fatalf("handleEviction() returned error: %v", err)
|
|
}
|
|
expectNames := []string{"pod-1", "pod-2"}
|
|
if len(got) != len(expectNames) {
|
|
t.Fatalf("got %d pods, want %d", len(got), len(expectNames))
|
|
}
|
|
for i, pod := range got {
|
|
if pod.Name != expectNames[i] {
|
|
t.Errorf("pod[%d] = %s, want %s", i, pod.Name, expectNames[i])
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("unallocated eviction-labeled pods are evicted and excluded", func(t *testing.T) {
|
|
pod1 := newEvictionTestPod("pod-1", evictLabel, false)
|
|
pod2 := newEvictionTestPod("pod-2", normalLabel, false)
|
|
r := newEvictionTestReconciler(map[string]string{}, pod1, pod2)
|
|
|
|
got, err := r.handleEviction(ctx, pool, []*corev1.Pod{pod1, pod2})
|
|
if err != nil {
|
|
t.Fatalf("handleEviction() returned error: %v", err)
|
|
}
|
|
if len(got) != 1 || got[0].Name != "pod-2" {
|
|
t.Fatalf("got %v, want [pod-2]", got)
|
|
}
|
|
// pod-1 should be deleted
|
|
check := &corev1.Pod{}
|
|
if err := r.Client.Get(ctx, types.NamespacedName{Name: "pod-1", Namespace: "default"}, check); err == nil {
|
|
t.Error("expected pod-1 to be deleted")
|
|
}
|
|
})
|
|
|
|
t.Run("allocated eviction-labeled pods are kept", func(t *testing.T) {
|
|
pod1 := newEvictionTestPod("pod-1", evictLabel, false)
|
|
pod2 := newEvictionTestPod("pod-2", normalLabel, false)
|
|
alloc := map[string]string{"pod-1": "sandbox-1"}
|
|
r := newEvictionTestReconciler(alloc, pod1, pod2)
|
|
|
|
got, err := r.handleEviction(ctx, pool, []*corev1.Pod{pod1, pod2})
|
|
if err != nil {
|
|
t.Fatalf("handleEviction() returned error: %v", err)
|
|
}
|
|
expectNames := []string{"pod-1", "pod-2"}
|
|
if len(got) != len(expectNames) {
|
|
t.Fatalf("got %d pods, want %d", len(got), len(expectNames))
|
|
}
|
|
for i, pod := range got {
|
|
if pod.Name != expectNames[i] {
|
|
t.Errorf("pod[%d] = %s, want %s", i, pod.Name, expectNames[i])
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("mix of allocated and unallocated eviction-labeled pods", func(t *testing.T) {
|
|
pod1 := newEvictionTestPod("pod-1", evictLabel, false)
|
|
pod2 := newEvictionTestPod("pod-2", evictLabel, false)
|
|
pod3 := newEvictionTestPod("pod-3", normalLabel, false)
|
|
alloc := map[string]string{"pod-1": "sandbox-1"}
|
|
r := newEvictionTestReconciler(alloc, pod1, pod2, pod3)
|
|
|
|
got, err := r.handleEviction(ctx, pool, []*corev1.Pod{pod1, pod2, pod3})
|
|
if err != nil {
|
|
t.Fatalf("handleEviction() returned error: %v", err)
|
|
}
|
|
expectNames := []string{"pod-1", "pod-3"}
|
|
if len(got) != len(expectNames) {
|
|
t.Fatalf("got %d pods, want %d", len(got), len(expectNames))
|
|
}
|
|
for i, pod := range got {
|
|
if pod.Name != expectNames[i] {
|
|
t.Errorf("pod[%d] = %s, want %s", i, pod.Name, expectNames[i])
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("deleting pods with eviction label are not evicted", func(t *testing.T) {
|
|
pod1 := newEvictionTestPod("pod-1", evictLabel, true)
|
|
r := newEvictionTestReconciler(map[string]string{}, pod1)
|
|
|
|
got, err := r.handleEviction(ctx, pool, []*corev1.Pod{pod1})
|
|
if err != nil {
|
|
t.Fatalf("handleEviction() returned error: %v", err)
|
|
}
|
|
if len(got) != 1 || got[0].Name != "pod-1" {
|
|
t.Fatalf("expected deleting pod to be kept, got %v", got)
|
|
}
|
|
})
|
|
|
|
t.Run("empty pod list", func(t *testing.T) {
|
|
r := newEvictionTestReconciler(map[string]string{})
|
|
|
|
got, err := r.handleEviction(ctx, pool, []*corev1.Pod{})
|
|
if err != nil {
|
|
t.Fatalf("handleEviction() returned error: %v", err)
|
|
}
|
|
if len(got) != 0 {
|
|
t.Fatalf("expected empty result, got %d pods", len(got))
|
|
}
|
|
})
|
|
|
|
t.Run("evicts multiple idle pods and keeps allocated ones", func(t *testing.T) {
|
|
pod1 := newEvictionTestPod("pod-1", evictLabel, false)
|
|
pod2 := newEvictionTestPod("pod-2", evictLabel, false)
|
|
pod3 := newEvictionTestPod("pod-3", evictLabel, false)
|
|
alloc := map[string]string{"pod-2": "sandbox-1"}
|
|
r := newEvictionTestReconciler(alloc, pod1, pod2, pod3)
|
|
|
|
got, err := r.handleEviction(ctx, pool, []*corev1.Pod{pod1, pod2, pod3})
|
|
if err != nil {
|
|
t.Fatalf("handleEviction() returned error: %v", err)
|
|
}
|
|
if len(got) != 1 || got[0].Name != "pod-2" {
|
|
t.Fatalf("expected only pod-2, got %v", got)
|
|
}
|
|
|
|
check := &corev1.Pod{}
|
|
if err := r.Client.Get(ctx, types.NamespacedName{Name: "pod-1", Namespace: "default"}, check); err == nil {
|
|
t.Error("expected pod-1 to be deleted")
|
|
}
|
|
if err := r.Client.Get(ctx, types.NamespacedName{Name: "pod-2", Namespace: "default"}, check); err != nil {
|
|
t.Error("expected allocated pod-2 to still exist")
|
|
}
|
|
if err := r.Client.Get(ctx, types.NamespacedName{Name: "pod-3", Namespace: "default"}, check); err == nil {
|
|
t.Error("expected pod-3 to be deleted")
|
|
}
|
|
})
|
|
}
|