Skip to content

Commit 124a2b4

Browse files
committed
Merge branch 'pr/workspace-resources-only'
2 parents ba1a1e2 + c62e038 commit 124a2b4

File tree

7 files changed

+531
-4
lines changed

7 files changed

+531
-4
lines changed

controllers/sandbox_controller.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,9 @@ func (r *SandboxReconciler) reconcilePod(ctx context.Context, sandbox *sandboxv1
423423
if pod.Labels == nil {
424424
pod.Labels = make(map[string]string)
425425
}
426+
if pod.Annotations == nil {
427+
pod.Annotations = make(map[string]string)
428+
}
426429
changed := false
427430
if pod.Labels[sandboxLabel] != nameHash {
428431
pod.Labels[sandboxLabel] = nameHash
@@ -435,6 +438,12 @@ func (r *SandboxReconciler) reconcilePod(ctx context.Context, sandbox *sandboxv1
435438
changed = true
436439
}
437440
}
441+
for k, v := range sandbox.Spec.PodTemplate.ObjectMeta.Annotations {
442+
if pod.Annotations[k] != v {
443+
pod.Annotations[k] = v
444+
changed = true
445+
}
446+
}
438447

439448
// Set controller reference if the pod is not controlled by anything.
440449
if controllerRef := metav1.GetControllerOf(pod); controllerRef == nil {

controllers/sandbox_controller_test.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -614,6 +614,9 @@ func TestReconcilePod(t *testing.T) {
614614
"agents.x-k8s.io/sandbox-name-hash": nameHash,
615615
"custom-label": "label-val",
616616
},
617+
Annotations: map[string]string{
618+
"custom-annotation": "anno-val",
619+
},
617620
OwnerReferences: []metav1.OwnerReference{sandboxControllerRef(sandboxName)},
618621
},
619622
Spec: corev1.PodSpec{
@@ -686,7 +689,7 @@ func TestReconcilePod(t *testing.T) {
686689
wantPod: nil,
687690
},
688691
{
689-
name: "adopts existing pod via annotation - pod gets label and owner reference",
692+
name: "adopts existing pod via annotation - pod gets metadata and owner reference",
690693
initialObjs: []runtime.Object{
691694
&corev1.Pod{
692695
ObjectMeta: metav1.ObjectMeta{
@@ -718,6 +721,9 @@ func TestReconcilePod(t *testing.T) {
718721
Labels: map[string]string{
719722
extensionsv1alpha1.SandboxIDLabel: "claim-uid-1",
720723
},
724+
Annotations: map[string]string{
725+
"example.com/workspace": "true",
726+
},
721727
},
722728
Spec: corev1.PodSpec{
723729
Containers: []corev1.Container{
@@ -738,6 +744,9 @@ func TestReconcilePod(t *testing.T) {
738744
sandboxLabel: nameHash,
739745
extensionsv1alpha1.SandboxIDLabel: "claim-uid-1",
740746
},
747+
Annotations: map[string]string{
748+
"example.com/workspace": "true",
749+
},
741750
OwnerReferences: []metav1.OwnerReference{sandboxControllerRef(sandboxName)},
742751
},
743752
Spec: corev1.PodSpec{
@@ -790,6 +799,9 @@ func TestReconcilePod(t *testing.T) {
790799
"agents.x-k8s.io/sandbox-name-hash": nameHash,
791800
"custom-label": "label-val",
792801
},
802+
Annotations: map[string]string{
803+
"custom-annotation": "anno-val",
804+
},
793805
// Should still have the original controller reference
794806
OwnerReferences: []metav1.OwnerReference{
795807
{

extensions/api/v1alpha1/sandboxclaim_types.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,21 @@ type SandboxTemplateRef struct {
6666
Name string `json:"name,omitempty" protobuf:"bytes,1,name=name"`
6767
}
6868

69+
// WorkspaceResources defines per-claim resource overrides for the workspace container.
70+
type WorkspaceResources struct {
71+
// CPUMillicores is the desired CPU request/limit for the workspace container.
72+
// +optional
73+
CPUMillicores int32 `json:"cpuMillicores,omitempty"`
74+
75+
// MemoryMB is the desired memory request/limit for the workspace container.
76+
// +optional
77+
MemoryMB int32 `json:"memoryMB,omitempty"`
78+
79+
// DiskGB is the desired ephemeral-storage request/limit for the workspace container.
80+
// +optional
81+
DiskGB int32 `json:"diskGB,omitempty"`
82+
}
83+
6984
// SandboxClaimSpec defines the desired state of Sandbox
7085
type SandboxClaimSpec struct {
7186
// sandboxTemplateRef defines the name of the SandboxTemplate to be used for creating a Sandbox.
@@ -75,6 +90,10 @@ type SandboxClaimSpec struct {
7590
// lifecycle defines when and how the SandboxClaim should be shut down.
7691
// +optional
7792
Lifecycle *Lifecycle `json:"lifecycle,omitempty"`
93+
94+
// WorkspaceResources overrides resource requests/limits for the workspace container at claim time.
95+
// +optional
96+
WorkspaceResources *WorkspaceResources `json:"workspaceResources,omitempty"`
7897
}
7998

8099
// SandboxClaimStatus defines the observed state of Sandbox.

extensions/api/v1alpha1/zz_generated.deepcopy.go

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

extensions/controllers/sandboxclaim_controller.go

Lines changed: 164 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"k8s.io/apimachinery/pkg/api/equality"
2727
k8errors "k8s.io/apimachinery/pkg/api/errors"
2828
"k8s.io/apimachinery/pkg/api/meta"
29+
"k8s.io/apimachinery/pkg/api/resource"
2930
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3031
"k8s.io/apimachinery/pkg/runtime"
3132
"k8s.io/client-go/tools/record"
@@ -347,8 +348,154 @@ func ensureClaimIdentityLabels(labels map[string]string, claim *extensionsv1alph
347348
return labels
348349
}
349350

351+
func applyWorkspaceResourceOverrides(container *corev1.Container, overrides *extensionsv1alpha1.WorkspaceResources) {
352+
if overrides == nil {
353+
return
354+
}
355+
if container.Resources.Requests == nil {
356+
container.Resources.Requests = corev1.ResourceList{}
357+
}
358+
if container.Resources.Limits == nil {
359+
container.Resources.Limits = corev1.ResourceList{}
360+
}
361+
if overrides.CPUMillicores > 0 {
362+
qty := *resource.NewMilliQuantity(int64(overrides.CPUMillicores), resource.DecimalSI)
363+
container.Resources.Requests[corev1.ResourceCPU] = qty
364+
container.Resources.Limits[corev1.ResourceCPU] = qty
365+
}
366+
if overrides.MemoryMB > 0 {
367+
qty := *resource.NewQuantity(int64(overrides.MemoryMB)*1024*1024, resource.BinarySI)
368+
container.Resources.Requests[corev1.ResourceMemory] = qty
369+
container.Resources.Limits[corev1.ResourceMemory] = qty
370+
}
371+
if overrides.DiskGB > 0 {
372+
qty := *resource.NewQuantity(int64(overrides.DiskGB)*1024*1024*1024, resource.BinarySI)
373+
container.Resources.Requests[corev1.ResourceEphemeralStorage] = qty
374+
container.Resources.Limits[corev1.ResourceEphemeralStorage] = qty
375+
}
376+
}
377+
378+
func applyClaimWorkspaceResourcesToPodSpec(spec *corev1.PodSpec, claim *extensionsv1alpha1.SandboxClaim) {
379+
if claim.Spec.WorkspaceResources == nil {
380+
return
381+
}
382+
for i := range spec.Containers {
383+
container := &spec.Containers[i]
384+
if container.Name != "workspace" {
385+
continue
386+
}
387+
applyWorkspaceResourceOverrides(container, claim.Spec.WorkspaceResources)
388+
}
389+
}
390+
391+
// reconcileWorkspaceResources patches the pod's workspace container resources
392+
// in-place if the claim's WorkspaceResources differ from the pod's current values.
393+
// This triggers Kubernetes InPlacePodVerticalScaling (K8s 1.27+).
394+
func (r *SandboxClaimReconciler) reconcileWorkspaceResources(ctx context.Context, sandbox *v1alpha1.Sandbox, claim *extensionsv1alpha1.SandboxClaim) error {
395+
if claim.Spec.WorkspaceResources == nil {
396+
return nil
397+
}
398+
logger := log.FromContext(ctx)
399+
400+
// Find the pod owned by this sandbox.
401+
pod := &corev1.Pod{}
402+
if err := r.Get(ctx, client.ObjectKey{Namespace: sandbox.Namespace, Name: sandbox.Name}, pod); err != nil {
403+
return nil // Pod may not exist yet (still starting).
404+
}
405+
if pod.Status.Phase != corev1.PodRunning {
406+
return nil // Only resize running pods.
407+
}
408+
409+
for i, c := range pod.Spec.Containers {
410+
if c.Name != "workspace" {
411+
continue
412+
}
413+
patch := buildResizePatch(c.Resources, claim.Spec.WorkspaceResources)
414+
if patch == nil {
415+
return nil // No change needed.
416+
}
417+
418+
// Patch pod resources in-place. On K8s 1.27+ with InPlacePodVerticalScaling,
419+
// the kubelet detects the resource change and calls UpdateContainerResources
420+
// on the container runtime (e.g., isol8-runtime update).
421+
podPatch := pod.DeepCopy()
422+
podPatch.Spec.Containers[i].Resources = *patch
423+
if err := r.Patch(ctx, podPatch, client.StrategicMergeFrom(pod)); err != nil {
424+
return fmt.Errorf("patch pod resources: %w", err)
425+
}
426+
logger.Info("resized workspace container", "pod", pod.Name,
427+
"cpuMillicores", claim.Spec.WorkspaceResources.CPUMillicores,
428+
"memoryMB", claim.Spec.WorkspaceResources.MemoryMB)
429+
return nil
430+
}
431+
return nil
432+
}
433+
434+
// buildResizePatch compares current container resources with the desired
435+
// WorkspaceResources and returns updated ResourceRequirements if they differ.
436+
// Returns nil if no change is needed. DiskGB is intentionally excluded
437+
// because ephemeral storage cannot be resized in-place.
438+
func buildResizePatch(current corev1.ResourceRequirements, desired *extensionsv1alpha1.WorkspaceResources) *corev1.ResourceRequirements {
439+
target := corev1.ResourceRequirements{
440+
Requests: current.Requests.DeepCopy(),
441+
Limits: current.Limits.DeepCopy(),
442+
}
443+
if target.Requests == nil {
444+
target.Requests = corev1.ResourceList{}
445+
}
446+
if target.Limits == nil {
447+
target.Limits = corev1.ResourceList{}
448+
}
449+
changed := false
450+
451+
if desired.CPUMillicores > 0 {
452+
qty := *resource.NewMilliQuantity(int64(desired.CPUMillicores), resource.DecimalSI)
453+
if !current.Limits[corev1.ResourceCPU].Equal(qty) {
454+
target.Requests[corev1.ResourceCPU] = qty
455+
target.Limits[corev1.ResourceCPU] = qty
456+
changed = true
457+
}
458+
}
459+
if desired.MemoryMB > 0 {
460+
qty := *resource.NewQuantity(int64(desired.MemoryMB)*1024*1024, resource.BinarySI)
461+
if !current.Limits[corev1.ResourceMemory].Equal(qty) {
462+
target.Requests[corev1.ResourceMemory] = qty
463+
target.Limits[corev1.ResourceMemory] = qty
464+
changed = true
465+
}
466+
}
467+
468+
if !changed {
469+
return nil
470+
}
471+
return &target
472+
}
473+
474+
func mergeTemplatePodMetadata(target *v1alpha1.PodMetadata, template v1alpha1.PodMetadata) {
475+
if len(template.Labels) > 0 {
476+
if target.Labels == nil {
477+
target.Labels = make(map[string]string, len(template.Labels))
478+
}
479+
for k, v := range template.Labels {
480+
if _, exists := target.Labels[k]; !exists {
481+
target.Labels[k] = v
482+
}
483+
}
484+
}
485+
if len(template.Annotations) > 0 {
486+
if target.Annotations == nil {
487+
target.Annotations = make(map[string]string, len(template.Annotations))
488+
}
489+
for k, v := range template.Annotations {
490+
if _, exists := target.Annotations[k]; !exists {
491+
target.Annotations[k] = v
492+
}
493+
}
494+
}
495+
}
496+
350497
// adoptSandboxFromCandidates picks the best candidate and transfers ownership to the claim.
351-
func (r *SandboxClaimReconciler) adoptSandboxFromCandidates(ctx context.Context, claim *extensionsv1alpha1.SandboxClaim, candidates []*v1alpha1.Sandbox) (*v1alpha1.Sandbox, error) {
498+
func (r *SandboxClaimReconciler) adoptSandboxFromCandidates(ctx context.Context, claim *extensionsv1alpha1.SandboxClaim, template *extensionsv1alpha1.SandboxTemplate, candidates []*v1alpha1.Sandbox) (*v1alpha1.Sandbox, error) {
352499
log := log.FromContext(ctx)
353500

354501
// Sort: ready sandboxes first, then by creation time (oldest first)
@@ -410,9 +557,14 @@ func (r *SandboxClaimReconciler) adoptSandboxFromCandidates(ctx context.Context,
410557
adopted.Annotations[asmetrics.TraceContextAnnotation] = tc
411558
}
412559

560+
if template != nil {
561+
mergeTemplatePodMetadata(&adopted.Spec.PodTemplate.ObjectMeta, template.Spec.PodTemplate.ObjectMeta)
562+
}
563+
413564
// Propagate claim identity labels for discovery and NetworkPolicy targeting
414565
adopted.Labels = ensureClaimIdentityLabels(adopted.Labels, claim)
415566
adopted.Spec.PodTemplate.ObjectMeta.Labels = ensureClaimIdentityLabels(adopted.Spec.PodTemplate.ObjectMeta.Labels, claim)
567+
applyClaimWorkspaceResourcesToPodSpec(&adopted.Spec.PodTemplate.Spec, claim)
416568

417569
// Update uses optimistic concurrency (resourceVersion) so concurrent
418570
// claims racing to adopt the same sandbox will conflict and retry.
@@ -487,6 +639,7 @@ func (r *SandboxClaimReconciler) createSandbox(ctx context.Context, claim *exten
487639
sandbox.Annotations[v1alpha1.SandboxTemplateRefAnnotation] = template.Name
488640

489641
template.Spec.PodTemplate.DeepCopyInto(&sandbox.Spec.PodTemplate)
642+
applyClaimWorkspaceResourcesToPodSpec(&sandbox.Spec.PodTemplate.Spec, claim)
490643
// TODO: this is a workaround, remove replica assignment related issue #202
491644
replicas := int32(1)
492645
sandbox.Spec.Replicas = &replicas
@@ -566,12 +719,15 @@ func (r *SandboxClaimReconciler) getOrCreateSandbox(ctx context.Context, claim *
566719
}
567720

568721
if sandbox != nil {
569-
logger.Info("sandbox already exists, skipping update", "name", sandbox.Name)
570722
if !metav1.IsControlledBy(sandbox, claim) {
571723
err := fmt.Errorf("sandbox %q is not controlled by claim %q. Please use a different claim name or delete the sandbox manually", sandbox.Name, claim.Name)
572724
logger.Error(err, "Sandbox controller mismatch")
573725
return nil, err
574726
}
727+
// Reconcile workspace resources on the existing pod (in-place resize).
728+
if err := r.reconcileWorkspaceResources(ctx, sandbox, claim); err != nil {
729+
logger.Error(err, "failed to reconcile workspace resources")
730+
}
575731
return sandbox, nil
576732
}
577733

@@ -634,7 +790,12 @@ func (r *SandboxClaimReconciler) getOrCreateSandbox(ctx context.Context, claim *
634790

635791
// Try to adopt from warm pool
636792
if len(adoptionCandidates) > 0 {
637-
adopted, err := r.adoptSandboxFromCandidates(ctx, claim, adoptionCandidates)
793+
var template *extensionsv1alpha1.SandboxTemplate
794+
template, err := r.getTemplate(ctx, claim)
795+
if err != nil && !k8errors.IsNotFound(err) {
796+
return nil, err
797+
}
798+
adopted, err := r.adoptSandboxFromCandidates(ctx, claim, template, adoptionCandidates)
638799
if err != nil {
639800
return nil, err
640801
}

0 commit comments

Comments
 (0)