Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 13:39:33 +08:00

550 lines
23 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"
"encoding/json"
"testing"
"time"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
sandboxv1alpha1 "github.com/alibaba/OpenSandbox/sandbox-k8s/apis/sandbox/v1alpha1"
"github.com/alibaba/OpenSandbox/sandbox-k8s/internal/controller/algorithm"
)
// newTestAllocator creates an Allocator backed by mock store and syncer.
func newTestAllocator(ctrl *gomock.Controller) (Allocator, *MockAllocationStore, *MockAllocationSyncer) {
store := NewMockAllocationStore(ctrl)
syncer := NewMockAllocationSyncer(ctrl)
return &defaultAllocator{store: store, syncer: syncer, algorithm: &algorithm.PackedSchedule{}}, store, syncer
}
// --- Schedule ---
func TestSchedule(t *testing.T) {
replica1 := int32(1)
replica2 := int32(2)
tests := []struct {
name string
spec *AllocSpec
poolAlloc *PoolAllocation
sandboxAllocs map[string]*SandboxAllocation
releases map[string]*AllocationRelease
released map[string]*AllocationReleased
wantAction *algorithm.AllocAction
}{
{
name: "allocate normally - 2 pods for 2 sandboxes",
spec: &AllocSpec{
Pods: []*corev1.Pod{
{ObjectMeta: metav1.ObjectMeta{Name: "pod1"}, Status: corev1.PodStatus{Phase: corev1.PodRunning, Conditions: []corev1.PodCondition{{Type: corev1.PodReady, Status: corev1.ConditionTrue}}}},
{ObjectMeta: metav1.ObjectMeta{Name: "pod2"}, Status: corev1.PodStatus{Phase: corev1.PodRunning, Conditions: []corev1.PodCondition{{Type: corev1.PodReady, Status: corev1.ConditionTrue}}}},
},
Pool: &sandboxv1alpha1.Pool{ObjectMeta: metav1.ObjectMeta{Name: "pool1"}},
Sandboxes: []*sandboxv1alpha1.BatchSandbox{
{ObjectMeta: metav1.ObjectMeta{Name: "sbx1"}, Spec: sandboxv1alpha1.BatchSandboxSpec{Replicas: &replica1}},
{ObjectMeta: metav1.ObjectMeta{Name: "sbx2"}, Spec: sandboxv1alpha1.BatchSandboxSpec{Replicas: &replica1}},
},
},
poolAlloc: &PoolAllocation{PodAllocation: map[string]string{}},
sandboxAllocs: map[string]*SandboxAllocation{"sbx1": {Pods: []string{}}, "sbx2": {Pods: []string{}}},
releases: map[string]*AllocationRelease{"sbx1": {Pods: []string{}}, "sbx2": {Pods: []string{}}},
released: map[string]*AllocationReleased{"sbx1": {Pods: []string{}}, "sbx2": {Pods: []string{}}},
wantAction: &algorithm.AllocAction{
ToAllocate: map[string][]string{"sbx1": {"pod1"}, "sbx2": {"pod2"}},
ToRelease: map[string][]string{},
PodSupplement: 0,
},
},
{
name: "skip non-running pods",
spec: &AllocSpec{
Pods: []*corev1.Pod{
{ObjectMeta: metav1.ObjectMeta{Name: "pod1"}, Status: corev1.PodStatus{Phase: corev1.PodRunning, Conditions: []corev1.PodCondition{{Type: corev1.PodReady, Status: corev1.ConditionTrue}}}},
{ObjectMeta: metav1.ObjectMeta{Name: "pod2"}, Status: corev1.PodStatus{Phase: corev1.PodPending}},
},
Pool: &sandboxv1alpha1.Pool{ObjectMeta: metav1.ObjectMeta{Name: "pool1"}},
Sandboxes: []*sandboxv1alpha1.BatchSandbox{
{ObjectMeta: metav1.ObjectMeta{Name: "sbx1"}, Spec: sandboxv1alpha1.BatchSandboxSpec{Replicas: &replica1}},
{ObjectMeta: metav1.ObjectMeta{Name: "sbx2"}, Spec: sandboxv1alpha1.BatchSandboxSpec{Replicas: &replica1}},
},
},
poolAlloc: &PoolAllocation{PodAllocation: map[string]string{}},
sandboxAllocs: map[string]*SandboxAllocation{"sbx1": {Pods: []string{}}, "sbx2": {Pods: []string{}}},
releases: map[string]*AllocationRelease{"sbx1": {Pods: []string{}}, "sbx2": {Pods: []string{}}},
released: map[string]*AllocationReleased{"sbx1": {Pods: []string{}}, "sbx2": {Pods: []string{}}},
wantAction: &algorithm.AllocAction{
ToAllocate: map[string][]string{"sbx1": {"pod1"}},
ToRelease: map[string][]string{},
PodSupplement: 1,
},
},
{
name: "partial allocated - allocate remaining",
spec: &AllocSpec{
Pods: []*corev1.Pod{
{ObjectMeta: metav1.ObjectMeta{Name: "pod1"}, Status: corev1.PodStatus{Phase: corev1.PodRunning, Conditions: []corev1.PodCondition{{Type: corev1.PodReady, Status: corev1.ConditionTrue}}}},
{ObjectMeta: metav1.ObjectMeta{Name: "pod2"}, Status: corev1.PodStatus{Phase: corev1.PodRunning, Conditions: []corev1.PodCondition{{Type: corev1.PodReady, Status: corev1.ConditionTrue}}}},
},
Pool: &sandboxv1alpha1.Pool{ObjectMeta: metav1.ObjectMeta{Name: "pool1"}},
Sandboxes: []*sandboxv1alpha1.BatchSandbox{
{ObjectMeta: metav1.ObjectMeta{Name: "sbx1"}, Spec: sandboxv1alpha1.BatchSandboxSpec{Replicas: &replica2}},
},
},
poolAlloc: &PoolAllocation{PodAllocation: map[string]string{"pod1": "sbx1"}},
sandboxAllocs: map[string]*SandboxAllocation{"sbx1": {Pods: []string{"pod1"}}},
releases: map[string]*AllocationRelease{"sbx1": {Pods: []string{}}},
released: map[string]*AllocationReleased{"sbx1": {Pods: []string{}}},
wantAction: &algorithm.AllocAction{
ToAllocate: map[string][]string{"sbx1": {"pod2"}},
ToRelease: map[string][]string{},
PodSupplement: 0,
},
},
{
name: "with release - pods to release",
spec: &AllocSpec{
Pods: []*corev1.Pod{
{ObjectMeta: metav1.ObjectMeta{Name: "pod1"}, Status: corev1.PodStatus{Phase: corev1.PodRunning, Conditions: []corev1.PodCondition{{Type: corev1.PodReady, Status: corev1.ConditionTrue}}}},
},
Pool: &sandboxv1alpha1.Pool{ObjectMeta: metav1.ObjectMeta{Name: "pool1"}},
Sandboxes: []*sandboxv1alpha1.BatchSandbox{
{ObjectMeta: metav1.ObjectMeta{Name: "sbx1"}, Spec: sandboxv1alpha1.BatchSandboxSpec{Replicas: &replica1}},
},
},
poolAlloc: &PoolAllocation{PodAllocation: map[string]string{"pod1": "sbx1"}},
sandboxAllocs: map[string]*SandboxAllocation{"sbx1": {Pods: []string{"pod1"}}},
releases: map[string]*AllocationRelease{"sbx1": {Pods: []string{"pod1"}}},
released: map[string]*AllocationReleased{"sbx1": {Pods: []string{}}},
wantAction: &algorithm.AllocAction{
ToAllocate: map[string][]string{},
ToRelease: map[string][]string{"sbx1": {"pod1"}},
PodSupplement: 0,
},
},
{
name: "partial release - only unreleased pods in ToRelease",
spec: &AllocSpec{
Pods: []*corev1.Pod{
{ObjectMeta: metav1.ObjectMeta{Name: "pod1"}, Status: corev1.PodStatus{Phase: corev1.PodRunning, Conditions: []corev1.PodCondition{{Type: corev1.PodReady, Status: corev1.ConditionTrue}}}},
{ObjectMeta: metav1.ObjectMeta{Name: "pod2"}, Status: corev1.PodStatus{Phase: corev1.PodRunning, Conditions: []corev1.PodCondition{{Type: corev1.PodReady, Status: corev1.ConditionTrue}}}},
},
Pool: &sandboxv1alpha1.Pool{ObjectMeta: metav1.ObjectMeta{Name: "pool1"}},
Sandboxes: []*sandboxv1alpha1.BatchSandbox{
{ObjectMeta: metav1.ObjectMeta{Name: "sbx1"}, Spec: sandboxv1alpha1.BatchSandboxSpec{Replicas: &replica2}},
},
},
poolAlloc: &PoolAllocation{PodAllocation: map[string]string{"pod1": "sbx1", "pod2": "sbx1"}},
sandboxAllocs: map[string]*SandboxAllocation{"sbx1": {Pods: []string{"pod1", "pod2"}}},
releases: map[string]*AllocationRelease{"sbx1": {Pods: []string{"pod1", "pod2"}}},
released: map[string]*AllocationReleased{"sbx1": {Pods: []string{"pod1"}}},
wantAction: &algorithm.AllocAction{
ToAllocate: map[string][]string{},
ToRelease: map[string][]string{"sbx1": {"pod2"}},
PodSupplement: 0,
},
},
{
name: "not enough pods - PodSupplement > 0",
spec: &AllocSpec{
Pods: []*corev1.Pod{
{ObjectMeta: metav1.ObjectMeta{Name: "pod1"}, Status: corev1.PodStatus{Phase: corev1.PodRunning, Conditions: []corev1.PodCondition{{Type: corev1.PodReady, Status: corev1.ConditionTrue}}}},
},
Pool: &sandboxv1alpha1.Pool{ObjectMeta: metav1.ObjectMeta{Name: "pool1"}},
Sandboxes: []*sandboxv1alpha1.BatchSandbox{
{ObjectMeta: metav1.ObjectMeta{Name: "sbx1"}, Spec: sandboxv1alpha1.BatchSandboxSpec{Replicas: &replica2}},
},
},
poolAlloc: &PoolAllocation{PodAllocation: map[string]string{}},
sandboxAllocs: map[string]*SandboxAllocation{"sbx1": {Pods: []string{}}},
releases: map[string]*AllocationRelease{"sbx1": {Pods: []string{}}},
released: map[string]*AllocationReleased{"sbx1": {Pods: []string{}}},
wantAction: &algorithm.AllocAction{
ToAllocate: map[string][]string{"sbx1": {"pod1"}},
ToRelease: map[string][]string{},
PodSupplement: 1,
},
},
{
name: "skip already allocated pod - other sandbox gets supplement",
spec: &AllocSpec{
Pods: []*corev1.Pod{
{ObjectMeta: metav1.ObjectMeta{Name: "pod1"}, Status: corev1.PodStatus{Phase: corev1.PodRunning, Conditions: []corev1.PodCondition{{Type: corev1.PodReady, Status: corev1.ConditionTrue}}}},
},
Pool: &sandboxv1alpha1.Pool{ObjectMeta: metav1.ObjectMeta{Name: "pool1"}},
Sandboxes: []*sandboxv1alpha1.BatchSandbox{
{ObjectMeta: metav1.ObjectMeta{Name: "sbx1"}, Spec: sandboxv1alpha1.BatchSandboxSpec{Replicas: &replica1}},
{ObjectMeta: metav1.ObjectMeta{Name: "sbx2"}, Spec: sandboxv1alpha1.BatchSandboxSpec{Replicas: &replica1}},
},
},
poolAlloc: &PoolAllocation{PodAllocation: map[string]string{"pod1": "sbx1"}},
sandboxAllocs: map[string]*SandboxAllocation{"sbx1": {Pods: []string{"pod1"}}, "sbx2": {Pods: []string{}}},
releases: map[string]*AllocationRelease{"sbx1": {Pods: []string{}}, "sbx2": {Pods: []string{}}},
released: map[string]*AllocationReleased{"sbx1": {Pods: []string{}}, "sbx2": {Pods: []string{}}},
wantAction: &algorithm.AllocAction{
ToAllocate: map[string][]string{},
ToRelease: map[string][]string{},
PodSupplement: 1,
},
},
{
name: "terminating sandbox - queue unreleased pods for release, no supplement",
spec: &AllocSpec{
Pods: []*corev1.Pod{
{ObjectMeta: metav1.ObjectMeta{Name: "pod1"}, Status: corev1.PodStatus{Phase: corev1.PodRunning, Conditions: []corev1.PodCondition{{Type: corev1.PodReady, Status: corev1.ConditionTrue}}}},
{ObjectMeta: metav1.ObjectMeta{Name: "pod2"}, Status: corev1.PodStatus{Phase: corev1.PodRunning, Conditions: []corev1.PodCondition{{Type: corev1.PodReady, Status: corev1.ConditionTrue}}}},
},
Pool: &sandboxv1alpha1.Pool{ObjectMeta: metav1.ObjectMeta{Name: "pool1"}},
Sandboxes: []*sandboxv1alpha1.BatchSandbox{
{
ObjectMeta: metav1.ObjectMeta{
Name: "sbx1",
DeletionTimestamp: &metav1.Time{Time: time.Now()},
},
Spec: sandboxv1alpha1.BatchSandboxSpec{Replicas: &replica2},
},
},
},
poolAlloc: &PoolAllocation{PodAllocation: map[string]string{"pod1": "sbx1", "pod2": "sbx1"}},
sandboxAllocs: map[string]*SandboxAllocation{"sbx1": {Pods: []string{"pod1", "pod2"}}},
releases: map[string]*AllocationRelease{},
released: map[string]*AllocationReleased{"sbx1": {Pods: []string{"pod1"}}},
wantAction: &algorithm.AllocAction{
ToAllocate: map[string][]string{},
ToRelease: map[string][]string{"sbx1": {"pod2"}},
PodSupplement: 0,
},
},
{
name: "orphan sandbox - pods in store but sandbox no longer in spec",
spec: &AllocSpec{
Pods: []*corev1.Pod{
{ObjectMeta: metav1.ObjectMeta{Name: "pod1"}, Status: corev1.PodStatus{Phase: corev1.PodRunning, Conditions: []corev1.PodCondition{{Type: corev1.PodReady, Status: corev1.ConditionTrue}}}},
{ObjectMeta: metav1.ObjectMeta{Name: "pod2"}, Status: corev1.PodStatus{Phase: corev1.PodRunning, Conditions: []corev1.PodCondition{{Type: corev1.PodReady, Status: corev1.ConditionTrue}}}},
},
Pool: &sandboxv1alpha1.Pool{ObjectMeta: metav1.ObjectMeta{Name: "pool1"}},
// sbx-orphan is not in spec (e.g. force-deleted), but pod1/pod2 still in store.
Sandboxes: []*sandboxv1alpha1.BatchSandbox{},
},
poolAlloc: &PoolAllocation{PodAllocation: map[string]string{"pod1": "sbx-orphan", "pod2": "sbx-orphan"}},
sandboxAllocs: map[string]*SandboxAllocation{},
releases: map[string]*AllocationRelease{},
released: map[string]*AllocationReleased{},
wantAction: &algorithm.AllocAction{
ToAllocate: map[string][]string{},
ToRelease: map[string][]string{"sbx-orphan": {"pod1", "pod2"}},
PodSupplement: 0,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
allocator, store, syncer := newTestAllocator(ctrl)
store.EXPECT().Recover(gomock.Any(), gomock.Any()).Return(nil).Times(1)
store.EXPECT().GetAllocation(gomock.Any(), gomock.Any()).Return(tt.poolAlloc, nil).Times(1)
for _, sbx := range tt.spec.Sandboxes {
syncer.EXPECT().GetAllocation(gomock.Any(), sbx).Return(tt.sandboxAllocs[sbx.Name], nil).Times(1)
syncer.EXPECT().GetReleased(gomock.Any(), sbx).Return(tt.released[sbx.Name], nil).Times(1)
// Terminating sandboxes skip GetRelease; only active sandboxes need it.
if sbx.DeletionTimestamp.IsZero() {
syncer.EXPECT().GetRelease(gomock.Any(), sbx).Return(tt.releases[sbx.Name], nil).Times(1)
}
}
action, err := allocator.Schedule(context.Background(), tt.spec)
assert.NoError(t, err)
assert.Equal(t, tt.wantAction.ToAllocate, action.ToAllocate)
assert.Equal(t, tt.wantAction.PodSupplement, action.PodSupplement)
// ToRelease values may be in any order when built from a map (e.g. orphan sandbox GC).
assert.Equal(t, len(tt.wantAction.ToRelease), len(action.ToRelease))
for sandboxName, wantPods := range tt.wantAction.ToRelease {
assert.ElementsMatch(t, wantPods, action.ToRelease[sandboxName])
}
})
}
}
// --- GetPoolAllocation ---
func TestGetPoolAllocation(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
allocator, store, _ := newTestAllocator(ctrl)
pool := &sandboxv1alpha1.Pool{ObjectMeta: metav1.ObjectMeta{Name: "pool1"}}
store.EXPECT().GetAllocation(gomock.Any(), pool).Return(&PoolAllocation{
PodAllocation: map[string]string{"pod1": "sbx1"},
}, nil).Times(1)
alloc, err := allocator.GetPoolAllocation(context.Background(), pool)
assert.NoError(t, err)
assert.Equal(t, map[string]string{"pod1": "sbx1"}, alloc)
}
func TestGetPoolAllocation_Empty(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
allocator, store, _ := newTestAllocator(ctrl)
pool := &sandboxv1alpha1.Pool{ObjectMeta: metav1.ObjectMeta{Name: "pool1"}}
store.EXPECT().GetAllocation(gomock.Any(), pool).Return(nil, nil).Times(1)
alloc, err := allocator.GetPoolAllocation(context.Background(), pool)
assert.NoError(t, err)
assert.Equal(t, map[string]string{}, alloc)
}
// --- ClearPoolAllocation ---
func TestClearPoolAllocation(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
allocator, store, _ := newTestAllocator(ctrl)
store.EXPECT().ClearAllocation(gomock.Any(), "ns1", "pool1").Return(nil).Times(1)
err := allocator.ClearPoolAllocation(context.Background(), "ns1", "pool1")
assert.NoError(t, err)
}
// --- annoAllocationSyncer: finalizer behavior ---
// newTestSyncer creates an annoAllocationSyncer backed by a fake k8s client
// with the given sandbox pre-created.
func newTestSyncer(sandbox *sandboxv1alpha1.BatchSandbox) (*annoAllocationSyncer, *sandboxv1alpha1.BatchSandbox) {
scheme := runtime.NewScheme()
_ = sandboxv1alpha1.AddToScheme(scheme)
fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(sandbox).Build()
return &annoAllocationSyncer{client: fakeClient}, sandbox
}
func TestSetAllocation_AddsFinalizer(t *testing.T) {
sandbox := &sandboxv1alpha1.BatchSandbox{
ObjectMeta: metav1.ObjectMeta{Name: "sbx1", Namespace: "default"},
Spec: sandboxv1alpha1.BatchSandboxSpec{PoolRef: "pool1"},
}
syncer, sbx := newTestSyncer(sandbox)
err := syncer.SetAllocation(context.Background(), sbx, &SandboxAllocation{Pods: []string{"pod1"}})
assert.NoError(t, err)
assert.Contains(t, sbx.Finalizers, FinalizerPoolAllocation)
}
func TestSetReleased_FinalizerBehavior(t *testing.T) {
now := metav1.NewTime(time.Now())
tests := []struct {
name string
allocated []string
released []string
deletionTimestamp *metav1.Time
finalizersBefore []string
wantFinalizer bool
}{
{
name: "not deleting - finalizer kept regardless of release coverage",
allocated: []string{"pod1"},
released: []string{"pod1"},
finalizersBefore: []string{FinalizerPoolAllocation},
wantFinalizer: true,
},
{
name: "deleting - partial release - finalizer kept",
allocated: []string{"pod1", "pod2"},
released: []string{"pod1"},
deletionTimestamp: &now,
finalizersBefore: []string{FinalizerPoolAllocation},
wantFinalizer: true,
},
{
name: "deleting - all pods released - finalizer removed",
allocated: []string{"pod1", "pod2"},
released: []string{"pod1", "pod2"},
deletionTimestamp: &now,
finalizersBefore: []string{FinalizerPoolAllocation},
wantFinalizer: false,
},
{
name: "deleting - no pods allocated - finalizer removed",
allocated: []string{},
released: []string{},
deletionTimestamp: &now,
finalizersBefore: []string{FinalizerPoolAllocation},
wantFinalizer: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
allocJSON, _ := marshalJSON(&SandboxAllocation{Pods: tt.allocated})
sandbox := &sandboxv1alpha1.BatchSandbox{
ObjectMeta: metav1.ObjectMeta{
Name: "sbx1",
Namespace: "default",
Finalizers: tt.finalizersBefore,
DeletionTimestamp: tt.deletionTimestamp,
Annotations: map[string]string{
AnnoAllocStatusKey: allocJSON,
},
},
}
syncer, sbx := newTestSyncer(sandbox)
err := syncer.SetReleased(context.Background(), sbx, &AllocationReleased{Pods: tt.released})
assert.NoError(t, err)
if tt.wantFinalizer {
assert.Contains(t, sbx.Finalizers, FinalizerPoolAllocation, "finalizer should be kept")
} else {
assert.NotContains(t, sbx.Finalizers, FinalizerPoolAllocation, "finalizer should be removed")
}
})
}
}
// marshalJSON is a test helper that marshals v to a JSON string.
func marshalJSON(v any) (string, error) {
b, err := json.Marshal(v)
return string(b), err
}
func TestSyncSandboxAllocation_Success(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
allocator, store, syncer := newTestAllocator(ctrl)
sandbox := &sandboxv1alpha1.BatchSandbox{
ObjectMeta: metav1.ObjectMeta{Name: "sbx1", Namespace: "ns1"},
Spec: sandboxv1alpha1.BatchSandboxSpec{PoolRef: "pool1"},
}
newPods := []string{"pod1", "pod2"}
// Phase 1: snapshot old state, then update memory.
syncer.EXPECT().GetAllocation(gomock.Any(), sandbox).Return(&SandboxAllocation{Pods: []string{}}, nil).Times(1)
store.EXPECT().UpdateAllocation(gomock.Any(), "ns1", "pool1", "sbx1", newPods).Times(1)
// Phase 2: persist to annotation.
syncer.EXPECT().SetAllocation(gomock.Any(), sandbox, gomock.Any()).DoAndReturn(
func(ctx context.Context, sbx *sandboxv1alpha1.BatchSandbox, alloc *SandboxAllocation) error {
assert.Equal(t, newPods, alloc.Pods)
return nil
}).Times(1)
err := allocator.SyncSandboxAllocation(context.Background(), sandbox, newPods)
assert.NoError(t, err)
}
func TestSyncSandboxAllocation_GetFailed(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
allocator, _, syncer := newTestAllocator(ctrl)
sandbox := &sandboxv1alpha1.BatchSandbox{
ObjectMeta: metav1.ObjectMeta{Name: "sbx1", Namespace: "ns1"},
Spec: sandboxv1alpha1.BatchSandboxSpec{PoolRef: "pool1"},
}
// GetAllocation fails: should return early without touching store or SetAllocation.
syncer.EXPECT().GetAllocation(gomock.Any(), sandbox).Return(nil, assert.AnError).Times(1)
err := allocator.SyncSandboxAllocation(context.Background(), sandbox, []string{"pod1"})
assert.Error(t, err)
}
func TestSyncSandboxAllocation_SetFailed_Rollback(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
allocator, store, syncer := newTestAllocator(ctrl)
sandbox := &sandboxv1alpha1.BatchSandbox{
ObjectMeta: metav1.ObjectMeta{Name: "sbx1", Namespace: "ns1"},
Spec: sandboxv1alpha1.BatchSandboxSpec{PoolRef: "pool1"},
}
oldPods := []string{"pod-old"}
newPods := []string{"pod1"}
syncer.EXPECT().GetAllocation(gomock.Any(), sandbox).Return(&SandboxAllocation{Pods: oldPods}, nil).Times(1)
// Phase 1: optimistic memory update.
store.EXPECT().UpdateAllocation(gomock.Any(), "ns1", "pool1", "sbx1", newPods).Times(1)
// Phase 2: annotation fails → rollback memory to old state.
syncer.EXPECT().SetAllocation(gomock.Any(), sandbox, gomock.Any()).Return(assert.AnError).Times(1)
store.EXPECT().UpdateAllocation(gomock.Any(), "ns1", "pool1", "sbx1", oldPods).Times(1)
err := allocator.SyncSandboxAllocation(context.Background(), sandbox, newPods)
assert.Error(t, err)
}
// --- SyncSandboxReleased ---
func TestSyncSandboxReleased_Success(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
allocator, store, syncer := newTestAllocator(ctrl)
sandbox := &sandboxv1alpha1.BatchSandbox{
ObjectMeta: metav1.ObjectMeta{Name: "sbx1", Namespace: "ns1"},
Spec: sandboxv1alpha1.BatchSandboxSpec{PoolRef: "pool1"},
}
pods := []string{"pod1", "pod2"}
// Phase 1: persist to annotation first to prevent premature re-allocation.
syncer.EXPECT().SetReleased(gomock.Any(), sandbox, gomock.Any()).DoAndReturn(
func(ctx context.Context, sbx *sandboxv1alpha1.BatchSandbox, released *AllocationReleased) error {
assert.Equal(t, pods, released.Pods)
return nil
}).Times(1)
// Phase 2: release from memory only after annotation is committed.
store.EXPECT().ReleaseAllocation(gomock.Any(), "ns1", "pool1", pods).Times(1)
err := allocator.SyncSandboxReleased(context.Background(), sandbox, pods)
assert.NoError(t, err)
}
func TestSyncSandboxReleased_SetFailed(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
allocator, _, syncer := newTestAllocator(ctrl)
sandbox := &sandboxv1alpha1.BatchSandbox{
ObjectMeta: metav1.ObjectMeta{Name: "sbx1", Namespace: "ns1"},
Spec: sandboxv1alpha1.BatchSandboxSpec{PoolRef: "pool1"},
}
pods := []string{"pod1"}
syncer.EXPECT().SetReleased(gomock.Any(), sandbox, gomock.Any()).Return(assert.AnError).Times(1)
// ReleaseAllocation must NOT be called when annotation sync fails:
// pods remain "in use" in the memory store to prevent re-allocation.
err := allocator.SyncSandboxReleased(context.Background(), sandbox, pods)
assert.Error(t, err)
}