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

689 lines
24 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"
"fmt"
"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/types"
"k8s.io/client-go/util/retry"
"k8s.io/utils/ptr"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
logf "sigs.k8s.io/controller-runtime/pkg/log"
sandboxv1alpha1 "github.com/alibaba/OpenSandbox/sandbox-k8s/apis/sandbox/v1alpha1"
"github.com/alibaba/OpenSandbox/sandbox-k8s/internal/controller/strategy"
"github.com/alibaba/OpenSandbox/sandbox-k8s/internal/utils"
controllerutils "github.com/alibaba/OpenSandbox/sandbox-k8s/internal/utils/controller"
)
const internalPauseSnapshotSuffix = "-pause"
const supportedPauseReplicas int32 = 1
func internalPauseSnapshotName(batchSandboxName string) string {
// TODO: handle Kubernetes resource name length limits for long BatchSandbox names.
return batchSandboxName + internalPauseSnapshotSuffix
}
func unsupportedPauseReplicasMessage(replicas *int32) string {
if replicas == nil {
return "pause/resume currently supports only BatchSandbox spec.replicas=1; spec.replicas is unset"
}
return fmt.Sprintf("pause/resume currently supports only BatchSandbox spec.replicas=1; got spec.replicas=%d", *replicas)
}
func ensureImagePullSecret(template *corev1.PodTemplateSpec, secretName string) {
if template == nil || secretName == "" {
return
}
for _, secret := range template.Spec.ImagePullSecrets {
if secret.Name == secretName {
return
}
}
template.Spec.ImagePullSecrets = append(template.Spec.ImagePullSecrets, corev1.LocalObjectReference{Name: secretName})
}
func snapshotFailureMessage(snapshot *sandboxv1alpha1.SandboxSnapshot) string {
for _, cond := range snapshot.Status.Conditions {
if cond.Type == sandboxv1alpha1.SandboxSnapshotConditionFailed && cond.Status == sandboxv1alpha1.ConditionTrue && cond.Message != "" {
return cond.Message
}
}
return "snapshot failed"
}
func copyPodTemplateMap(values map[string]string) map[string]string {
if len(values) == 0 {
return nil
}
copied := make(map[string]string, len(values))
for key, value := range values {
copied[key] = value
}
if len(copied) == 0 {
return nil
}
return copied
}
func sourcePodTemplateForPause(pod *corev1.Pod) *corev1.PodTemplateSpec {
if pod == nil {
return nil
}
labels := copyPodTemplateMap(pod.Labels)
delete(labels, LabelPoolName)
delete(labels, LabelPoolRevision)
delete(labels, LabelBatchSandboxNameKey)
delete(labels, LabelBatchSandboxPodIndexKey)
spec := *pod.Spec.DeepCopy()
spec.NodeName = ""
return &corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: labels,
Annotations: copyPodTemplateMap(pod.Annotations),
},
Spec: spec,
}
}
func (r *BatchSandboxReconciler) deleteInternalPauseSnapshot(ctx context.Context, bs *sandboxv1alpha1.BatchSandbox) error {
log := logf.FromContext(ctx)
snapshot := &sandboxv1alpha1.SandboxSnapshot{}
if err := r.Get(ctx, types.NamespacedName{Namespace: bs.Namespace, Name: internalPauseSnapshotName(bs.Name)}, snapshot); err != nil {
if errors.IsNotFound(err) {
return nil
}
return err
}
if snapshot.Status.Phase != sandboxv1alpha1.SandboxSnapshotPhaseSucceed {
return nil
}
if err := r.Delete(ctx, snapshot); err != nil && !errors.IsNotFound(err) {
return err
}
log.Info("Deleted SandboxSnapshot after successful resume", "snapshot", snapshot.Name)
return nil
}
func (r *BatchSandboxReconciler) hasReadyResumePod(ctx context.Context, bs *sandboxv1alpha1.BatchSandbox) (bool, error) {
poolStrategy := strategy.NewPoolStrategy(bs)
if poolStrategy.IsPooledMode() {
alloc, err := parseSandboxAllocation(bs)
if err != nil {
return false, err
}
for _, podName := range alloc.Pods {
pod := &corev1.Pod{}
if err := r.Get(ctx, types.NamespacedName{Namespace: bs.Namespace, Name: podName}, pod); err != nil {
if errors.IsNotFound(err) {
continue
}
return false, err
}
if utils.IsPodReady(pod) {
return true, nil
}
}
}
podList := &corev1.PodList{}
if err := r.List(ctx, podList,
client.InNamespace(bs.Namespace),
client.MatchingLabels{LabelBatchSandboxNameKey: bs.Name},
); err != nil {
return false, err
}
for i := range podList.Items {
if utils.IsPodReady(&podList.Items[i]) {
return true, nil
}
}
pod := &corev1.Pod{}
if err := r.Get(ctx, types.NamespacedName{Namespace: bs.Namespace, Name: fmt.Sprintf("%s-%d", bs.Name, batchSandboxFirstPodIndex)}, pod); err != nil {
if errors.IsNotFound(err) {
return false, nil
}
return false, err
}
return utils.IsPodReady(pod), nil
}
// dispatchPauseResume implements the 5-case dispatch table from the design doc.
// Returns (result, handled, error). If handled=true, the caller should return immediately.
func (r *BatchSandboxReconciler) dispatchPauseResume(ctx context.Context, bs *sandboxv1alpha1.BatchSandbox) (ctrl.Result, bool, error) {
log := logf.FromContext(ctx)
generation := bs.Generation
pauseObservedGen := bs.Status.PauseObservedGeneration
pause := bs.Spec.Pause
// In-progress phases take priority over generation checks so that controller-owned
// spec mutations (template solidification, image replacement, pool detachment) do not
// get mistaken for new external pause/resume requests.
log.Info("Dispatch: checking phase", "currentPhase", bs.Status.Phase, "generation", generation, "pauseObservedGen", pauseObservedGen, "pause", pause)
if bs.Status.Phase == sandboxv1alpha1.BatchSandboxPhaseResuming {
log.Info("Dispatch: phase is Resuming, continuing resume")
result, err := r.continueResume(ctx, bs)
if err != nil {
return result, true, err
}
// Return handled=false to let normal flow update phase from Resuming to Succeed.
return result, false, nil
}
if bs.Status.Phase == sandboxv1alpha1.BatchSandboxPhasePausing {
log.Info("Dispatch: phase is Pausing, syncing pause state")
result, err := r.syncPauseOrClear(ctx, bs)
return result, true, err
}
if generation > pauseObservedGen {
if pause != nil {
if *pause {
log.Info("Dispatch: handlePause", "generation", generation, "pauseObservedGeneration", pauseObservedGen)
result, err := r.handlePause(ctx, bs)
return result, true, err
}
log.Info("Dispatch: handleResume", "generation", generation, "pauseObservedGeneration", pauseObservedGen, "currentPhase", bs.Status.Phase)
result, err := r.handleResume(ctx, bs)
return result, true, err
}
// No pause intent — skip the dedicated ACK API call. The normal flow's
// persistRuntimeView will update PauseObservedGeneration in its status patch.
log.Info("Dispatch: no pause intent, deferring ACK to status patch", "generation", generation, "pauseObservedGeneration", pauseObservedGen)
return ctrl.Result{}, false, nil
}
return ctrl.Result{}, false, nil
}
// handlePause implements the pause flow:
// 1. ACK (pauseObservedGeneration + phase=Pausing)
// 2. Stop task-executor tasks, if any, while keeping the source Pod allocated
// 3. Create SandboxSnapshot child resource
func (r *BatchSandboxReconciler) handlePause(ctx context.Context, bs *sandboxv1alpha1.BatchSandbox) (ctrl.Result, error) {
log := logf.FromContext(ctx)
// The pause snapshot format records one source pod's container images, so this
// controller only supports rootfs pause/resume for single-replica sandboxes.
if bs.Spec.Replicas == nil || *bs.Spec.Replicas != supportedPauseReplicas {
msg := unsupportedPauseReplicasMessage(bs.Spec.Replicas)
log.Info("Rejecting pause for unsupported replica count", "message", msg)
phase := bs.Status.Phase
if phase == "" {
phase = sandboxv1alpha1.BatchSandboxPhaseSucceed
}
if err := r.ackPauseWithPhase(ctx, bs, phase, ""); err != nil {
return ctrl.Result{}, err
}
if err := r.setCondition(ctx, bs, sandboxv1alpha1.BatchSandboxConditionPauseFailed, sandboxv1alpha1.ConditionTrue, "UnsupportedReplicas", msg); err != nil {
return ctrl.Result{}, err
}
return ctrl.Result{}, nil
}
snapshot := &sandboxv1alpha1.SandboxSnapshot{}
snapshotName := internalPauseSnapshotName(bs.Name)
err := r.Get(ctx, types.NamespacedName{Namespace: bs.Namespace, Name: snapshotName}, snapshot)
if err == nil && snapshot.DeletionTimestamp != nil {
log.Info("Waiting for stale SandboxSnapshot deletion before retrying pause", "snapshot", snapshotName)
return ctrl.Result{RequeueAfter: time.Second}, nil
}
if err == nil && (snapshot.Status.Phase == sandboxv1alpha1.SandboxSnapshotPhaseFailed || snapshot.Status.Phase == sandboxv1alpha1.SandboxSnapshotPhaseSucceed) {
log.Info("Deleting terminal SandboxSnapshot for retry", "snapshot", snapshotName, "phase", snapshot.Status.Phase)
if err := r.Delete(ctx, snapshot); err != nil && !errors.IsNotFound(err) {
return ctrl.Result{}, err
}
return ctrl.Result{RequeueAfter: time.Second}, nil
}
if err != nil && !errors.IsNotFound(err) {
return ctrl.Result{}, err
}
_ = r.setCondition(ctx, bs, sandboxv1alpha1.BatchSandboxConditionPauseFailed, sandboxv1alpha1.ConditionFalse, "", "")
if err := r.ackPauseWithPhase(ctx, bs, sandboxv1alpha1.BatchSandboxPhasePausing, ""); err != nil {
return ctrl.Result{}, err
}
if errors.IsNotFound(err) {
if created, err := r.ensureInternalPauseSnapshot(ctx, bs, snapshotName); err != nil {
return ctrl.Result{}, err
} else if !created {
log.Info("Waiting for task cleanup before creating SandboxSnapshot", "snapshot", snapshotName)
}
}
return ctrl.Result{RequeueAfter: time.Second}, nil
}
func (r *BatchSandboxReconciler) ensureInternalPauseSnapshot(ctx context.Context, bs *sandboxv1alpha1.BatchSandbox, snapshotName string) (bool, error) {
log := logf.FromContext(ctx)
tasksStopped, err := r.stopTasksBeforePause(ctx, bs)
if err != nil {
return false, err
}
if !tasksStopped {
return false, nil
}
snapshot := &sandboxv1alpha1.SandboxSnapshot{
ObjectMeta: metav1.ObjectMeta{
Name: snapshotName,
Namespace: bs.Namespace,
},
Spec: sandboxv1alpha1.SandboxSnapshotSpec{
SandboxName: bs.Name,
},
}
if err := controllerutil.SetControllerReference(bs, snapshot, r.Scheme); err != nil {
return false, err
}
if err := r.Create(ctx, snapshot); err != nil {
if errors.IsAlreadyExists(err) {
return true, nil
}
return false, err
}
log.Info("Created SandboxSnapshot", "snapshot", snapshotName)
return true, nil
}
func (r *BatchSandboxReconciler) stopTasksBeforePause(ctx context.Context, bs *sandboxv1alpha1.BatchSandbox) (bool, error) {
log := logf.FromContext(ctx)
taskStrategy := strategy.NewTaskSchedulingStrategy(bs)
if !taskStrategy.NeedTaskScheduling() {
return true, nil
}
poolStrategy := strategy.NewPoolStrategy(bs)
pods, err := r.listPods(ctx, poolStrategy, bs)
if err != nil {
return false, err
}
sch, err := r.getTaskScheduler(ctx, bs, pods)
if err != nil {
return false, err
}
stoppingTasks := sch.StopTask()
if len(stoppingTasks) > 0 {
log.Info("Stopping tasks before pause", "count", len(stoppingTasks))
}
if err := sch.Schedule(); err != nil {
return false, fmt.Errorf("failed to stop tasks before pause: %w", err)
}
unfinishedTasks := r.getTasksCleanupUnfinished(bs, sch)
if len(unfinishedTasks) > 0 {
log.Info("Task cleanup before pause is unfinished", "unfinishedCount", len(unfinishedTasks))
return false, nil
}
log.Info("Task cleanup before pause is finished")
return true, nil
}
// handleResume implements the resume flow:
// 1. ACK (pauseObservedGeneration + phase=Resuming)
// 2. Requeue so a subsequent pass can consume the snapshot result
func (r *BatchSandboxReconciler) handleResume(ctx context.Context, bs *sandboxv1alpha1.BatchSandbox) (ctrl.Result, error) {
log := logf.FromContext(ctx)
_ = r.setCondition(ctx, bs, sandboxv1alpha1.BatchSandboxConditionResumeFailed, sandboxv1alpha1.ConditionFalse, "", "")
log.Info("ACK Resuming phase")
if err := r.ackPauseWithPhase(ctx, bs, sandboxv1alpha1.BatchSandboxPhaseResuming, ""); err != nil {
return ctrl.Result{}, err
}
return ctrl.Result{RequeueAfter: time.Second}, nil
}
// syncPauseOrClear waits for the internal pause snapshot to finish and transitions the
// BatchSandbox into Paused or a retryable/terminal failure state.
func (r *BatchSandboxReconciler) syncPauseOrClear(ctx context.Context, bs *sandboxv1alpha1.BatchSandbox) (ctrl.Result, error) {
log := logf.FromContext(ctx)
snapshot := &sandboxv1alpha1.SandboxSnapshot{}
err := r.Get(ctx, types.NamespacedName{Namespace: bs.Namespace, Name: internalPauseSnapshotName(bs.Name)}, snapshot)
if errors.IsNotFound(err) {
snapshotName := internalPauseSnapshotName(bs.Name)
if created, createErr := r.ensureInternalPauseSnapshot(ctx, bs, snapshotName); createErr != nil {
return ctrl.Result{}, createErr
} else if !created {
log.Info("SandboxSnapshot not created yet; waiting for task cleanup", "snapshot", snapshotName)
}
return ctrl.Result{RequeueAfter: time.Second}, nil
}
if err != nil {
return ctrl.Result{}, err
}
switch snapshot.Status.Phase {
case sandboxv1alpha1.SandboxSnapshotPhaseSucceed:
log.Info("SandboxSnapshot Succeed, completing pause")
if err := r.completePause(ctx, bs); err != nil {
return ctrl.Result{}, err
}
return ctrl.Result{RequeueAfter: time.Second}, nil
case sandboxv1alpha1.SandboxSnapshotPhaseFailed:
msg := snapshotFailureMessage(snapshot)
log.Info("SandboxSnapshot Failed", "message", msg)
phase := sandboxv1alpha1.BatchSandboxPhaseSucceed
reason := "SnapshotFailed"
if _, podErr := r.findPodForSandbox(ctx, bs); podErr != nil {
phase = sandboxv1alpha1.BatchSandboxPhaseFailed
reason = "PodNotFound"
}
if err := r.ackPauseWithPhase(ctx, bs, phase, ""); err != nil {
return ctrl.Result{}, err
}
if err := r.setCondition(ctx, bs, sandboxv1alpha1.BatchSandboxConditionPauseFailed, sandboxv1alpha1.ConditionTrue, reason, msg); err != nil {
return ctrl.Result{}, err
}
return ctrl.Result{}, nil
case sandboxv1alpha1.SandboxSnapshotPhasePending, sandboxv1alpha1.SandboxSnapshotPhaseCommitting:
log.Info("SandboxSnapshot in progress", "phase", snapshot.Status.Phase)
return ctrl.Result{RequeueAfter: time.Second}, nil
default:
return ctrl.Result{RequeueAfter: time.Second}, nil
}
}
// completePause finalizes the pause operation:
// 1. Normal mode: delete all Pods (cascade via OwnerRef)
// 2. Pool mode: solidify the source Pod template and clear poolRef. Pool Controller GC
// then releases and deletes the old allocated pool Pod.
// 3. Set phase=Paused only after pod deletion/release is accepted.
// 4. spec.pause remains unchanged; the next external request (or server retry bridge)
// is responsible for changing it.
func (r *BatchSandboxReconciler) completePause(ctx context.Context, bs *sandboxv1alpha1.BatchSandbox) error {
log := logf.FromContext(ctx)
poolStrategy := strategy.NewPoolStrategy(bs)
wasPooled := poolStrategy.IsPooledMode()
pods, err := r.listPods(ctx, poolStrategy, bs)
if err != nil {
return err
}
var pooledTemplate *corev1.PodTemplateSpec
if wasPooled {
if len(pods) == 0 {
return fmt.Errorf("no allocated pods found for pooled BatchSandbox %s/%s", bs.Namespace, bs.Name)
}
pooledTemplate = sourcePodTemplateForPause(pods[0])
if err := retry.RetryOnConflict(retry.DefaultBackoff, func() error {
latest := &sandboxv1alpha1.BatchSandbox{}
if err := r.Get(ctx, types.NamespacedName{Namespace: bs.Namespace, Name: bs.Name}, latest); err != nil {
return err
}
patch := client.MergeFrom(latest.DeepCopy())
latest.Spec.Template = pooledTemplate.DeepCopy()
latest.Spec.PoolRef = ""
controllerutil.RemoveFinalizer(latest, FinalizerPoolAllocation)
if latest.Annotations != nil {
delete(latest.Annotations, AnnoAllocReleaseKey)
}
return r.Patch(ctx, latest, patch)
}); err != nil {
return err
}
bs.Spec.Template = pooledTemplate.DeepCopy()
bs.Spec.PoolRef = ""
controllerutil.RemoveFinalizer(bs, FinalizerPoolAllocation)
if bs.Annotations != nil {
delete(bs.Annotations, AnnoAllocReleaseKey)
}
log.Info("Detached pooled BatchSandbox after pause", "sourcePod", pods[0].Name)
}
controllerKey := controllerutils.GetControllerKey(bs)
BatchSandboxScaleExpectations.DeleteExpectations(controllerKey)
log.Info("Cleared scale expectations before pod deletion", "controllerKey", controllerKey)
if !wasPooled {
for _, pod := range pods {
if err := r.Delete(ctx, pod); err != nil && !errors.IsNotFound(err) {
log.Error(err, "Failed to delete pod during pause", "pod", pod.Name)
return err
}
log.Info("Deleted pod during pause", "pod", pod.Name)
}
}
r.deleteTaskScheduler(ctx, bs)
var latest *sandboxv1alpha1.BatchSandbox
if err := retry.RetryOnConflict(retry.DefaultBackoff, func() error {
latest = &sandboxv1alpha1.BatchSandbox{}
if err := r.Get(ctx, types.NamespacedName{Namespace: bs.Namespace, Name: bs.Name}, latest); err != nil {
return err
}
latest.Status.Phase = sandboxv1alpha1.BatchSandboxPhasePaused
// Pool pause rewrites spec.template and clears spec.poolRef, which advances generation.
// Acknowledge that controller-owned spec change only while pause is still requested,
// so spec.pause=true is not replayed and a queued resume request is still observed.
if latest.Spec.Pause != nil && *latest.Spec.Pause {
latest.Status.PauseObservedGeneration = latest.Generation
}
applyBatchSandboxPhaseConditions(&latest.Status)
return r.Status().Update(ctx, latest)
}); err != nil {
return err
}
r.StatusRVExpectation.Expect(latest)
return nil
}
// continueResume continues the resume flow:
// 1. Read SandboxSnapshot status for image URIs
// 2. Replace template container images
// 3. Pool mode: clear poolRef
// 4. Leave spec.pause and spec.replicas untouched; normal reconciliation recreates
// the single supported runtime replica from the rewritten template.
func (r *BatchSandboxReconciler) continueResume(ctx context.Context, bs *sandboxv1alpha1.BatchSandbox) (ctrl.Result, error) {
log := logf.FromContext(ctx)
snapshot := &sandboxv1alpha1.SandboxSnapshot{}
if err := r.Get(ctx, types.NamespacedName{Namespace: bs.Namespace, Name: internalPauseSnapshotName(bs.Name)}, snapshot); err != nil {
if errors.IsNotFound(err) {
readyPodExists, readyErr := r.hasReadyResumePod(ctx, bs)
if readyErr != nil {
return ctrl.Result{}, readyErr
}
if readyPodExists {
log.Info("SandboxSnapshot missing for resume, but a ready pod already exists; treating resume as complete")
_ = r.ackPauseWithPhase(ctx, bs, sandboxv1alpha1.BatchSandboxPhaseSucceed, "")
_ = r.setCondition(ctx, bs, sandboxv1alpha1.BatchSandboxConditionResumeFailed, sandboxv1alpha1.ConditionFalse, "", "")
_ = r.setCondition(ctx, bs, sandboxv1alpha1.BatchSandboxConditionPodFailed, sandboxv1alpha1.ConditionFalse, "", "")
return ctrl.Result{}, nil
}
log.Info("SandboxSnapshot not found for resume, rolling back to Paused")
_ = r.ackPauseWithPhase(ctx, bs, sandboxv1alpha1.BatchSandboxPhasePaused, "")
_ = r.setCondition(ctx, bs, sandboxv1alpha1.BatchSandboxConditionResumeFailed, sandboxv1alpha1.ConditionTrue, "SnapshotNotFound", "SandboxSnapshot not found")
return ctrl.Result{}, nil
}
return ctrl.Result{}, err
}
if snapshot.Status.Phase != sandboxv1alpha1.SandboxSnapshotPhaseSucceed {
msg := fmt.Sprintf("snapshot not ready: phase=%s", snapshot.Status.Phase)
log.Error(nil, msg)
_ = r.ackPauseWithPhase(ctx, bs, sandboxv1alpha1.BatchSandboxPhasePaused, "")
_ = r.setCondition(ctx, bs, sandboxv1alpha1.BatchSandboxConditionResumeFailed, sandboxv1alpha1.ConditionTrue, "SnapshotNotReady", msg)
return ctrl.Result{}, nil
}
imageMap := make(map[string]string)
for _, c := range snapshot.Status.Containers {
imageMap[c.ContainerName] = c.ImageURI
}
var patched *sandboxv1alpha1.BatchSandbox
if err := retry.RetryOnConflict(retry.DefaultBackoff, func() error {
latest := &sandboxv1alpha1.BatchSandbox{}
if err := r.Get(ctx, types.NamespacedName{Namespace: bs.Namespace, Name: bs.Name}, latest); err != nil {
return err
}
patch := client.MergeFrom(latest.DeepCopy())
if latest.Spec.Template != nil {
for i := range latest.Spec.Template.Spec.Containers {
if img, ok := imageMap[latest.Spec.Template.Spec.Containers[i].Name]; ok {
latest.Spec.Template.Spec.Containers[i].Image = img
}
}
ensureImagePullSecret(latest.Spec.Template, r.ResumePullSecret)
}
if latest.Spec.PoolRef != "" {
latest.Spec.PoolRef = ""
controllerutil.RemoveFinalizer(latest, FinalizerPoolAllocation)
}
if err := r.Patch(ctx, latest, patch); err != nil {
return err
}
patched = latest.DeepCopy()
return nil
}); err != nil {
return ctrl.Result{}, err
}
if patched != nil {
bs.ObjectMeta = patched.ObjectMeta
bs.Spec = patched.Spec
}
return ctrl.Result{RequeueAfter: time.Second}, nil
}
func (r *BatchSandboxReconciler) ackPauseGeneration(ctx context.Context, bs *sandboxv1alpha1.BatchSandbox) error {
var latest *sandboxv1alpha1.BatchSandbox
if err := retry.RetryOnConflict(retry.DefaultBackoff, func() error {
latest = &sandboxv1alpha1.BatchSandbox{}
if err := r.Get(ctx, types.NamespacedName{Namespace: bs.Namespace, Name: bs.Name}, latest); err != nil {
return err
}
latest.Status.PauseObservedGeneration = latest.Generation
applyBatchSandboxPhaseConditions(&latest.Status)
return r.Status().Update(ctx, latest)
}); err != nil {
return err
}
r.StatusRVExpectation.Expect(latest)
bs.Status.PauseObservedGeneration = bs.Generation
applyBatchSandboxPhaseConditions(&bs.Status)
return nil
}
func (r *BatchSandboxReconciler) ackPauseWithPhase(ctx context.Context, bs *sandboxv1alpha1.BatchSandbox, phase sandboxv1alpha1.BatchSandboxPhase, _ string) error {
var latest *sandboxv1alpha1.BatchSandbox
if err := retry.RetryOnConflict(retry.DefaultBackoff, func() error {
latest = &sandboxv1alpha1.BatchSandbox{}
if err := r.Get(ctx, types.NamespacedName{Namespace: bs.Namespace, Name: bs.Name}, latest); err != nil {
return err
}
latest.Status.PauseObservedGeneration = latest.Generation
latest.Status.Phase = phase
applyBatchSandboxPhaseConditions(&latest.Status)
return r.Status().Update(ctx, latest)
}); err != nil {
return err
}
r.StatusRVExpectation.Expect(latest)
bs.Status.PauseObservedGeneration = bs.Generation
bs.Status.Phase = phase
applyBatchSandboxPhaseConditions(&bs.Status)
return nil
}
// findPodForSandbox finds the Pod associated with a BatchSandbox.
func (r *BatchSandboxReconciler) findPodForSandbox(ctx context.Context, bs *sandboxv1alpha1.BatchSandbox) (*corev1.Pod, error) {
poolStrategy := strategy.NewPoolStrategy(bs)
pods, err := r.listPods(ctx, poolStrategy, bs)
if err != nil {
return nil, err
}
if len(pods) == 0 {
return nil, fmt.Errorf("no pods found for BatchSandbox %s/%s", bs.Namespace, bs.Name)
}
return pods[0], nil
}
func (r *BatchSandboxReconciler) setCondition(
ctx context.Context,
bs *sandboxv1alpha1.BatchSandbox,
conditionType sandboxv1alpha1.BatchSandboxConditionType,
status string,
reason string,
message string,
) error {
var latest *sandboxv1alpha1.BatchSandbox
if err := retry.RetryOnConflict(retry.DefaultBackoff, func() error {
latest = &sandboxv1alpha1.BatchSandbox{}
if err := r.Get(ctx, types.NamespacedName{Namespace: bs.Namespace, Name: bs.Name}, latest); err != nil {
return err
}
var conditions []sandboxv1alpha1.BatchSandboxCondition
found := false
for _, c := range latest.Status.Conditions {
if c.Type == conditionType {
if status == sandboxv1alpha1.ConditionFalse {
continue
}
c.Status = status
c.Reason = reason
c.Message = message
c.LastTransitionTime = ptr.To(metav1.Now())
found = true
}
conditions = append(conditions, c)
}
if !found && status == sandboxv1alpha1.ConditionTrue {
conditions = append(conditions, sandboxv1alpha1.BatchSandboxCondition{
Type: conditionType,
Status: status,
Reason: reason,
Message: message,
LastTransitionTime: ptr.To(metav1.Now()),
})
}
latest.Status.Conditions = conditions
return r.Status().Update(ctx, latest)
}); err != nil {
return err
}
r.StatusRVExpectation.Expect(latest)
return nil
}