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
434 lines
11 KiB
Go
434 lines
11 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"
|
|
|
|
v1 "k8s.io/api/core/v1"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/util/intstr"
|
|
|
|
sandboxv1alpha1 "github.com/alibaba/OpenSandbox/sandbox-k8s/apis/sandbox/v1alpha1"
|
|
)
|
|
|
|
func TestResolveMaxUnavailable(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
pool *sandboxv1alpha1.Pool
|
|
desiredTotal int32
|
|
want int32
|
|
}{
|
|
{
|
|
name: "default 25% of 10 = 3 (rounded up)",
|
|
pool: &sandboxv1alpha1.Pool{
|
|
Spec: sandboxv1alpha1.PoolSpec{},
|
|
},
|
|
desiredTotal: 10,
|
|
want: 3,
|
|
},
|
|
{
|
|
name: "default 25% of 4 = 1",
|
|
pool: &sandboxv1alpha1.Pool{
|
|
Spec: sandboxv1alpha1.PoolSpec{},
|
|
},
|
|
desiredTotal: 4,
|
|
want: 1,
|
|
},
|
|
{
|
|
name: "custom percentage 50% of 10 = 5",
|
|
pool: &sandboxv1alpha1.Pool{
|
|
Spec: sandboxv1alpha1.PoolSpec{
|
|
UpdateStrategy: &sandboxv1alpha1.UpdateStrategy{
|
|
MaxUnavailable: intStrPtr("50%"),
|
|
},
|
|
},
|
|
},
|
|
desiredTotal: 10,
|
|
want: 5,
|
|
},
|
|
{
|
|
name: "custom absolute value 3",
|
|
pool: &sandboxv1alpha1.Pool{
|
|
Spec: sandboxv1alpha1.PoolSpec{
|
|
UpdateStrategy: &sandboxv1alpha1.UpdateStrategy{
|
|
MaxUnavailable: intStrIntPtr(3),
|
|
},
|
|
},
|
|
},
|
|
desiredTotal: 10,
|
|
want: 3,
|
|
},
|
|
{
|
|
name: "absolute value 0 defaults to 1",
|
|
pool: &sandboxv1alpha1.Pool{
|
|
Spec: sandboxv1alpha1.PoolSpec{
|
|
UpdateStrategy: &sandboxv1alpha1.UpdateStrategy{
|
|
MaxUnavailable: intStrPtr("0"),
|
|
},
|
|
},
|
|
},
|
|
desiredTotal: 10,
|
|
want: 1,
|
|
},
|
|
{
|
|
name: "percentage rounds up - 10% of 9 = 1",
|
|
pool: &sandboxv1alpha1.Pool{
|
|
Spec: sandboxv1alpha1.PoolSpec{
|
|
UpdateStrategy: &sandboxv1alpha1.UpdateStrategy{
|
|
MaxUnavailable: intStrPtr("10%"),
|
|
},
|
|
},
|
|
},
|
|
desiredTotal: 9,
|
|
want: 1,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := getUpdateMaxUnavailable(tt.pool, tt.desiredTotal)
|
|
if got != tt.want {
|
|
t.Errorf("resolveMaxUnavailable() = %v, want %v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRecreateUpdateStrategy_Compute(t *testing.T) {
|
|
ctx := context.Background()
|
|
updateRevision := "v2"
|
|
|
|
tests := []struct {
|
|
name string
|
|
pool *sandboxv1alpha1.Pool
|
|
pods []*v1.Pod
|
|
idlePods []string
|
|
wantIdlePods []string
|
|
wantDeletePods []string
|
|
wantSupplyNew int32
|
|
}{
|
|
{
|
|
name: "all pods already at update revision - no deletion",
|
|
pool: &sandboxv1alpha1.Pool{
|
|
Spec: sandboxv1alpha1.PoolSpec{
|
|
UpdateStrategy: &sandboxv1alpha1.UpdateStrategy{
|
|
MaxUnavailable: intStrPtr("25%"),
|
|
},
|
|
},
|
|
},
|
|
pods: []*v1.Pod{
|
|
makePod("pod-1", "v2", true, true),
|
|
makePod("pod-2", "v2", true, true),
|
|
},
|
|
idlePods: []string{"pod-1", "pod-2"},
|
|
wantIdlePods: []string{"pod-1", "pod-2"},
|
|
wantDeletePods: []string{},
|
|
wantSupplyNew: 0,
|
|
},
|
|
{
|
|
name: "all idle pods at old revision - delete within budget",
|
|
pool: &sandboxv1alpha1.Pool{
|
|
Spec: sandboxv1alpha1.PoolSpec{
|
|
UpdateStrategy: &sandboxv1alpha1.UpdateStrategy{
|
|
MaxUnavailable: intStrPtr("100%"),
|
|
},
|
|
},
|
|
},
|
|
pods: []*v1.Pod{
|
|
makePod("pod-1", "v1", true, true),
|
|
makePod("pod-2", "v1", true, true),
|
|
},
|
|
idlePods: []string{"pod-1", "pod-2"},
|
|
wantIdlePods: []string{},
|
|
wantDeletePods: []string{"pod-1", "pod-2"},
|
|
wantSupplyNew: 2,
|
|
},
|
|
{
|
|
name: "old revision pods exceed budget - partial deletion",
|
|
pool: &sandboxv1alpha1.Pool{
|
|
Spec: sandboxv1alpha1.PoolSpec{
|
|
UpdateStrategy: &sandboxv1alpha1.UpdateStrategy{
|
|
MaxUnavailable: intStrPtr("25%"),
|
|
},
|
|
},
|
|
},
|
|
pods: []*v1.Pod{
|
|
makePod("pod-1", "v1", true, true),
|
|
makePod("pod-2", "v1", true, true),
|
|
makePod("pod-3", "v1", true, true),
|
|
makePod("pod-4", "v1", true, true),
|
|
},
|
|
idlePods: []string{"pod-1", "pod-2", "pod-3", "pod-4"},
|
|
wantIdlePods: []string{"pod-1", "pod-2", "pod-3"},
|
|
wantDeletePods: []string{"pod-4"},
|
|
wantSupplyNew: 1,
|
|
},
|
|
{
|
|
name: "unavailable pods don't consume budget",
|
|
pool: &sandboxv1alpha1.Pool{
|
|
Spec: sandboxv1alpha1.PoolSpec{
|
|
UpdateStrategy: &sandboxv1alpha1.UpdateStrategy{
|
|
MaxUnavailable: intStrPtr("25%"),
|
|
},
|
|
},
|
|
},
|
|
pods: []*v1.Pod{
|
|
makePod("pod-1", "v1", false, true),
|
|
makePod("pod-2", "v1", true, true),
|
|
makePod("pod-3", "v1", true, true),
|
|
makePod("pod-4", "v1", true, true),
|
|
},
|
|
idlePods: []string{"pod-1", "pod-2", "pod-3", "pod-4"},
|
|
wantIdlePods: []string{"pod-2", "pod-3", "pod-4"},
|
|
wantDeletePods: []string{"pod-1"},
|
|
wantSupplyNew: 1,
|
|
},
|
|
{
|
|
name: "mixed revisions - only delete old revision",
|
|
pool: &sandboxv1alpha1.Pool{
|
|
Spec: sandboxv1alpha1.PoolSpec{
|
|
UpdateStrategy: &sandboxv1alpha1.UpdateStrategy{
|
|
MaxUnavailable: intStrPtr("100%"),
|
|
},
|
|
},
|
|
},
|
|
pods: []*v1.Pod{
|
|
makePod("pod-1", "v1", true, true),
|
|
makePod("pod-2", "v2", true, true),
|
|
makePod("pod-3", "v1", true, true),
|
|
},
|
|
idlePods: []string{"pod-1", "pod-2", "pod-3"},
|
|
wantIdlePods: []string{"pod-2"},
|
|
wantDeletePods: []string{"pod-1", "pod-3"},
|
|
wantSupplyNew: 2,
|
|
},
|
|
{
|
|
name: "allocated pods not in idle list are ignored",
|
|
pool: &sandboxv1alpha1.Pool{
|
|
Spec: sandboxv1alpha1.PoolSpec{
|
|
UpdateStrategy: &sandboxv1alpha1.UpdateStrategy{
|
|
MaxUnavailable: intStrPtr("100%"),
|
|
},
|
|
},
|
|
},
|
|
pods: []*v1.Pod{
|
|
makePod("pod-1", "v1", true, true),
|
|
makePod("pod-2", "v1", true, false),
|
|
makePod("pod-3", "v1", true, true),
|
|
},
|
|
idlePods: []string{"pod-1", "pod-3"},
|
|
wantIdlePods: []string{},
|
|
wantDeletePods: []string{"pod-1", "pod-3"},
|
|
wantSupplyNew: 2,
|
|
},
|
|
{
|
|
name: "unready pods count towards curUnavailable",
|
|
pool: &sandboxv1alpha1.Pool{
|
|
Spec: sandboxv1alpha1.PoolSpec{
|
|
UpdateStrategy: &sandboxv1alpha1.UpdateStrategy{
|
|
MaxUnavailable: intStrPtr("50%"),
|
|
},
|
|
},
|
|
},
|
|
pods: []*v1.Pod{
|
|
makePod("pod-1", "v1", false, false),
|
|
makePod("pod-2", "v1", false, false),
|
|
makePod("pod-3", "v1", true, true),
|
|
makePod("pod-4", "v1", true, true),
|
|
},
|
|
idlePods: []string{"pod-3", "pod-4"},
|
|
wantIdlePods: []string{"pod-3", "pod-4"},
|
|
wantDeletePods: []string{},
|
|
wantSupplyNew: 0,
|
|
},
|
|
{
|
|
name: "empty idle pods",
|
|
pool: &sandboxv1alpha1.Pool{
|
|
Spec: sandboxv1alpha1.PoolSpec{
|
|
UpdateStrategy: &sandboxv1alpha1.UpdateStrategy{
|
|
MaxUnavailable: intStrPtr("25%"),
|
|
},
|
|
},
|
|
},
|
|
pods: []*v1.Pod{
|
|
makePod("pod-1", "v1", true, false),
|
|
makePod("pod-2", "v1", true, false),
|
|
},
|
|
idlePods: []string{},
|
|
wantIdlePods: []string{},
|
|
wantDeletePods: []string{},
|
|
wantSupplyNew: 0,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
resetPodCounter()
|
|
strategy := &recreateUpdateStrategy{pool: tt.pool}
|
|
result := strategy.Compute(ctx, updateRevision, tt.pods, tt.idlePods)
|
|
|
|
if !stringSlicesEqualUnordered(result.IdlePods, tt.wantIdlePods) {
|
|
t.Errorf("IdlePods = %v, want %v", result.IdlePods, tt.wantIdlePods)
|
|
}
|
|
if !stringSlicesEqualUnordered(result.ToDeletePods, tt.wantDeletePods) {
|
|
t.Errorf("ToDeletePods = %v, want %v", result.ToDeletePods, tt.wantDeletePods)
|
|
}
|
|
if result.SupplyUpdateRevision != tt.wantSupplyNew {
|
|
t.Errorf("SupplyUpdateRevision = %v, want %v", result.SupplyUpdateRevision, tt.wantSupplyNew)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRecreateUpdateStrategy_Compute_Sorting(t *testing.T) {
|
|
ctx := context.Background()
|
|
updateRevision := "v2"
|
|
|
|
pool := &sandboxv1alpha1.Pool{
|
|
Spec: sandboxv1alpha1.PoolSpec{
|
|
UpdateStrategy: &sandboxv1alpha1.UpdateStrategy{
|
|
MaxUnavailable: intStrPtr("25%"),
|
|
},
|
|
},
|
|
}
|
|
|
|
now := metav1.Now()
|
|
older := metav1.NewTime(now.Add(-2 * time.Hour))
|
|
newer := metav1.NewTime(now.Add(-1 * time.Hour))
|
|
|
|
pods := []*v1.Pod{
|
|
{
|
|
ObjectMeta: metav1.ObjectMeta{Name: "assigned-running", CreationTimestamp: newer},
|
|
Spec: v1.PodSpec{NodeName: "node-1"},
|
|
Status: v1.PodStatus{
|
|
Phase: v1.PodRunning,
|
|
Conditions: []v1.PodCondition{
|
|
{Type: v1.PodReady, Status: v1.ConditionTrue, LastTransitionTime: older},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
ObjectMeta: metav1.ObjectMeta{Name: "unassigned-pending", CreationTimestamp: older},
|
|
Spec: v1.PodSpec{NodeName: ""},
|
|
Status: v1.PodStatus{Phase: v1.PodPending},
|
|
},
|
|
{
|
|
ObjectMeta: metav1.ObjectMeta{Name: "assigned-not-ready", CreationTimestamp: now},
|
|
Spec: v1.PodSpec{NodeName: "node-2"},
|
|
Status: v1.PodStatus{Phase: v1.PodRunning, Conditions: []v1.PodCondition{}},
|
|
},
|
|
}
|
|
|
|
for _, pod := range pods {
|
|
if pod.Labels == nil {
|
|
pod.Labels = make(map[string]string)
|
|
}
|
|
pod.Labels[LabelPoolRevision] = "v1"
|
|
}
|
|
|
|
strategy := &recreateUpdateStrategy{pool: pool}
|
|
result := strategy.Compute(ctx, updateRevision, pods, []string{"assigned-running", "unassigned-pending", "assigned-not-ready"})
|
|
|
|
wantDelete := []string{"unassigned-pending", "assigned-not-ready"}
|
|
wantIdle := []string{"assigned-running"}
|
|
|
|
if !stringSlicesEqual(result.ToDeletePods, wantDelete) {
|
|
t.Errorf("ToDeletePods = %v, want %v", result.ToDeletePods, wantDelete)
|
|
}
|
|
if !stringSlicesEqual(result.IdlePods, wantIdle) {
|
|
t.Errorf("IdlePods = %v, want %v", result.IdlePods, wantIdle)
|
|
}
|
|
}
|
|
|
|
var podCreationCounter int
|
|
|
|
func makePod(name, revision string, ready, idle bool) *v1.Pod {
|
|
podCreationCounter++
|
|
pod := &v1.Pod{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: name,
|
|
CreationTimestamp: metav1.NewTime(
|
|
metav1.Now().Add(time.Duration(podCreationCounter) * time.Second),
|
|
),
|
|
Labels: map[string]string{
|
|
LabelPoolRevision: revision,
|
|
},
|
|
},
|
|
Spec: v1.PodSpec{
|
|
NodeName: "node-1",
|
|
},
|
|
Status: v1.PodStatus{
|
|
Phase: v1.PodRunning,
|
|
},
|
|
}
|
|
|
|
if ready {
|
|
pod.Status.Conditions = []v1.PodCondition{
|
|
{Type: v1.PodReady, Status: v1.ConditionTrue},
|
|
}
|
|
}
|
|
|
|
if !idle {
|
|
pod.Labels["allocated"] = "true"
|
|
}
|
|
|
|
return pod
|
|
}
|
|
|
|
func resetPodCounter() {
|
|
podCreationCounter = 0
|
|
}
|
|
|
|
func intStrPtr(s string) *intstr.IntOrString {
|
|
val := intstr.FromString(s)
|
|
return &val
|
|
}
|
|
|
|
func intStrIntPtr(i int) *intstr.IntOrString {
|
|
val := intstr.FromInt(i)
|
|
return &val
|
|
}
|
|
|
|
func stringSlicesEqual(a, b []string) bool {
|
|
if len(a) != len(b) {
|
|
return false
|
|
}
|
|
for i := range a {
|
|
if a[i] != b[i] {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func stringSlicesEqualUnordered(a, b []string) bool {
|
|
if len(a) != len(b) {
|
|
return false
|
|
}
|
|
set := make(map[string]bool, len(a))
|
|
for _, s := range a {
|
|
set[s] = true
|
|
}
|
|
for _, s := range b {
|
|
if !set[s] {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|