Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions api/v1alpha1/sandboxclaim_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ type SandboxClaimSpec struct {
// +optional
ShutdownTime *metav1.Time `json:"shutdownTime,omitempty"`

// PauseTime specifies the absolute time when the sandbox should be paused automatically
// This will be set as spec.pauseTime (absolute time) on the Sandbox
// +optional
PauseTime *metav1.Time `json:"pauseTime,omitempty"`

// ClaimTimeout specifies the maximum duration to wait for claiming sandboxes
// If the timeout is reached, the claim will be marked as Completed regardless of
// whether all replicas were successfully claimed
Expand Down
4 changes: 4 additions & 0 deletions api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions config/crd/bases/agents.kruise.io_sandboxclaims.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,12 @@ spec:
- name
type: object
type: array
pauseTime:
description: |-
PauseTime specifies the absolute time when the sandbox should be paused automatically
This will be set as spec.pauseTime (absolute time) on the Sandbox
format: date-time
type: string
shutdownTime:
description: |-
ShutdownTime specifies the absolute time when the sandbox should be shut down
Expand Down
15 changes: 10 additions & 5 deletions pkg/controller/sandboxclaim/core/common_control.go
Original file line number Diff line number Diff line change
Expand Up @@ -261,11 +261,16 @@ func (c *commonControl) buildClaimOptions(ctx context.Context, claim *agentsv1al
sbx.SetAnnotations(annotations)
}

// 3. apply shutdownTime
if claim.Spec.ShutdownTime != nil {
sbx.SetTimeout(infra.TimeoutOptions{
ShutdownTime: claim.Spec.ShutdownTime.Time,
})
// 3. apply shutdownTime and pauseTime
if claim.Spec.ShutdownTime != nil || claim.Spec.PauseTime != nil {
opts := infra.TimeoutOptions{}
if claim.Spec.ShutdownTime != nil {
opts.ShutdownTime = claim.Spec.ShutdownTime.Time
}
if claim.Spec.PauseTime != nil {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add e2e for this case?

opts.PauseTime = claim.Spec.PauseTime.Time
}
sbx.SetTimeout(opts)
}
},
ReserveFailedSandbox: claim.Spec.ReserveFailedSandbox,
Expand Down
88 changes: 87 additions & 1 deletion pkg/controller/sandboxclaim/core/common_control_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -695,6 +695,7 @@ func TestCommonControl_buildClaimOptions(t *testing.T) {

ctx := context.Background()
shutdownTime := metav1.Now()
pauseTime := metav1.Now()
timeoutDuration := metav1.Duration{Duration: 3 * time.Minute}

tests := []struct {
Expand Down Expand Up @@ -802,6 +803,91 @@ func TestCommonControl_buildClaimOptions(t *testing.T) {
}
},
},
{
name: "claim with pauseTime",
claim: &agentsv1alpha1.SandboxClaim{
ObjectMeta: metav1.ObjectMeta{
Name: "test-claim",
Namespace: "default",
UID: "test-uid-pause",
},
Spec: agentsv1alpha1.SandboxClaimSpec{
TemplateName: "test-template",
PauseTime: &pauseTime,
},
},
sandboxSet: &agentsv1alpha1.SandboxSet{
ObjectMeta: metav1.ObjectMeta{
Name: "test-template",
Namespace: "default",
},
},
expectError: false,
validate: func(t *testing.T, opts infra.ClaimSandboxOptions) {
if opts.User != "test-uid-pause" {
t.Errorf("User = %v, want %v", opts.User, "test-uid-pause")
}
if opts.Modifier == nil {
t.Fatal("Modifier should not be nil")
}
mockSandbox := &sandboxcr.Sandbox{
Sandbox: &agentsv1alpha1.Sandbox{
ObjectMeta: metav1.ObjectMeta{Name: "test-sandbox", Namespace: "default"},
},
}
opts.Modifier(mockSandbox)

// validate PauseTime
if mockSandbox.Sandbox.Spec.PauseTime == nil || *mockSandbox.Sandbox.Spec.PauseTime != pauseTime {
t.Error("Expected PauseTime to be set on sandbox")
}
},
},
{
name: "claim with both shutdownTime and pauseTime",
claim: &agentsv1alpha1.SandboxClaim{
ObjectMeta: metav1.ObjectMeta{
Name: "test-claim",
Namespace: "default",
UID: "test-uid-both-times",
},
Spec: agentsv1alpha1.SandboxClaimSpec{
TemplateName: "test-template",
ShutdownTime: &shutdownTime,
PauseTime: &pauseTime,
},
},
sandboxSet: &agentsv1alpha1.SandboxSet{
ObjectMeta: metav1.ObjectMeta{
Name: "test-template",
Namespace: "default",
},
},
expectError: false,
validate: func(t *testing.T, opts infra.ClaimSandboxOptions) {
if opts.User != "test-uid-both-times" {
t.Errorf("User = %v, want %v", opts.User, "test-uid-both-times")
}
if opts.Modifier == nil {
t.Fatal("Modifier should not be nil")
}
mockSandbox := &sandboxcr.Sandbox{
Sandbox: &agentsv1alpha1.Sandbox{
ObjectMeta: metav1.ObjectMeta{Name: "test-sandbox", Namespace: "default"},
},
}
opts.Modifier(mockSandbox)

// validate PauseTime
if mockSandbox.Sandbox.Spec.PauseTime == nil || *mockSandbox.Sandbox.Spec.PauseTime != pauseTime {
t.Error("Expected PauseTime to be set on sandbox")
}
// validate ShutdownTime
if mockSandbox.Sandbox.Spec.ShutdownTime == nil || *mockSandbox.Sandbox.Spec.ShutdownTime != shutdownTime {
t.Error("Expected ShutdownTime to be set on sandbox")
}
},
},
{
name: "claim with inplaceUpdate - image only",
claim: &agentsv1alpha1.SandboxClaim{
Expand Down Expand Up @@ -2232,4 +2318,4 @@ func TestBuildClaimOptions_CSIMount_Test(t *testing.T) {
}
})
}
}
}
106 changes: 101 additions & 5 deletions test/e2e/sandboxclaim_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,20 +38,23 @@ var _ = Describe("SandboxClaim", func() {
namespace string
)

// Helper function to list sandboxes claimed by a specific SandboxClaim
// Uses AnnotationOwner (which stores the claim's UID) for filtering
// Helper function to list sandboxes claimed by a specific SandboxClaim.
// Match AnnotationOwner (claim UID) and/or LabelSandboxClaimName — the controller sets both,
// but listing by label alone stays correct if the UID is not yet visible on a stale client read.
listClaimedSandboxes := func(ctx context.Context, claim *agentsv1alpha1.SandboxClaim) ([]agentsv1alpha1.Sandbox, error) {
sandboxList := &agentsv1alpha1.SandboxList{}
if err := k8sClient.List(ctx, sandboxList, client.InNamespace(claim.Namespace)); err != nil {
return nil, err
}

// AnnotationOwner is set to the claim's UID for uniqueness
expectedOwner := string(claim.UID)
claimed := []agentsv1alpha1.Sandbox{}
for _, sandbox := range sandboxList.Items {
if sandbox.Annotations != nil &&
sandbox.Annotations[agentsv1alpha1.AnnotationOwner] == expectedOwner {
byOwner := sandbox.Annotations != nil &&
sandbox.Annotations[agentsv1alpha1.AnnotationOwner] == expectedOwner
byClaimName := sandbox.Labels != nil &&
sandbox.Labels[agentsv1alpha1.LabelSandboxClaimName] == claim.Name
if byOwner || byClaimName {
claimed = append(claimed, sandbox)
}
}
Expand Down Expand Up @@ -281,6 +284,99 @@ var _ = Describe("SandboxClaim", func() {
})
})

Context("PauseTime propagation from SandboxClaim", func() {
var (
sandboxSet *agentsv1alpha1.SandboxSet
sandboxClaim *agentsv1alpha1.SandboxClaim
)

BeforeEach(func() {
sandboxSet = &agentsv1alpha1.SandboxSet{
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("test-pool-pausetime-%d", time.Now().UnixNano()),
Namespace: namespace,
},
Spec: agentsv1alpha1.SandboxSetSpec{
Replicas: 3,
EmbeddedSandboxTemplate: agentsv1alpha1.EmbeddedSandboxTemplate{
Template: &corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "test-container",
Image: "nginx:stable-alpine3.23",
},
},
},
},
},
},
}
Expect(k8sClient.Create(ctx, sandboxSet)).To(Succeed())

Eventually(func() int32 {
_ = k8sClient.Get(ctx, types.NamespacedName{
Name: sandboxSet.Name,
Namespace: sandboxSet.Namespace,
}, sandboxSet)
return sandboxSet.Status.AvailableReplicas
}, time.Minute*2, time.Second).Should(Equal(int32(3)))
})

AfterEach(func() {
if sandboxClaim != nil {
_ = k8sClient.Delete(ctx, sandboxClaim)
}
if sandboxSet != nil {
_ = k8sClient.Delete(ctx, sandboxSet)
}
})

It("should propagate pauseTime from claim spec to the claimed sandbox", func() {
pauseAt := metav1.NewTime(time.Date(2099, 7, 1, 12, 0, 0, 0, time.UTC))

By("Creating a SandboxClaim with pauseTime")
sandboxClaim = &agentsv1alpha1.SandboxClaim{
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("test-claim-pause-%d", time.Now().UnixNano()),
Namespace: namespace,
},
Spec: agentsv1alpha1.SandboxClaimSpec{
TemplateName: sandboxSet.Name,
Replicas: ptr.To(int32(1)),
PauseTime: &pauseAt,
ClaimTimeout: &metav1.Duration{Duration: 5 * time.Minute},
},
}
Expect(k8sClient.Create(ctx, sandboxClaim)).To(Succeed())

var claimedSandboxes []agentsv1alpha1.Sandbox
By("Waiting for successful completion and the claimed sandbox")
Eventually(func(g Gomega) {
g.Expect(k8sClient.Get(ctx, types.NamespacedName{
Name: sandboxClaim.Name,
Namespace: sandboxClaim.Namespace,
}, sandboxClaim)).To(Succeed())
g.Expect(sandboxClaim.Status.Phase).To(Equal(agentsv1alpha1.SandboxClaimPhaseCompleted))
g.Expect(sandboxClaim.Status.ClaimedReplicas).To(Equal(int32(1)))
g.Expect(sandboxClaim.Status.Message).To(ContainSubstring("Successfully claimed"))
list, err := listClaimedSandboxes(ctx, sandboxClaim)
g.Expect(err).NotTo(HaveOccurred())
g.Expect(list).To(HaveLen(1))
claimedSandboxes = list
}).WithTimeout(3 * time.Minute).WithPolling(2 * time.Second).Should(Succeed())

fresh := &agentsv1alpha1.Sandbox{}
Expect(k8sClient.Get(ctx, types.NamespacedName{
Namespace: namespace,
Name: claimedSandboxes[0].Name,
}, fresh)).To(Succeed())
Expect(fresh.Spec.PauseTime).NotTo(BeNil())
Expect(fresh.Spec.PauseTime.Time).To(BeTemporally("~", pauseAt.Time, time.Minute))
Expect(fresh.Spec.ShutdownTime).To(BeNil())
})
})

Context("Replicas immutability (webhook validation)", func() {
var sandboxClaim *agentsv1alpha1.SandboxClaim

Expand Down
Loading