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

1092 lines
34 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"
gerrors "errors"
"fmt"
"net"
"reflect"
"strconv"
"sync"
"testing"
"time"
"github.com/golang/mock/gomock"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/runtime"
k8sruntime "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/rand"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/client-go/tools/record"
"k8s.io/client-go/util/retry"
"k8s.io/utils/ptr"
"k8s.io/utils/set"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
sandboxv1alpha1 "github.com/alibaba/OpenSandbox/sandbox-k8s/apis/sandbox/v1alpha1"
"github.com/alibaba/OpenSandbox/sandbox-k8s/internal/controller/strategy"
taskscheduler "github.com/alibaba/OpenSandbox/sandbox-k8s/internal/scheduler"
mock_scheduler "github.com/alibaba/OpenSandbox/sandbox-k8s/internal/scheduler/mock"
"github.com/alibaba/OpenSandbox/sandbox-k8s/internal/utils/fieldindex"
)
func init() {
testscheme = k8sruntime.NewScheme()
utilruntime.Must(corev1.AddToScheme(testscheme))
utilruntime.Must(sandboxv1alpha1.AddToScheme(testscheme))
}
var testscheme *k8sruntime.Scheme
var _ = Describe("BatchSandbox Controller", func() {
var (
timeout = 30 * time.Second
interval = 5 * time.Second
)
// None Pooling Mode
Context("When create new batch sandbox, create pod base on pod template", func() {
const resourceBaseName = "test-batch-sandbox"
ctx := context.Background()
typeNamespacedName := types.NamespacedName{
Name: resourceBaseName,
Namespace: "default",
}
BeforeEach(func() {
typeNamespacedName.Name = fmt.Sprintf("%s-%s", resourceBaseName, rand.String(5))
By(fmt.Sprintf("creating the custom resource %s for the Kind BatchSandbox", typeNamespacedName))
resource := &sandboxv1alpha1.BatchSandbox{
ObjectMeta: metav1.ObjectMeta{
Name: typeNamespacedName.Name,
Namespace: typeNamespacedName.Namespace,
},
Spec: sandboxv1alpha1.BatchSandboxSpec{
Replicas: ptr.To(int32(3)),
Template: &v1.PodTemplateSpec{
Spec: v1.PodSpec{
Containers: []v1.Container{
{
Name: "main",
Image: "example.com",
},
},
},
},
},
}
Expect(k8sClient.Create(ctx, resource)).Should(Succeed())
bs := &sandboxv1alpha1.BatchSandbox{}
Eventually(func(g Gomega) {
g.Expect(k8sClient.Get(ctx, typeNamespacedName, bs)).To(Succeed())
}, timeout, interval).Should(Succeed())
By(fmt.Sprintf("wait the custom resource %s created", typeNamespacedName))
})
AfterEach(func() {
resource := &sandboxv1alpha1.BatchSandbox{}
err := k8sClient.Get(ctx, typeNamespacedName, resource)
if !errors.IsNotFound(err) {
Expect(err).NotTo(HaveOccurred())
} else {
return
}
By(fmt.Sprintf("Cleanup the specific resource instance BatchSandbox %s", typeNamespacedName))
Expect(k8sClient.Delete(ctx, resource)).To(Succeed())
})
It("should successfully create pod, update batch sandbox status, endpoints info", func() {
wantIPSet := make(set.Set[string])
podIPMap := make(map[string]string)
Eventually(func(g Gomega) {
bs := &sandboxv1alpha1.BatchSandbox{}
if err := k8sClient.Get(ctx, typeNamespacedName, bs); err != nil {
return
}
allPods := &corev1.PodList{}
g.Expect(k8sClient.List(ctx, allPods, &client.ListOptions{Namespace: bs.Namespace})).Should(Succeed())
pods := []*corev1.Pod{}
for i := range allPods.Items {
po := &allPods.Items[i]
if metav1.IsControlledBy(po, bs) {
pods = append(pods, po)
if po.Status.PodIP != "" {
continue
}
if i%2 == 0 {
mockIP := randomIPv4().String()
wantIPSet.Insert(mockIP)
podIPMap[po.Name] = mockIP
po.Status.PodIP = mockIP
po.Status.Phase = corev1.PodRunning
po.Status.Conditions = []corev1.PodCondition{{Type: corev1.PodReady, Status: corev1.ConditionTrue}}
Expect(k8sClient.Status().Update(context.Background(), po)).To(Succeed())
}
}
}
g.Expect(len(pods)).To(Equal(int(*bs.Spec.Replicas)))
g.Expect(bs.Status.ObservedGeneration).To(Equal(bs.Generation))
g.Expect(bs.Status.Replicas).To(Equal(*bs.Spec.Replicas))
gotIPs := []string{}
if raw := bs.Annotations[AnnotationSandboxEndpoints]; raw != "" {
json.Unmarshal([]byte(raw), &gotIPs)
}
podIndex, err := calPodIndex(strategy.NewPoolStrategy(bs), bs, pods)
g.Expect(err).NotTo(HaveOccurred())
expectedIPs := make([]string, len(pods))
for _, pod := range pods {
idx, ok := podIndex[pod.Name]
g.Expect(ok).To(BeTrue(), fmt.Sprintf("pod %s should have index", pod.Name))
if pod.Status.PodIP != "" {
expectedIPs[idx] = pod.Status.PodIP
} else {
expectedIPs[idx] = ""
}
}
g.Expect(gotIPs).To(Equal(expectedIPs), "endpoints should be ordered by pod index, unassigned pods should have empty string")
}, timeout, interval).Should(Succeed())
})
It("should successfully correctly create new Pod and update batch sandbox status when user scale out", func() {
Expect(retry.RetryOnConflict(retry.DefaultBackoff, func() error {
bs := &sandboxv1alpha1.BatchSandbox{}
if err := k8sClient.Get(ctx, typeNamespacedName, bs); err != nil {
return err
}
*bs.Spec.Replicas = *bs.Spec.Replicas + 1 // scale out using the latest resourceVersion
return k8sClient.Update(ctx, bs)
})).Should(Succeed())
Eventually(func(g Gomega) {
batchsandbox := &sandboxv1alpha1.BatchSandbox{}
if err := k8sClient.Get(ctx, typeNamespacedName, batchsandbox); err != nil {
return
}
g.Expect(batchsandbox.Status.ObservedGeneration).To(Equal(batchsandbox.Generation))
g.Expect(batchsandbox.Status.Replicas).To(Equal(*batchsandbox.Spec.Replicas))
}, timeout, interval).Should(Succeed())
Eventually(func(g Gomega) {
bs := &sandboxv1alpha1.BatchSandbox{}
g.Expect(k8sClient.Get(ctx, typeNamespacedName, bs)).To(Succeed())
pods := &v1.PodList{}
g.Expect(k8sClient.List(ctx, pods, &client.ListOptions{
Namespace: bs.Namespace,
FieldSelector: fields.SelectorFromSet(fields.Set{fieldindex.IndexNameForOwnerRefUID: string(bs.UID)}),
})).Should(Succeed())
g.Expect(int32(len(pods.Items))).To(Equal(*bs.Spec.Replicas))
}, timeout, interval).Should(Succeed())
})
It("should successfully correctly supply Pod when pod is deleted unexpectedly", func() {
Eventually(func(g Gomega) {
bs := &sandboxv1alpha1.BatchSandbox{}
if err := k8sClient.Get(ctx, typeNamespacedName, bs); err != nil {
return
}
g.Expect(bs.Status.ObservedGeneration).To(Equal(bs.Generation))
g.Expect(bs.Status.Replicas).To(Equal(*bs.Spec.Replicas))
}, timeout, interval).Should(Succeed())
bs := &sandboxv1alpha1.BatchSandbox{}
Expect(k8sClient.Get(ctx, typeNamespacedName, bs)).Should(Succeed())
pods := &v1.PodList{}
Expect(k8sClient.List(ctx, pods, &client.ListOptions{
Namespace: bs.Namespace,
FieldSelector: fields.SelectorFromSet(fields.Set{fieldindex.IndexNameForOwnerRefUID: string(bs.UID)}),
})).Should(Succeed())
Expect(int32(len(pods.Items))).To(Equal(*bs.Spec.Replicas))
// delete first pod
oldPod := pods.Items[0]
Expect(k8sClient.Delete(ctx, &oldPod)).Should(Succeed())
// wait supply pod
Eventually(func(g Gomega) {
newPod := &corev1.Pod{}
if err := k8sClient.Get(ctx, types.NamespacedName{
Namespace: bs.Namespace,
Name: oldPod.Name,
}, newPod); err != nil {
return
}
g.Expect(newPod.CreationTimestamp).NotTo(Equal(oldPod.CreationTimestamp))
}, timeout, interval).Should(Succeed())
})
It("should delete batch sandbox and related Pods for expired batch sandbox", func() {
Expect(retry.RetryOnConflict(retry.DefaultRetry, func() error {
bs := &sandboxv1alpha1.BatchSandbox{}
if err := k8sClient.Get(ctx, typeNamespacedName, bs); err != nil {
return err
}
bs.Spec.ExpireTime = &metav1.Time{Time: time.Now().Add(3 * time.Second)}
return k8sClient.Update(ctx, bs)
})).Should(Succeed())
Eventually(
func(g Gomega) {
bs := &sandboxv1alpha1.BatchSandbox{}
g.Expect(errors.IsNotFound(k8sClient.Get(ctx, typeNamespacedName, bs))).To(BeTrue())
allPods := &corev1.PodList{}
g.Expect(k8sClient.List(ctx, allPods, &client.ListOptions{Namespace: bs.Namespace})).Should(Succeed())
pods := []*corev1.Pod{}
for i := range allPods.Items {
po := &allPods.Items[i]
if metav1.IsControlledBy(po, bs) {
pods = append(pods, po)
}
}
g.Expect(len(pods)).To(BeZero())
},
timeout, interval).Should(Succeed())
})
})
// None Pooling Mode - Heterogeneous Pods
Context("When create new batch sandbox with ShardPatches, create heterogeneous pods", func() {
const resourceBaseName = "test-batch-sandbox-shard"
ctx := context.Background()
typeNamespacedName := types.NamespacedName{
Name: resourceBaseName,
Namespace: "default",
}
BeforeEach(func() {
typeNamespacedName.Name = fmt.Sprintf("%s-%s", resourceBaseName, rand.String(5))
By(fmt.Sprintf("creating the custom resource %s for the Kind BatchSandbox with ShardPatches", typeNamespacedName))
resource := &sandboxv1alpha1.BatchSandbox{
ObjectMeta: metav1.ObjectMeta{
Name: typeNamespacedName.Name,
Namespace: typeNamespacedName.Namespace,
},
Spec: sandboxv1alpha1.BatchSandboxSpec{
Replicas: ptr.To(int32(3)),
Template: &v1.PodTemplateSpec{
Spec: v1.PodSpec{
Containers: []v1.Container{
{
Name: "main",
Image: "example.com",
Command: []string{"default-command"},
},
},
},
},
ShardPatches: []runtime.RawExtension{
{
Raw: []byte(`{"spec":{"containers":[{"name":"main","command":["custom-command-0"]}]}}`),
},
{
Raw: []byte(`{"spec":{"containers":[{"name":"main","command":["custom-command-1"]}]}}`),
},
{
Raw: []byte(`{"spec":{"containers":[{"name":"main","command":["custom-command-2"]}]}}`),
},
},
},
}
Expect(k8sClient.Create(ctx, resource)).Should(Succeed())
bs := &sandboxv1alpha1.BatchSandbox{}
Eventually(func(g Gomega) {
g.Expect(k8sClient.Get(ctx, typeNamespacedName, bs)).To(Succeed())
}, timeout, interval).Should(Succeed())
By(fmt.Sprintf("wait the custom resource %s created", typeNamespacedName))
})
AfterEach(func() {
resource := &sandboxv1alpha1.BatchSandbox{}
err := k8sClient.Get(ctx, typeNamespacedName, resource)
if !errors.IsNotFound(err) {
Expect(err).NotTo(HaveOccurred())
} else {
return
}
By(fmt.Sprintf("Cleanup the specific resource instance BatchSandbox %s", typeNamespacedName))
Expect(k8sClient.Delete(ctx, resource)).To(Succeed())
})
It("should successfully create heterogeneous pods with different commands", func() {
Eventually(func(g Gomega) {
bs := &sandboxv1alpha1.BatchSandbox{}
if err := k8sClient.Get(ctx, typeNamespacedName, bs); err != nil {
return
}
allPods := &corev1.PodList{}
g.Expect(k8sClient.List(ctx, allPods, &client.ListOptions{Namespace: bs.Namespace})).Should(Succeed())
pods := []*corev1.Pod{}
for i := range allPods.Items {
po := &allPods.Items[i]
if metav1.IsControlledBy(po, bs) {
pods = append(pods, po)
}
}
g.Expect(len(pods)).To(Equal(int(*bs.Spec.Replicas)))
// Verify each pod has the correct patched command
for _, pod := range pods {
indexLabel := pod.Labels[LabelBatchSandboxPodIndexKey]
g.Expect(indexLabel).NotTo(BeEmpty())
idx, err := strconv.Atoi(indexLabel)
g.Expect(err).NotTo(HaveOccurred())
g.Expect(idx).To(BeNumerically(">=", 0))
g.Expect(idx).To(BeNumerically("<", int(*bs.Spec.Replicas)))
// Verify the command was patched
g.Expect(len(pod.Spec.Containers)).To(BeNumerically(">", 0))
mainContainer := pod.Spec.Containers[0]
expectedCommand := fmt.Sprintf("custom-command-%d", idx)
g.Expect(mainContainer.Command).To(Equal([]string{expectedCommand}))
}
g.Expect(bs.Status.ObservedGeneration).To(Equal(bs.Generation))
g.Expect(bs.Status.Replicas).To(Equal(*bs.Spec.Replicas))
}, timeout, interval).Should(Succeed())
})
})
// Pooling Mode
Context("When create new batch sandbox, get pod from pool", func() {
const resourceBaseName = "test-batch-sandbox-pooling-mode"
var replicas int32 = 3
ctx := context.Background()
typeNamespacedName := types.NamespacedName{
Name: resourceBaseName,
Namespace: "default",
}
BeforeEach(func() {
typeNamespacedName.Name = fmt.Sprintf("%s-%s", resourceBaseName, rand.String(5))
By(fmt.Sprintf("creating the custom resource %s for the Kind BatchSandbox", typeNamespacedName))
resource := &sandboxv1alpha1.BatchSandbox{
ObjectMeta: metav1.ObjectMeta{
Name: typeNamespacedName.Name,
Namespace: typeNamespacedName.Namespace,
},
Spec: sandboxv1alpha1.BatchSandboxSpec{
Replicas: ptr.To(replicas),
PoolRef: "test-pool",
},
}
Expect(k8sClient.Create(ctx, resource)).Should(Succeed())
bs := &sandboxv1alpha1.BatchSandbox{}
Eventually(func(g Gomega) {
g.Expect(k8sClient.Get(ctx, typeNamespacedName, bs)).To(Succeed())
}, timeout, interval).Should(Succeed())
By(fmt.Sprintf("wait the custom resource %s created", typeNamespacedName))
})
AfterEach(func() {
resource := &sandboxv1alpha1.BatchSandbox{}
err := k8sClient.Get(ctx, typeNamespacedName, resource)
if !errors.IsNotFound(err) {
Expect(err).NotTo(HaveOccurred())
}
By(fmt.Sprintf("Cleanup the specific resource instance BatchSandbox %s", typeNamespacedName))
Expect(k8sClient.Delete(ctx, resource)).To(Succeed())
})
It("should successfully update batch sandbox status, sbx endpoints info when get pod from pool alloc", func() {
// mock pool allocation
mockPods := []string{}
for i := range replicas {
po := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Namespace: typeNamespacedName.Namespace,
Name: fmt.Sprintf("test-pod-%d", i),
},
Spec: v1.PodSpec{
Containers: []v1.Container{
{Name: "main", Image: "test", Command: []string{"hello"}},
},
},
}
mockPods = append(mockPods, po.Name)
Expect(k8sClient.Create(context.Background(), po)).To(Succeed())
if i%2 == 0 {
po.Spec.NodeName = "node-1.2.3.4"
po.Status.PodIP = fmt.Sprintf("1.2.3.%d", i+1)
po.Status.Phase = corev1.PodRunning
po.Status.Conditions = []corev1.PodCondition{{Type: corev1.PodReady, Status: corev1.ConditionTrue}}
}
Expect(k8sClient.Status().Update(context.Background(), po)).To(Succeed())
}
Expect(retry.RetryOnConflict(retry.DefaultRetry, func() error {
bs := &sandboxv1alpha1.BatchSandbox{}
if err := k8sClient.Get(ctx, typeNamespacedName, bs); err != nil {
return err
}
setSandboxAllocation(bs, SandboxAllocation{Pods: mockPods})
return k8sClient.Update(ctx, bs)
})).Should(Succeed())
By(fmt.Sprintf("Mock pool allocate Pod %v for BatchSandbox %s", mockPods, typeNamespacedName))
Eventually(func(g Gomega) {
bs := &sandboxv1alpha1.BatchSandbox{}
if err := k8sClient.Get(ctx, typeNamespacedName, bs); err != nil {
return
}
g.Expect(bs.Status.ObservedGeneration).To(Equal(bs.Generation))
g.Expect(bs.Status.Replicas).To(Equal(*bs.Spec.Replicas))
gotIPs := []string{}
if raw := bs.Annotations[AnnotationSandboxEndpoints]; raw != "" {
json.Unmarshal([]byte(raw), &gotIPs)
}
alloc, err := parseSandboxAllocation(bs)
g.Expect(err).NotTo(HaveOccurred())
expectedIPs := make([]string, len(alloc.Pods))
for idx, podName := range alloc.Pods {
pod := &corev1.Pod{}
err := k8sClient.Get(ctx, types.NamespacedName{Namespace: bs.Namespace, Name: podName}, pod)
g.Expect(err).NotTo(HaveOccurred())
if pod.Spec.NodeName != "" || pod.Status.PodIP != "" {
expectedIPs[idx] = pod.Status.PodIP
} else {
expectedIPs[idx] = ""
}
}
g.Expect(gotIPs).To(Equal(expectedIPs), "endpoints should be ordered by pool allocation order, unassigned pods should have empty string")
}, timeout, interval).Should(Succeed())
})
})
})
func randomIPv4() net.IP {
rand.Seed(time.Now().UnixNano())
ip := make(net.IP, 4)
for i := range ip {
ip[i] = byte(rand.Intn(256))
}
return ip
}
var _ = Describe("BatchSandbox Task Scheduler", func() {
var (
timeout = 30 * time.Second
interval = 5 * time.Second
)
// None Pooling mode
Context("When create new batch sandbox, create pod base on pod template", func() {
const resourceBaseName = "test-task-batch-sandbox"
ctx := context.Background()
typeNamespacedName := types.NamespacedName{
Name: resourceBaseName,
Namespace: "default",
}
BeforeEach(func() {
typeNamespacedName.Name = fmt.Sprintf("%s-%s", resourceBaseName, rand.String(5))
By(fmt.Sprintf("creating the custom resource %s for the Kind BatchSandbox", typeNamespacedName))
resource := &sandboxv1alpha1.BatchSandbox{
ObjectMeta: metav1.ObjectMeta{
Name: typeNamespacedName.Name,
Namespace: typeNamespacedName.Namespace,
},
Spec: sandboxv1alpha1.BatchSandboxSpec{
Replicas: ptr.To(int32(1)),
Template: &v1.PodTemplateSpec{
Spec: v1.PodSpec{
Containers: []v1.Container{
{
Name: "main",
Image: "example.com",
},
},
},
},
TaskTemplate: &sandboxv1alpha1.TaskTemplateSpec{
Spec: sandboxv1alpha1.TaskSpec{
Process: &sandboxv1alpha1.ProcessTask{
Command: []string{"echo", "hello"},
},
},
},
},
}
Expect(k8sClient.Create(ctx, resource)).Should(Succeed())
bs := &sandboxv1alpha1.BatchSandbox{}
Eventually(func(g Gomega) {
g.Expect(k8sClient.Get(ctx, typeNamespacedName, bs)).To(Succeed())
}, timeout, interval).Should(Succeed())
By(fmt.Sprintf("wait the custom resource %s created", typeNamespacedName))
})
AfterEach(func() {
resource := &sandboxv1alpha1.BatchSandbox{}
err := k8sClient.Get(ctx, typeNamespacedName, resource)
if !errors.IsNotFound(err) {
Expect(err).NotTo(HaveOccurred())
} else {
// resource is already deleted
return
}
By(fmt.Sprintf("Cleanup the specific resource instance BatchSandbox %s", typeNamespacedName))
Expect(k8sClient.Delete(ctx, resource)).To(Succeed())
})
It("should successfully add task cleanup finalizer", func() {
Eventually(func(g Gomega) {
bs := &sandboxv1alpha1.BatchSandbox{}
if err := k8sClient.Get(ctx, typeNamespacedName, bs); err != nil {
return
}
g.Expect(controllerutil.ContainsFinalizer(bs, FinalizerTaskCleanup)).To(BeTrue())
}, timeout, interval).Should(Succeed())
})
It("should successfully update task status(task_pending=1), because all pods is unassigned", func() {
Eventually(func(g Gomega) {
bs := &sandboxv1alpha1.BatchSandbox{}
if err := k8sClient.Get(ctx, typeNamespacedName, bs); err != nil {
return
}
g.Expect(bs.Status.ObservedGeneration).To(Equal(bs.Generation))
g.Expect(bs.Status.Replicas).To(Equal(*bs.Spec.Replicas))
g.Expect(bs.Status.TaskPending).To(Equal(*bs.Spec.Replicas))
g.Expect(bs.Status.TaskRunning).To(Equal(int32(0)))
g.Expect(bs.Status.TaskSucceed).To(Equal(int32(0)))
g.Expect(bs.Status.TaskFailed).To(Equal(int32(0)))
g.Expect(bs.Status.TaskUnknown).To(Equal(int32(0)))
}, timeout, interval).Should(Succeed())
})
It("should successfully delete BatchSandbox when all tasks(including pending task) cleanup is finished", func() {
bs := &sandboxv1alpha1.BatchSandbox{}
Expect(k8sClient.Get(ctx, typeNamespacedName, bs)).To(Succeed())
Eventually(func(g Gomega) {
bs := &sandboxv1alpha1.BatchSandbox{}
Expect(k8sClient.Get(ctx, typeNamespacedName, bs)).To(Succeed())
g.Expect(controllerutil.ContainsFinalizer(bs, FinalizerTaskCleanup)).To(BeTrue())
}, timeout, interval).Should(Succeed())
By(fmt.Sprintf("try to Delete BatchSandbox %s", typeNamespacedName))
Expect(k8sClient.Delete(ctx, bs)).To(Succeed())
Eventually(func(g Gomega) {
bs := &sandboxv1alpha1.BatchSandbox{}
err := k8sClient.Get(ctx, typeNamespacedName, bs)
g.Expect(errors.IsNotFound(err)).To(BeTrue())
}, timeout, interval).Should(Succeed())
})
})
})
func TestBatchSandboxReconciler_scheduleTasks(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
var (
fakeBatchSandbox = &sandboxv1alpha1.BatchSandbox{
TypeMeta: metav1.TypeMeta{
APIVersion: sandboxv1alpha1.GroupVersion.String(),
Kind: "BatchSandbox",
},
ObjectMeta: metav1.ObjectMeta{
Name: "test-batch-sandbox",
},
Spec: sandboxv1alpha1.BatchSandboxSpec{},
Status: sandboxv1alpha1.BatchSandboxStatus{},
}
)
type fields struct {
Client client.Client
Scheme *runtime.Scheme
Recorder record.EventRecorder
taskSchedulers sync.Map
}
type args struct {
ctx context.Context
tSch taskscheduler.TaskScheduler
batchSbx *sandboxv1alpha1.BatchSandbox
}
tests := []struct {
name string
fields fields
args args
wantErr bool
wantTaskStatus *taskScheduleResult
batchSandboxChecker func(bsbx *sandboxv1alpha1.BatchSandbox) error
}{
{
name: "schedule err",
args: args{
tSch: func() taskscheduler.TaskScheduler {
mockSche := mock_scheduler.NewMockTaskScheduler(ctrl)
mockSche.EXPECT().Schedule().Return(gerrors.New("err")).Times(1)
return mockSche
}(),
},
wantErr: true,
},
{
name: "tasks, succeed=1; releasedPod=1",
fields: fields{
Client: fake.NewClientBuilder().WithScheme(testscheme).WithObjects(fakeBatchSandbox).WithStatusSubresource(fakeBatchSandbox).Build(),
Recorder: record.NewFakeRecorder(10),
},
args: args{
tSch: func() taskscheduler.TaskScheduler {
mockSche := mock_scheduler.NewMockTaskScheduler(ctrl)
mockSche.EXPECT().Schedule().Return(nil).Times(1)
mockTask := mock_scheduler.NewMockTask(ctrl)
mockTask.EXPECT().GetState().Return(taskscheduler.SucceedTaskState).Times(1)
mockTask.EXPECT().IsResourceReleased().Return(true).Times(1)
mockTask.EXPECT().GetPodName().Return("pod-0").AnyTimes()
mockSche.EXPECT().ListTask().Return([]taskscheduler.Task{mockTask}).Times(1)
return mockSche
}(),
batchSbx: fakeBatchSandbox.DeepCopy(),
},
wantTaskStatus: &taskScheduleResult{Succeed: 1},
batchSandboxChecker: func(bsbx *sandboxv1alpha1.BatchSandbox) error {
release, err := parseSandboxReleased(bsbx)
if err != nil {
return err
}
if len(release.Pods) != 1 || release.Pods[0] != "pod-0" {
return fmt.Errorf("expect pod-0, actual %v", release.Pods)
}
return nil
},
},
}
for i := range tests {
tt := &tests[i]
t.Run(tt.name, func(t *testing.T) {
r := &BatchSandboxReconciler{
Client: tt.fields.Client,
Scheme: tt.fields.Scheme,
Recorder: tt.fields.Recorder,
}
gotTaskStatus, err := r.scheduleTasks(tt.args.ctx, tt.args.tSch, tt.args.batchSbx)
if (err != nil) != tt.wantErr {
t.Errorf("BatchSandboxReconciler.scheduleTasks() error = %v, wantErr %v", err, tt.wantErr)
}
if !tt.wantErr && !reflect.DeepEqual(gotTaskStatus, tt.wantTaskStatus) {
t.Errorf("BatchSandboxReconciler.scheduleTasks() = %v, want %v", gotTaskStatus, tt.wantTaskStatus)
}
if tt.wantErr && gotTaskStatus != nil {
t.Errorf("BatchSandboxReconciler.scheduleTasks() = %v, want nil when error", gotTaskStatus)
}
if tt.batchSandboxChecker != nil {
bsbx := &sandboxv1alpha1.BatchSandbox{}
if err := tt.fields.Client.Get(ctx, types.NamespacedName{Namespace: tt.args.batchSbx.Namespace, Name: tt.args.batchSbx.Name}, bsbx); err != nil {
t.Errorf("BatchSandboxReconciler Get() error = %v, wantErr %v", err, nil)
}
if err := tt.batchSandboxChecker(bsbx); err != nil {
t.Errorf("BatchSandboxReconciler batchSandboxChecker() error = %v, wantErr %v", err, nil)
}
}
})
}
}
func TestBatchSandboxReconciler_reconcileTasks(t *testing.T) {
ctx := context.Background()
type fields struct {
Client client.Client
Scheme *runtime.Scheme
Recorder record.EventRecorder
}
type args struct {
ctx context.Context
batchSbx *sandboxv1alpha1.BatchSandbox
pods []*corev1.Pod
}
tests := []struct {
name string
fields fields
args args
wantErr bool
checker func(r *BatchSandboxReconciler, batchSbx *sandboxv1alpha1.BatchSandbox) error
}{
{
name: "deletion with finalizer cleanup success",
fields: func() fields {
return fields{
Client: fake.NewClientBuilder().WithScheme(testscheme).Build(),
}
}(),
args: func() args {
return args{
ctx: ctx,
pods: []*corev1.Pod{},
}
}(),
wantErr: false,
checker: func(r *BatchSandboxReconciler, batchSbx *sandboxv1alpha1.BatchSandbox) error {
// Verify finalizer is removed by getting from client
updated := &sandboxv1alpha1.BatchSandbox{}
if err := r.Client.Get(context.Background(), types.NamespacedName{Namespace: batchSbx.Namespace, Name: batchSbx.Name}, updated); err != nil {
return fmt.Errorf("failed to get updated batchsandbox: %w", err)
}
if controllerutil.ContainsFinalizer(updated, FinalizerTaskCleanup) {
return fmt.Errorf("finalizer should be removed after cleanup, finalizers: %v", updated.Finalizers)
}
return nil
},
},
}
for i := range tests {
tt := &tests[i]
t.Run(tt.name, func(t *testing.T) {
r := &BatchSandboxReconciler{
Client: tt.fields.Client,
Scheme: tt.fields.Scheme,
Recorder: tt.fields.Recorder,
}
// Setup: create BatchSandbox with finalizer
bsbx := &sandboxv1alpha1.BatchSandbox{
ObjectMeta: metav1.ObjectMeta{
Name: "test-batch-cleanup",
Namespace: "default",
UID: types.UID("test-uid-cleanup"),
Finalizers: []string{FinalizerTaskCleanup},
},
Spec: sandboxv1alpha1.BatchSandboxSpec{
Replicas: ptr.To(int32(0)),
},
}
if err := r.Client.Create(ctx, bsbx); err != nil {
t.Fatalf("failed to create batchsandbox: %v", err)
}
// Set deletion timestamp after creation (fake client doesn't preserve it during Create)
now := metav1.Now()
bsbx.DeletionTimestamp = &now
got, err := r.reconcileTasks(tt.args.ctx, bsbx, tt.args.pods)
if (err != nil) != tt.wantErr {
t.Errorf("BatchSandboxReconciler.reconcileTasks() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !tt.wantErr && got == nil && tt.checker == nil {
t.Errorf("BatchSandboxReconciler.reconcileTasks() = nil, want non-nil")
}
if tt.checker != nil {
if err := tt.checker(r, bsbx); err != nil {
t.Errorf("BatchSandboxReconciler checker() error = %v", err)
}
}
})
}
}
func Test_parseIndex(t *testing.T) {
type args struct {
pod *corev1.Pod
}
tests := []struct {
name string
args args
want int
wantErr bool
}{
{
name: "from label",
args: args{
pod: &v1.Pod{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{LabelBatchSandboxPodIndexKey: "1"},
Name: "sbx-0"}},
},
want: 1,
},
{
name: "from name",
args: args{
pod: &v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "sbx-0"}},
},
want: 0,
},
{
name: "invalid name",
args: args{
pod: &v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "sbx"}},
},
want: -1,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := parseIndex(tt.args.pod)
if (err != nil) != tt.wantErr {
t.Errorf("parseIndex() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("parseIndex() = %v, want %v", got, tt.want)
}
})
}
}
func Test_calPodIndex(t *testing.T) {
type args struct {
batchSbx *sandboxv1alpha1.BatchSandbox
pods []*corev1.Pod
}
tests := []struct {
name string
args args
want map[string]int
wantErr bool
}{
{
name: "pool mode - valid allocation",
args: args{
batchSbx: &sandboxv1alpha1.BatchSandbox{
ObjectMeta: metav1.ObjectMeta{
Name: "test-batch",
Namespace: "default",
Annotations: map[string]string{
AnnoAllocStatusKey: `{"pods":["pod-0","pod-1","pod-2"]}`,
},
},
Spec: sandboxv1alpha1.BatchSandboxSpec{
PoolRef: "test-pool",
},
},
pods: []*corev1.Pod{
{ObjectMeta: metav1.ObjectMeta{Name: "pod-0"}},
{ObjectMeta: metav1.ObjectMeta{Name: "pod-1"}},
{ObjectMeta: metav1.ObjectMeta{Name: "pod-2"}},
},
},
want: map[string]int{
"pod-0": 0,
"pod-1": 1,
"pod-2": 2,
},
wantErr: false,
},
{
name: "pool mode - allocation annotation missing",
args: args{
batchSbx: &sandboxv1alpha1.BatchSandbox{
ObjectMeta: metav1.ObjectMeta{
Name: "test-batch",
Namespace: "default",
},
Spec: sandboxv1alpha1.BatchSandboxSpec{
PoolRef: "test-pool",
},
},
pods: []*corev1.Pod{
{ObjectMeta: metav1.ObjectMeta{Name: "pod-0"}},
},
},
want: map[string]int{},
wantErr: false,
},
{
name: "pool mode - invalid allocation json",
args: args{
batchSbx: &sandboxv1alpha1.BatchSandbox{
ObjectMeta: metav1.ObjectMeta{
Name: "test-batch",
Namespace: "default",
Annotations: map[string]string{
AnnoAllocStatusKey: `invalid-json`,
},
},
Spec: sandboxv1alpha1.BatchSandboxSpec{
PoolRef: "test-pool",
},
},
pods: []*corev1.Pod{},
},
want: nil,
wantErr: true,
},
{
name: "pool mode - pods not in allocation list",
args: args{
batchSbx: &sandboxv1alpha1.BatchSandbox{
ObjectMeta: metav1.ObjectMeta{
Name: "test-batch",
Namespace: "default",
Annotations: map[string]string{
AnnoAllocStatusKey: `{"pods":["pod-0","pod-1"]}`,
},
},
Spec: sandboxv1alpha1.BatchSandboxSpec{
PoolRef: "test-pool",
},
},
pods: []*corev1.Pod{
{ObjectMeta: metav1.ObjectMeta{Name: "pod-0"}},
{ObjectMeta: metav1.ObjectMeta{Name: "pod-1"}},
{ObjectMeta: metav1.ObjectMeta{Name: "pod-2"}},
},
},
want: map[string]int{
"pod-0": 0,
"pod-1": 1,
},
wantErr: false,
},
{
name: "non-pool mode - parse from pod labels",
args: args{
batchSbx: &sandboxv1alpha1.BatchSandbox{
ObjectMeta: metav1.ObjectMeta{
Name: "test-batch",
Namespace: "default",
},
Spec: sandboxv1alpha1.BatchSandboxSpec{
Replicas: ptr.To(int32(3)),
},
},
pods: []*corev1.Pod{
{ObjectMeta: metav1.ObjectMeta{
Name: "test-batch-0",
Labels: map[string]string{LabelBatchSandboxPodIndexKey: "0"},
}},
{ObjectMeta: metav1.ObjectMeta{
Name: "test-batch-1",
Labels: map[string]string{LabelBatchSandboxPodIndexKey: "1"},
}},
{ObjectMeta: metav1.ObjectMeta{
Name: "test-batch-2",
Labels: map[string]string{LabelBatchSandboxPodIndexKey: "2"},
}},
},
},
want: map[string]int{
"test-batch-0": 0,
"test-batch-1": 1,
"test-batch-2": 2,
},
wantErr: false,
},
{
name: "non-pool mode - parse from pod names",
args: args{
batchSbx: &sandboxv1alpha1.BatchSandbox{
ObjectMeta: metav1.ObjectMeta{
Name: "test-batch",
Namespace: "default",
},
Spec: sandboxv1alpha1.BatchSandboxSpec{
Replicas: ptr.To(int32(3)),
},
},
pods: []*corev1.Pod{
{ObjectMeta: metav1.ObjectMeta{Name: "test-batch-0"}},
{ObjectMeta: metav1.ObjectMeta{Name: "test-batch-1"}},
{ObjectMeta: metav1.ObjectMeta{Name: "test-batch-2"}},
},
},
want: map[string]int{
"test-batch-0": 0,
"test-batch-1": 1,
"test-batch-2": 2,
},
wantErr: false,
},
{
name: "non-pool mode - invalid pod name",
args: args{
batchSbx: &sandboxv1alpha1.BatchSandbox{
ObjectMeta: metav1.ObjectMeta{
Name: "test-batch",
Namespace: "default",
},
Spec: sandboxv1alpha1.BatchSandboxSpec{
Replicas: ptr.To(int32(1)),
},
},
pods: []*corev1.Pod{
{ObjectMeta: metav1.ObjectMeta{Name: "invalid-name-no-index"}},
},
},
want: nil,
wantErr: true,
},
{
name: "non-pool mode - empty pods list",
args: args{
batchSbx: &sandboxv1alpha1.BatchSandbox{
ObjectMeta: metav1.ObjectMeta{
Name: "test-batch",
Namespace: "default",
},
Spec: sandboxv1alpha1.BatchSandboxSpec{
Replicas: ptr.To(int32(0)),
},
},
pods: []*corev1.Pod{},
},
want: map[string]int{},
wantErr: false,
},
{
name: "non-pool mode - mixed label and name parsing",
args: args{
batchSbx: &sandboxv1alpha1.BatchSandbox{
ObjectMeta: metav1.ObjectMeta{
Name: "test-batch",
Namespace: "default",
},
Spec: sandboxv1alpha1.BatchSandboxSpec{
Replicas: ptr.To(int32(3)),
},
},
pods: []*corev1.Pod{
{ObjectMeta: metav1.ObjectMeta{
Name: "test-batch-0",
Labels: map[string]string{LabelBatchSandboxPodIndexKey: "5"},
}},
{ObjectMeta: metav1.ObjectMeta{Name: "test-batch-1"}},
},
},
want: map[string]int{
"test-batch-0": 5,
"test-batch-1": 1,
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
poolStrategy := strategy.NewPoolStrategy(tt.args.batchSbx)
got, err := calPodIndex(poolStrategy, tt.args.batchSbx, tt.args.pods)
if (err != nil) != tt.wantErr {
t.Errorf("calPodIndex() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("calPodIndex() = %v, want %v", got, tt.want)
}
})
}
}