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

639 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"
gerrors "errors"
"fmt"
"slices"
"strconv"
"strings"
"sync"
"time"
corev1 "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"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/strategicpatch"
"k8s.io/client-go/tools/record"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
sandboxv1alpha1 "github.com/alibaba/OpenSandbox/sandbox-k8s/apis/sandbox/v1alpha1"
poolassign "github.com/alibaba/OpenSandbox/sandbox-k8s/internal/controller/poolassign"
"github.com/alibaba/OpenSandbox/sandbox-k8s/internal/controller/strategy"
taskscheduler "github.com/alibaba/OpenSandbox/sandbox-k8s/internal/scheduler"
"github.com/alibaba/OpenSandbox/sandbox-k8s/internal/utils"
controllerutils "github.com/alibaba/OpenSandbox/sandbox-k8s/internal/utils/controller"
"github.com/alibaba/OpenSandbox/sandbox-k8s/internal/utils/expectations"
"github.com/alibaba/OpenSandbox/sandbox-k8s/internal/utils/fieldindex"
"github.com/alibaba/OpenSandbox/sandbox-k8s/internal/utils/requeueduration"
)
var (
BatchSandboxScaleExpectations = expectations.NewScaleExpectations()
DurationStore = requeueduration.DurationStore{}
)
const batchSandboxFirstPodIndex = 0
type taskScheduleResult struct {
Running, Failed, Succeed, Unknown, Pending int32
}
// BatchSandboxReconciler reconciles a BatchSandbox object
type BatchSandboxReconciler struct {
client.Client
Scheme *runtime.Scheme
Recorder record.EventRecorder
ProfileStore *poolassign.ProfileStore
taskSchedulers sync.Map
StatusRVExpectation expectations.ResourceVersionExpectation
// ResumePullSecret is the K8s Secret name for pulling snapshot images during resume.
ResumePullSecret string
}
// +kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=core,resources=events,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=core,resources=configmaps,verbs=get;list;watch
// +kubebuilder:rbac:groups=sandbox.opensandbox.io,resources=batchsandboxes,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=sandbox.opensandbox.io,resources=batchsandboxes/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=sandbox.opensandbox.io,resources=batchsandboxes/finalizers,verbs=update
// Reconcile is part of the main kubernetes reconciliation loop which aims to
// move the current state of the cluster closer to the desired state.
// TODO(user): Modify the Reconcile function to compare the state specified by
// the BatchSandbox object against the actual cluster state, and then
// perform operations to make the cluster state reflect the state specified by
// the user.
//
// For more details, check Reconcile and its Result here:
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.21.0/pkg/reconcile
func (r *BatchSandboxReconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ctrl.Result, retErr error) {
log := logf.FromContext(ctx)
start := time.Now()
var aggErrors []error
defer func() {
_ = DurationStore.Pop(req.String())
log.Info("Reconcile finished", "duration", time.Since(start).String(), "requeueAfter", result.RequeueAfter.String(), "error", retErr)
}()
batchSbx := &sandboxv1alpha1.BatchSandbox{}
if err := r.Get(ctx, client.ObjectKey{
Namespace: req.Namespace,
Name: req.Name,
}, batchSbx); err != nil {
if errors.IsNotFound(err) {
return ctrl.Result{}, nil
}
return ctrl.Result{}, err
}
// handle expire
if expireAt := batchSbx.Spec.ExpireTime; expireAt != nil {
now := time.Now()
if expireAt.Time.Before(now) {
if batchSbx.DeletionTimestamp == nil {
log.Info("batch sandbox expired, delete", "expireAt", expireAt)
if err := r.Delete(ctx, batchSbx); err != nil {
if errors.IsNotFound(err) {
return ctrl.Result{}, nil
}
return ctrl.Result{}, err
}
}
} else {
DurationStore.Push(types.NamespacedName{Namespace: batchSbx.Namespace, Name: batchSbx.Name}.String(), expireAt.Time.Sub(now))
}
}
// task schedule
taskStrategy := strategy.NewTaskSchedulingStrategy(batchSbx)
// pool strategy
poolStrategy := strategy.NewPoolStrategy(batchSbx)
if profileName := poolStrategy.AssignProfile(); profileName != "" {
updated, err := r.assignPool(ctx, batchSbx, profileName)
if err != nil {
return ctrl.Result{}, fmt.Errorf("failed to auto-assign pool: %w", err)
}
if updated {
return ctrl.Result{}, nil
}
}
// handle finalizers
if batchSbx.DeletionTimestamp == nil {
if taskStrategy.NeedTaskScheduling() {
if !controllerutil.ContainsFinalizer(batchSbx, FinalizerTaskCleanup) {
err := utils.UpdateFinalizer(r.Client, batchSbx, utils.AddFinalizerOpType, FinalizerTaskCleanup)
if err != nil {
log.Error(err, "failed to add finalizer", "finalizer", FinalizerTaskCleanup)
} else {
log.Info("added finalizer", "finalizer", FinalizerTaskCleanup)
}
return ctrl.Result{}, err
}
}
} else {
if !taskStrategy.NeedTaskScheduling() {
return ctrl.Result{}, nil
}
}
// Pause/Resume dispatch: handles pause/resume intent before normal scaling.
if result, handled, err := r.dispatchPauseResume(ctx, batchSbx); handled {
return result, err
}
// dispatchPauseResume may patch BatchSandbox spec/state (for example resume detaches a pooled
// sandbox from its pool). Recompute strategies from the latest object before listing pods so
// normal reconciliation does not keep using a stale pre-dispatch view.
taskStrategy = strategy.NewTaskSchedulingStrategy(batchSbx)
pods, err := r.listPods(ctx, poolStrategy, batchSbx)
if err != nil {
return ctrl.Result{}, fmt.Errorf("failed to list pods %w", err)
}
podIndex, err := calPodIndex(poolStrategy, batchSbx, pods)
if err != nil {
return ctrl.Result{}, fmt.Errorf("failed to cal pod index %w", err)
}
slices.SortStableFunc(pods, utils.MultiPodSorter([]func(a, b *corev1.Pod) int{
utils.WithPodIndexSorter(podIndex),
utils.PodNameSorter,
}).Sort)
// Normal mode owns pod lifecycle except while a sandbox is fully paused. In Paused, the
// snapshot-backed runtime is quiesced and pods must stay absent until resume rewrites the
// template images and transitions back through Resuming.
if !poolStrategy.IsPooledMode() && batchSbx.Status.Phase != sandboxv1alpha1.BatchSandboxPhasePaused {
err := r.scaleBatchSandbox(ctx, batchSbx, batchSbx.Spec.Template, pods)
if err != nil {
return ctrl.Result{}, fmt.Errorf("failed to scale batch sandbox %w", err)
}
}
runtimeView := buildRuntimeView(batchSbx, pods)
// Ensure PauseObservedGeneration is up-to-date so the status patch ACKs the
// current generation without requiring a dedicated API call.
// Skip during Resuming: a newer generation may carry a queued pause request
// that must remain unacknowledged until resume completes and handlePause runs.
if batchSbx.Status.Phase != sandboxv1alpha1.BatchSandboxPhaseResuming &&
runtimeView.status.PauseObservedGeneration < batchSbx.Generation {
runtimeView.status.PauseObservedGeneration = batchSbx.Generation
}
if batchSbx.Status.Phase == sandboxv1alpha1.BatchSandboxPhasePaused {
r.deleteTaskScheduler(ctx, batchSbx)
}
if taskStrategy.NeedTaskScheduling() && batchSbx.Status.Phase != sandboxv1alpha1.BatchSandboxPhasePaused {
ts, err := r.reconcileTasks(ctx, batchSbx, pods)
if err != nil {
aggErrors = append(aggErrors, err)
} else if ts != nil {
runtimeView.status.TaskRunning = ts.Running
runtimeView.status.TaskFailed = ts.Failed
runtimeView.status.TaskSucceed = ts.Succeed
runtimeView.status.TaskUnknown = ts.Unknown
runtimeView.status.TaskPending = ts.Pending
}
}
requeue, persistErrors := r.persistRuntimeView(ctx, batchSbx, runtimeView)
aggErrors = append(aggErrors, persistErrors...)
requeueAfter := DurationStore.Pop(req.String())
if requeue > 0 && (requeueAfter == 0 || requeue < requeueAfter) {
requeueAfter = requeue
}
return reconcile.Result{RequeueAfter: requeueAfter}, gerrors.Join(aggErrors...)
}
func calPodIndex(poolStrategy strategy.PoolStrategy, batchSbx *sandboxv1alpha1.BatchSandbox, pods []*corev1.Pod) (map[string]int, error) {
podIndex := map[string]int{}
if poolStrategy.IsPooledMode() {
// cal index from pool alloc result while using pooling
alloc, err := parseSandboxAllocation(batchSbx)
if err != nil {
return nil, err
}
for i := range alloc.Pods {
podIndex[alloc.Pods[i]] = i
}
} else {
for i := range pods {
po := pods[i]
idx, err := parseIndex(po)
if err != nil {
return nil, fmt.Errorf("batchsandbox: failed to parse %s/%s index %w", po.Namespace, po.Name, err)
}
podIndex[po.Name] = idx
}
}
return podIndex, nil
}
func (r *BatchSandboxReconciler) reconcileTasks(
ctx context.Context,
batchSbx *sandboxv1alpha1.BatchSandbox,
pods []*corev1.Pod,
) (*taskScheduleResult, error) {
log := logf.FromContext(ctx)
sch, err := r.getTaskScheduler(ctx, batchSbx, pods)
if err != nil {
return nil, err
}
// Because tasks are in-memory and there is no event mechanism, periodic reconciliation is required.
DurationStore.Push(types.NamespacedName{Namespace: batchSbx.Namespace, Name: batchSbx.Name}.String(), 3*time.Second)
if batchSbx.DeletionTimestamp != nil {
stoppingTasks := sch.StopTask()
if len(stoppingTasks) > 0 {
log.Info("stopping tasks", "count", len(stoppingTasks))
}
}
now := time.Now()
ts, err := r.scheduleTasks(ctx, sch, batchSbx)
if err != nil {
return nil, fmt.Errorf("failed to schedule tasks, err %w", err)
}
log.Info("schedule tasks completed", "costMs", time.Since(now).Milliseconds(), "task schedule result", utils.DumpJSON(ts))
// check task cleanup is finished
if batchSbx.DeletionTimestamp != nil {
unfinishedTasks := r.getTasksCleanupUnfinished(batchSbx, sch)
if len(unfinishedTasks) > 0 {
log.Info("tasks cleanup is unfinished", "unfinishedCount", len(unfinishedTasks))
} else {
var cleanupErr error
if controllerutil.ContainsFinalizer(batchSbx, FinalizerTaskCleanup) {
cleanupErr = utils.UpdateFinalizer(r.Client, batchSbx, utils.RemoveFinalizerOpType, FinalizerTaskCleanup)
if cleanupErr != nil {
if errors.IsNotFound(cleanupErr) {
cleanupErr = nil
} else {
log.Error(cleanupErr, "failed to remove finalizer", "finalizer", FinalizerTaskCleanup)
}
}
}
if cleanupErr == nil {
r.deleteTaskScheduler(ctx, batchSbx)
log.Info("task cleanup is finished, removed finalizer", "finalizer", FinalizerTaskCleanup)
}
// all tasks are cleaned up; skip returning task schedule result so the caller doesn't overwrite status
return nil, cleanupErr
}
}
return ts, nil
}
func (r *BatchSandboxReconciler) listPods(ctx context.Context, poolStrategy strategy.PoolStrategy, batchSbx *sandboxv1alpha1.BatchSandbox) ([]*corev1.Pod, error) {
var ret []*corev1.Pod
if poolStrategy.IsPooledMode() {
var (
allocSet = make(sets.Set[string])
releasedSet = make(sets.Set[string])
)
alloc, err := parseSandboxAllocation(batchSbx)
if err != nil {
return nil, err
}
allocSet.Insert(alloc.Pods...)
released, err := parseSandboxReleased(batchSbx)
if err != nil {
return nil, err
}
releasedSet.Insert(released.Pods...)
activePods := allocSet.Difference(releasedSet)
for name := range activePods {
pod := &corev1.Pod{}
// TODO maybe performance is problem
if err := r.Client.Get(ctx, types.NamespacedName{Namespace: batchSbx.Namespace, Name: name}, pod); err != nil {
if errors.IsNotFound(err) {
continue
}
return nil, err
}
ret = append(ret, pod)
}
} else {
podList := &corev1.PodList{}
if err := r.Client.List(ctx, podList, &client.ListOptions{
Namespace: batchSbx.Namespace,
FieldSelector: fields.SelectorFromSet(fields.Set{fieldindex.IndexNameForOwnerRefUID: string(batchSbx.UID)}),
}); err != nil {
return nil, err
}
for i := range podList.Items {
ret = append(ret, &podList.Items[i])
}
}
return ret, nil
}
func (r *BatchSandboxReconciler) getTaskScheduler(ctx context.Context, batchSbx *sandboxv1alpha1.BatchSandbox, pods []*corev1.Pod) (taskscheduler.TaskScheduler, error) {
log := logf.FromContext(ctx)
var tSch taskscheduler.TaskScheduler
key := types.NamespacedName{Namespace: batchSbx.Namespace, Name: batchSbx.Name}.String()
val, ok := r.taskSchedulers.Load(key)
// The reconciler guarantees that it will not concurrently reconcile the same BatchSandbox.
if !ok {
policy := sandboxv1alpha1.TaskResourcePolicyRetain
if batchSbx.Spec.TaskResourcePolicyWhenCompleted != nil {
policy = *batchSbx.Spec.TaskResourcePolicyWhenCompleted
}
taskStrategy := strategy.NewTaskSchedulingStrategy(batchSbx)
taskSpecs, err := taskStrategy.GenerateTaskSpecs()
if err != nil {
return nil, err
}
sc, err := taskscheduler.NewTaskScheduler(key, taskSpecs, pods, policy, log)
if err != nil {
return nil, fmt.Errorf("new task scheduler err %w", err)
}
log.Info("successfully created task scheduler")
tSch = sc
r.taskSchedulers.Store(key, sc)
} else {
tSch, ok = (val.(taskscheduler.TaskScheduler))
if !ok {
return nil, gerrors.New("invalid scheduler type stored")
}
// Update the pods list for this scheduler
tSch.UpdatePods(pods)
// Handle scale-out: register task specs for any replicas added since the
// scheduler was first created. Already-tracked task names are skipped.
taskStrategy := strategy.NewTaskSchedulingStrategy(batchSbx)
taskSpecs, err := taskStrategy.GenerateTaskSpecs()
if err != nil {
return nil, fmt.Errorf("failed to generate task specs for scale-out: %w", err)
}
if err := tSch.AddTasks(taskSpecs); err != nil {
return nil, fmt.Errorf("failed to add tasks on scale-out: %w", err)
}
}
return tSch, nil
}
func (r *BatchSandboxReconciler) deleteTaskScheduler(ctx context.Context, batchSbx *sandboxv1alpha1.BatchSandbox) {
key := types.NamespacedName{Namespace: batchSbx.Namespace, Name: batchSbx.Name}.String()
if _, ok := r.taskSchedulers.LoadAndDelete(key); ok {
log := logf.FromContext(ctx)
log.Info("delete task scheduler")
}
}
func (r *BatchSandboxReconciler) scheduleTasks(ctx context.Context, tSch taskscheduler.TaskScheduler, batchSbx *sandboxv1alpha1.BatchSandbox) (*taskScheduleResult, error) {
log := logf.FromContext(ctx)
if err := tSch.Schedule(); err != nil {
return nil, err
}
tasks := tSch.ListTask()
toReleasedPods := []string{}
var (
running, failed, succeed, unknown int32
pending int32
)
for i := range len(tasks) {
task := tasks[i]
if task.GetPodName() == "" {
pending++
} else {
state := task.GetState()
if task.IsResourceReleased() {
toReleasedPods = append(toReleasedPods, task.GetPodName())
}
switch state {
case taskscheduler.RunningTaskState:
running++
case taskscheduler.SucceedTaskState:
succeed++
case taskscheduler.FailedTaskState:
failed++
case taskscheduler.UnknownTaskState:
unknown++
}
}
}
if len(toReleasedPods) > 0 {
log.Info("try to release Pods", "count", len(toReleasedPods))
if err := r.releasePods(ctx, batchSbx, toReleasedPods); err != nil {
return nil, err
}
log.Info("successfully released Pods", "count", len(toReleasedPods))
}
return &taskScheduleResult{
Running: running,
Failed: failed,
Succeed: succeed,
Unknown: unknown,
Pending: pending,
}, nil
}
func (r *BatchSandboxReconciler) getTasksCleanupUnfinished(batchSbx *sandboxv1alpha1.BatchSandbox, tSch taskscheduler.TaskScheduler) []taskscheduler.Task {
var notReleased []taskscheduler.Task
for _, task := range tSch.ListTask() {
if !task.IsResourceReleased() {
notReleased = append(notReleased, task)
}
}
return notReleased
}
func (r *BatchSandboxReconciler) releasePods(ctx context.Context, batchSbx *sandboxv1alpha1.BatchSandbox, toReleasePods []string) error {
releasedSet := make(sets.Set[string])
released, err := parseSandboxReleased(batchSbx)
if err != nil {
return err
}
releasedSet.Insert(released.Pods...)
releasedSet.Insert(toReleasePods...)
newRelease := AllocationRelease{
Pods: sets.List(releasedSet),
}
raw, err := json.Marshal(newRelease)
if err != nil {
return fmt.Errorf("Failed to marshal released pod names: %v", err)
}
body := utils.DumpJSON(struct {
MetaData metav1.ObjectMeta `json:"metadata"`
}{
MetaData: metav1.ObjectMeta{
Annotations: map[string]string{
AnnoAllocReleaseKey: string(raw),
},
},
})
b := &sandboxv1alpha1.BatchSandbox{
ObjectMeta: metav1.ObjectMeta{
Namespace: batchSbx.Namespace,
Name: batchSbx.Name,
},
}
if err := r.Client.Patch(ctx, b, client.RawPatch(types.MergePatchType, []byte(body))); err != nil {
r.Recorder.Eventf(batchSbx, corev1.EventTypeWarning, EventReasonFailedRelease, "Failed to release pods: %v", err)
return err
}
if len(toReleasePods) > 0 {
r.Recorder.Eventf(batchSbx, corev1.EventTypeNormal, EventReasonPodReleased, "Released %d pod(s) back to pool: %v", len(toReleasePods), toReleasePods)
}
return nil
}
// Normal Mode
func (r *BatchSandboxReconciler) scaleBatchSandbox(ctx context.Context, batchSandbox *sandboxv1alpha1.BatchSandbox, podTemplateSpec *corev1.PodTemplateSpec, pods []*corev1.Pod) error {
log := logf.FromContext(ctx)
indexedPodMap := map[int]*corev1.Pod{}
for i := range pods {
pod := pods[i]
BatchSandboxScaleExpectations.ObserveScale(controllerutils.GetControllerKey(batchSandbox), expectations.Create, pod.Name)
idx, err := parseIndex(pod)
if err != nil {
return fmt.Errorf("failed to parse idx Pod %s, err %w", pod.Name, err)
}
indexedPodMap[idx] = pod
}
if satisfied, unsatisfiedDuration, dirtyPods := BatchSandboxScaleExpectations.SatisfiedExpectations(controllerutils.GetControllerKey(batchSandbox)); !satisfied {
log.Info("scale expectation is not satisfied", "unsatisfiedDuration", unsatisfiedDuration, "dirtyPods", dirtyPods)
DurationStore.Push(types.NamespacedName{Namespace: batchSandbox.Namespace, Name: batchSandbox.Name}.String(), expectations.ExpectationTimeout-unsatisfiedDuration)
return nil
}
// TODO consider supply Pods if Pods is deleted unexpectedly
var needCreateIndex []int
// TODO var needDeleteIndex []int
for i := 0; i < int(*batchSandbox.Spec.Replicas); i++ {
_, ok := indexedPodMap[i]
if !ok {
needCreateIndex = append(needCreateIndex, i)
}
}
// scale
if len(needCreateIndex) > 0 {
log.Info("try to create Pods", "count", len(needCreateIndex), "indexes", needCreateIndex)
}
for _, idx := range needCreateIndex {
pod, err := utils.GetPodFromTemplate(podTemplateSpec, batchSandbox, metav1.NewControllerRef(batchSandbox, sandboxv1alpha1.SchemeBuilder.GroupVersion.WithKind("BatchSandbox")))
if err != nil {
return err
}
// Apply shard patch if available for this index
if len(batchSandbox.Spec.ShardPatches) > 0 && idx < len(batchSandbox.Spec.ShardPatches) {
podBytes, err := json.Marshal(pod)
if err != nil {
return fmt.Errorf("failed to marshal pod: %w", err)
}
patch := batchSandbox.Spec.ShardPatches[idx]
modifiedPodBytes, err := strategicpatch.StrategicMergePatch(podBytes, patch.Raw, &corev1.Pod{})
if err != nil {
return fmt.Errorf("failed to apply shard patch for index %d: %w", idx, err)
}
if err := json.Unmarshal(modifiedPodBytes, pod); err != nil {
return fmt.Errorf("failed to unmarshal patched pod for index %d: %w", idx, err)
}
}
if err := ctrl.SetControllerReference(pod, batchSandbox, r.Scheme); err != nil {
return err
}
pod.Labels[LabelBatchSandboxPodIndexKey] = strconv.Itoa(idx)
pod.Labels[LabelBatchSandboxNameKey] = batchSandbox.Name
pod.Namespace = batchSandbox.Namespace
pod.Name = fmt.Sprintf("%s-%d", batchSandbox.Name, idx)
BatchSandboxScaleExpectations.ExpectScale(controllerutils.GetControllerKey(batchSandbox), expectations.Create, pod.Name)
if err := r.Create(ctx, pod); err != nil {
BatchSandboxScaleExpectations.ObserveScale(controllerutils.GetControllerKey(batchSandbox), expectations.Create, pod.Name)
r.Recorder.Eventf(batchSandbox, corev1.EventTypeWarning, EventReasonFailedCreate, "failed to create pod: %v, pod: %v", err, utils.DumpJSON(pod))
return err
}
r.Recorder.Eventf(batchSandbox, corev1.EventTypeNormal, EventReasonSuccessfulCreate, "succeed to create pod %s", pod.Name)
}
return nil
}
func parseIndex(pod *corev1.Pod) (int, error) {
if v := pod.Labels[LabelBatchSandboxPodIndexKey]; v != "" {
return strconv.Atoi(v)
}
idx := strings.LastIndex(pod.Name, "-")
if idx == -1 {
return -1, gerrors.New("batchsandbox: Invalid pod Name")
}
return strconv.Atoi(pod.Name[idx+1:])
}
// assignPool selects a Pool for the BatchSandbox using the assign package and writes
// the result back to spec.poolRef. Returns (true, nil) when the update was applied,
// which triggers a new reconcile with the concrete poolRef.
func (r *BatchSandboxReconciler) assignPool(ctx context.Context, batchSbx *sandboxv1alpha1.BatchSandbox, profileName string) (bool, error) {
log := logf.FromContext(ctx)
poolList := &sandboxv1alpha1.PoolList{}
if err := r.List(ctx, poolList, client.InNamespace(batchSbx.Namespace)); err != nil {
return false, fmt.Errorf("failed to list pools: %w", err)
}
pools := make([]*sandboxv1alpha1.Pool, 0, len(poolList.Items))
for i := range poolList.Items {
pools = append(pools, &poolList.Items[i])
}
profile := r.ProfileStore.GetProfile(profileName)
assigner := poolassign.NewDefaultAssigner(profile)
poolName, err := assigner.AssignPool(ctx, batchSbx, pools)
if err != nil {
r.Recorder.Eventf(batchSbx, corev1.EventTypeWarning, EventReasonFailedPoolAssign, "Failed to assign pool: %v", err)
return false, err
}
oldSbx := batchSbx.DeepCopy()
batchSbx.Spec.PoolRef = poolName
patch := client.MergeFrom(oldSbx)
if err := r.Patch(ctx, batchSbx, patch); err != nil {
return false, fmt.Errorf("failed to patch poolRef: %w", err)
}
log.Info("auto-assigned pool", "pool", poolName)
r.Recorder.Eventf(batchSbx, corev1.EventTypeNormal, EventReasonPoolAssigned, "Assigned to pool %s", poolName)
return true, nil
}
// SetupWithManager sets up the controller with the Manager.
func (r *BatchSandboxReconciler) SetupWithManager(mgr ctrl.Manager, maxConcurrentReconciles int) error {
return ctrl.NewControllerManagedBy(mgr).
For(&sandboxv1alpha1.BatchSandbox{}).
Named("batchsandbox").
Owns(&corev1.Pod{}).
Owns(&sandboxv1alpha1.SandboxSnapshot{}).
WithOptions(controller.Options{MaxConcurrentReconciles: maxConcurrentReconciles}).
Complete(r)
}