@@ -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