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
2 changes: 2 additions & 0 deletions api/v1alpha1/sandboxset_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ const (
LabelTemplateHash = InternalPrefix + "template-hash"
// LabelSandboxReservedFailed marks a failed sandbox retained for debugging.
LabelSandboxReservedFailed = InternalPrefix + "reserved-failed-sandbox"
// LabelSandboxID is the label key containing the resolved short sandbox ID.
LabelSandboxID = InternalPrefix + "sandbox-id"

AnnotationLock = InternalPrefix + "lock"
AnnotationOwner = InternalPrefix + "owner"
Expand Down
3 changes: 3 additions & 0 deletions pkg/controller/sandbox/core/recycle.go
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,9 @@ func (r *SandboxRecycleControl) resetMetadataForPool(ctx context.Context, box *a
return fmt.Errorf("failed to unmarshal updated-metadata-in-claim: %w", err)
}
for _, key := range updated.Labels {
if key == agentsv1alpha1.LabelSandboxID {
continue
}
delete(box.Labels, key)
}
for _, key := range updated.Annotations {
Expand Down
34 changes: 34 additions & 0 deletions pkg/controller/sandbox/core/recycle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1229,6 +1229,40 @@ func TestResetForPool(t *testing.T) {
},
expectError: "failed to unmarshal updated-metadata-in-claim",
},
{
name: "preserves LabelSandboxID even if specified in user updated labels",
box: &agentsv1alpha1.Sandbox{
ObjectMeta: metav1.ObjectMeta{
Name: "test-sandbox",
Namespace: "default",
Labels: map[string]string{
agentsv1alpha1.LabelSandboxPool: "test-pool",
agentsv1alpha1.LabelSandboxIsClaimed: "true",
agentsv1alpha1.LabelSandboxID: "my-short-id",
"user-label": "user-value",
},
Annotations: map[string]string{
agentsv1alpha1.AnnotationCleanup: "true",
agentsv1alpha1.AnnotationUpdatedMetadataInClaim: mustMarshal(agentsv1alpha1.UpdatedMetadataInClaim{
Labels: []string{"user-label", agentsv1alpha1.LabelSandboxID},
}),
},
},
},
sbs: &agentsv1alpha1.SandboxSet{
ObjectMeta: metav1.ObjectMeta{
Name: "test-pool",
Namespace: "default",
UID: types.UID("test-uid"),
},
Spec: agentsv1alpha1.SandboxSetSpec{Replicas: 1},
},
expectLabels: map[string]string{
agentsv1alpha1.LabelSandboxPool: "test-pool",
agentsv1alpha1.LabelSandboxIsClaimed: agentsv1alpha1.False,
agentsv1alpha1.LabelSandboxID: "my-short-id",
},
},
}

for _, tt := range tests {
Expand Down
4 changes: 2 additions & 2 deletions pkg/sandbox-manager/infra/sandboxcr/claim_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -409,7 +409,7 @@ func TestInfra_ClaimSandbox(t *testing.T) {
postCheck: func(t *testing.T, sbx infra.Sandbox) {
assert.Equal(t, "new-image", sbx.(*Sandbox).Spec.Template.Spec.Containers[0].Image)
metrics := GetMetricsFromSandbox(t, sbx)
assert.Greater(t, metrics.WaitReady, time.Duration(0))
assert.GreaterOrEqual(t, metrics.WaitReady, time.Duration(0))
},
},
{
Expand Down Expand Up @@ -445,7 +445,7 @@ func TestInfra_ClaimSandbox(t *testing.T) {
},
postCheck: func(t *testing.T, sbx infra.Sandbox) {
metrics := GetMetricsFromSandbox(t, sbx)
assert.Greater(t, metrics.WaitReady, time.Duration(0))
assert.GreaterOrEqual(t, metrics.WaitReady, time.Duration(0))
},
},
{
Expand Down
2 changes: 1 addition & 1 deletion pkg/sandbox-manager/infra/sandboxcr/clone_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1606,7 +1606,7 @@ func TestCloneSandbox_WithRateLimiter(t *testing.T) {
assert.Nil(t, sbx, "sandbox should be nil when rate limited")
assert.Error(t, err, "should return error when rate limited")
assert.Contains(t, err.Error(), "rate:", "error should indicate rate limit")
assert.Greater(t, metrics.Wait, time.Duration(0), "wait metric should include limiter wait cost")
assert.GreaterOrEqual(t, metrics.Wait, time.Duration(0), "wait metric should include limiter wait cost")
// Limiter runs after GetTemplate, so Total covers both stages and is at
// least Wait. This mirrors ClaimSandbox where the limiter is gated after
// the pick/lock stage.
Expand Down
88 changes: 88 additions & 0 deletions pkg/sandbox-manager/sandboxid/sandboxid.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
Copyright 2026.

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 sandboxid

import (
"encoding/base32"
"fmt"
"strings"

"github.com/google/uuid"
"github.com/openkruise/agents/api/v1alpha1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
types "k8s.io/apimachinery/pkg/types"
)

// LabelKey is the reserved Sandbox label key containing the resolved short sandbox ID.
const LabelKey = v1alpha1.LabelSandboxID

// Resolve returns the authoritative sandbox ID.
// If the sandbox-id label is present and non-empty, returns the label value.
// Otherwise, falls back to the legacy "<namespace>--<name>" format.
func Resolve(sandbox metav1.Object) string {
if sandbox == nil {
return ""
}
labels := sandbox.GetLabels()
if val, ok := labels[LabelKey]; ok && val != "" {
return val
}
return Legacy(sandbox.GetNamespace(), sandbox.GetName())
}

// Legacy returns the legacy deterministic format "<namespace>--<name>".
func Legacy(namespace, name string) string {
return fmt.Sprintf("%s--%s", namespace, name)
}

// GenerateShort decodes a Kubernetes UID as a 16-byte UUID and encodes it into 26 lowercase Base32 characters.
func GenerateShort(uid types.UID) (string, error) {
parsedUUID, err := uuid.Parse(string(uid))
if err != nil {
return "", fmt.Errorf("failed to parse UID as UUID: %w", err)
}
// UUID is 16 bytes
bytes := parsedUUID[:]

// RFC 4648 Base32 encoding with padding removed
encoder := base32.StdEncoding.WithPadding(base32.NoPadding)
encoded := encoder.EncodeToString(bytes)

return strings.ToLower(encoded), nil
}

// AssignShort checks if the sandbox already has a sandbox-id label.
// If not, generates a short ID from its UID and sets it as the label.
func AssignShort(sandbox metav1.Object) (changed bool, err error) {
if sandbox == nil {
return false, fmt.Errorf("sandbox is nil")
}
labels := sandbox.GetLabels()
if labels == nil {
labels = make(map[string]string)
}
if val, ok := labels[LabelKey]; ok && val != "" {
return false, nil
}
shortID, err := GenerateShort(sandbox.GetUID())
if err != nil {
return false, err
}
labels[LabelKey] = shortID
sandbox.SetLabels(labels)
return true, nil
}
Loading
Loading