Skip to content

Commit 1f10640

Browse files
committed
feat: kubernetes overwrites for PVC and entrypoint
Overview: Add the ability for a user to overwrite the entrypoint and PVCs in the sdk. DEP-157 Details: It is the first iteration of the feature. A more generic approach will be developed.
1 parent b8dc015 commit 1f10640

File tree

4 files changed

+128
-27
lines changed

4 files changed

+128
-27
lines changed

deploy/cloud/operator/api/v1alpha1/dynamocomponentdeployment_types.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,9 @@ type DynamoComponentDeploymentSharedSpec struct {
7979
LivenessProbe *corev1.Probe `json:"livenessProbe,omitempty"`
8080
ReadinessProbe *corev1.Probe `json:"readinessProbe,omitempty"`
8181
Replicas *int32 `json:"replicas,omitempty"`
82+
83+
// Kubernetes overwrites for the deployment
84+
KubernetesOverwrites *KubernetesOverwrites `json:"kubernetesOverwrites,omitempty"`
8285
}
8386

8487
type RunMode struct {
@@ -107,6 +110,11 @@ type IngressSpec struct {
107110
IngressControllerClassName *string `json:"ingressControllerClassName,omitempty"`
108111
}
109112

113+
type KubernetesOverwrites struct {
114+
Entrypoint *string `json:"entrypoint,omitempty"`
115+
PVCSettings map[string]*PVC `json:"pvcSettings,omitempty"`
116+
}
117+
110118
// DynamoComponentDeploymentStatus defines the observed state of DynamoComponentDeployment
111119
type DynamoComponentDeploymentStatus struct {
112120
// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster

deploy/cloud/operator/internal/controller/dynamocomponentdeployment_controller.go

Lines changed: 82 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1338,6 +1338,69 @@ func getDynamoComponentRepositoryNameAndDynamoComponentVersion(dynamoComponent *
13381338
return
13391339
}
13401340

1341+
func buildPVCVolumesAndMounts(
1342+
component *v1alpha1.DynamoComponentDeployment,
1343+
) (volumes []corev1.Volume, mounts []corev1.VolumeMount) {
1344+
used := map[string]bool{}
1345+
1346+
// addPVC adds a volume and corresponding volume mount to the pod spec based on the provided PVC configuration.
1347+
addPVC := func(volumeName string, pvc *v1alpha1.PVC) {
1348+
if pvc == nil || pvc.Name == nil {
1349+
return
1350+
}
1351+
claimName := *pvc.Name
1352+
mountPath := "/mnt/default"
1353+
if pvc.MountPoint != nil && *pvc.MountPoint != "" {
1354+
mountPath = *pvc.MountPoint
1355+
}
1356+
volume := corev1.Volume{
1357+
Name: volumeName,
1358+
VolumeSource: corev1.VolumeSource{
1359+
PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
1360+
ClaimName: claimName,
1361+
},
1362+
},
1363+
}
1364+
mount := corev1.VolumeMount{
1365+
Name: volumeName,
1366+
MountPath: mountPath,
1367+
}
1368+
if used[volumeName] {
1369+
for i, v := range volumes {
1370+
if v.Name == volumeName {
1371+
volumes[i] = volume
1372+
break
1373+
}
1374+
}
1375+
for i, m := range mounts {
1376+
if m.Name == volumeName {
1377+
mounts[i] = mount
1378+
break
1379+
}
1380+
}
1381+
} else {
1382+
volumes = append(volumes, volume)
1383+
mounts = append(mounts, mount)
1384+
used[volumeName] = true
1385+
}
1386+
}
1387+
1388+
// Handle default PVC
1389+
if component.Spec.PVC != nil {
1390+
volumeName := getPvcName(component, component.Spec.PVC.Name)
1391+
addPVC(volumeName, component.Spec.PVC)
1392+
}
1393+
1394+
// Handle overwrites
1395+
if ow := component.Spec.KubernetesOverwrites; ow != nil && ow.PVCSettings != nil {
1396+
for volumeName, pvc := range ow.PVCSettings {
1397+
addPVC(volumeName, pvc)
1398+
}
1399+
}
1400+
1401+
return volumes, mounts
1402+
}
1403+
13411404
//nolint:gocyclo,nakedret
13421405
func (r *DynamoComponentDeploymentReconciler) generatePodTemplateSpec(ctx context.Context, opt generateResourceOption) (podTemplateSpec *corev1.PodTemplateSpec, err error) {
13431406
podLabels := r.getKubeLabels(opt.dynamoComponentDeployment, opt.dynamoComponent)
@@ -1500,33 +1563,10 @@ func (r *DynamoComponentDeploymentReconciler) generatePodTemplateSpec(ctx contex
15001563
sharedMemorySizeLimit.SetMilli(memoryLimit.MilliValue() / 2)
15011564
}
15021565

1503-
volumes = append(volumes, corev1.Volume{
1504-
Name: KubeValueNameSharedMemory,
1505-
VolumeSource: corev1.VolumeSource{
1506-
EmptyDir: &corev1.EmptyDirVolumeSource{
1507-
Medium: corev1.StorageMediumMemory,
1508-
SizeLimit: &sharedMemorySizeLimit,
1509-
},
1510-
},
1511-
})
1512-
volumeMounts = append(volumeMounts, corev1.VolumeMount{
1513-
Name: KubeValueNameSharedMemory,
1514-
MountPath: "/dev/shm",
1515-
})
1516-
if opt.dynamoComponentDeployment.Spec.PVC != nil {
1517-
volumes = append(volumes, corev1.Volume{
1518-
Name: getPvcName(opt.dynamoComponentDeployment, opt.dynamoComponentDeployment.Spec.PVC.Name),
1519-
VolumeSource: corev1.VolumeSource{
1520-
PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
1521-
ClaimName: getPvcName(opt.dynamoComponentDeployment, opt.dynamoComponentDeployment.Spec.PVC.Name),
1522-
},
1523-
},
1524-
})
1525-
volumeMounts = append(volumeMounts, corev1.VolumeMount{
1526-
Name: getPvcName(opt.dynamoComponentDeployment, opt.dynamoComponentDeployment.Spec.PVC.Name),
1527-
MountPath: *opt.dynamoComponentDeployment.Spec.PVC.MountPoint,
1528-
})
1529-
}
1566+
// Handle default PVC settings. Apply overwrites if the name matches, add otherwise.
1567+
pvcVolumes, pvcMounts := buildPVCVolumesAndMounts(opt.dynamoComponentDeployment)
1568+
volumes = append(volumes, pvcVolumes...)
1569+
volumeMounts = append(volumeMounts, pvcMounts...)
15301570

15311571
imageName := opt.dynamoComponent.GetImage()
15321572
if imageName == "" {
@@ -1654,6 +1694,21 @@ func (r *DynamoComponentDeploymentReconciler) generatePodTemplateSpec(ctx contex
16541694
container.SecurityContext.RunAsUser = &[]int64{0}[0]
16551695
}
16561696

1697+
if opt.dynamoComponentDeployment.Spec.KubernetesOverwrites != nil {
1698+
overwrites := opt.dynamoComponentDeployment.Spec.KubernetesOverwrites
1699+
1700+
// Handle Entrypoint overwrite. Entrypoint needs to be renamed to Command.
1701+
if overwrites.Entrypoint != nil {
1702+
parts := strings.Fields(*overwrites.Entrypoint)
1703+
if len(parts) > 0 {
1704+
container.Command = []string{parts[0]}
1705+
if len(parts) > 1 {
1706+
container.Args = parts[1:]
1707+
}
1708+
}
1709+
}
1710+
}
1711+
16571712
containers = append(containers, container)
16581713

16591714
debuggerImage := "python:3.12-slim"

deploy/sdk/src/dynamo/sdk/cli/build.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ class ServiceInfo(BaseModel):
116116
module_path: str
117117
class_name: str
118118
config: ServiceConfig
119+
kubernetes_overwrites: t.Optional[t.Dict[str, t.Any]] = None
119120

120121
@classmethod
121122
def from_service(cls, service: ServiceInterface[T]) -> ServiceInfo:
@@ -145,12 +146,16 @@ def from_service(cls, service: ServiceInterface[T]) -> ServiceInfo:
145146
http_exposed=len(api_endpoints) > 0,
146147
api_endpoints=api_endpoints,
147148
)
149+
kubernetes_overwrites = None
150+
if hasattr(service, "kubernetes_overwrites") and service.kubernetes_overwrites:
151+
kubernetes_overwrites = service.kubernetes_overwrites.model_dump()
148152

149153
return cls(
150154
name=name,
151155
module_path=service.__module__,
152156
class_name=service_class.__name__,
153157
config=config,
158+
kubernetes_overwrites=kubernetes_overwrites,
154159
)
155160

156161

@@ -225,6 +230,13 @@ def to_dict(self) -> t.Dict[str, t.Any]:
225230
service_dict["config"]["api_endpoints"] = service["config"][
226231
"api_endpoints"
227232
]
233+
234+
# Add kubernetes overwrites if available
235+
if service.get("kubernetes_overwrites"):
236+
service_dict["config"]["kubernetes_overwrites"] = service[
237+
"kubernetes_overwrites"
238+
]
239+
228240
services_dict.append(service_dict)
229241
result["services"] = services_dict
230242
return result

deploy/sdk/src/dynamo/sdk/lib/service.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
from _bentoml_sdk.images import Image
2727
from _bentoml_sdk.service.config import validate
2828
from fastapi import FastAPI
29+
from pydantic import BaseModel
2930

3031
from dynamo.sdk.core.protocol.interface import DynamoTransport, LinkedServices
3132
from dynamo.sdk.lib.decorators import DynamoEndpoint
@@ -45,6 +46,24 @@ class ComponentType(str, Enum):
4546
# etc.
4647

4748

49+
class PVC(BaseModel):
50+
"""Class for Kubernetes PVC Overwrites."""
51+
52+
create: Optional[bool] = None
53+
name: Optional[str] = None
54+
storage_class: Optional[str] = None
55+
size: Optional[str] = None # Represent `resource.Quantity` as a string, e.g. "10Gi"
56+
volume_access_mode: Optional[str] = None # e.g., "ReadWriteOnce", "ReadOnlyMany"
57+
mount_point: Optional[str] = None
58+
59+
60+
class KubernetesOverwrites(BaseModel):
61+
"""Class for Kubernetes Overwrites."""
62+
63+
entrypoint: Optional[str] = None
64+
pvc_settings: Optional[Dict[str, PVC]] = None
65+
66+
4867
@dataclass
4968
class DynamoConfig:
5069
"""Configuration for Dynamo components"""
@@ -76,6 +95,7 @@ def __init__(
7695
envs: Optional[list[dict[str, Any]]] = None,
7796
dynamo_config: Optional[DynamoConfig] = None,
7897
app: Optional[FastAPI] = None,
98+
kubernetes_overwrites: Optional[KubernetesOverwrites] = None,
7999
):
80100
service_name = inner.__name__
81101
service_args = self._get_service_args(service_name)
@@ -140,6 +160,7 @@ def __init__(
140160
self.config["api_endpoints"] = self._api_endpoints.copy()
141161

142162
self._linked_services: List[DynamoService] = [] # Track linked services
163+
self.kubernetes_overwrites = kubernetes_overwrites
143164

144165
def _get_service_args(self, service_name: str) -> Optional[dict]:
145166
"""Get ServiceArgs from environment config if specified"""
@@ -306,6 +327,7 @@ def service(
306327
envs: Optional[list[dict[str, Any]]] = None,
307328
dynamo: Optional[Union[Dict[str, Any], DynamoConfig]] = None,
308329
app: Optional[FastAPI] = None,
330+
kubernetes_overwrites: Optional[Union[dict, KubernetesOverwrites]] = None,
309331
**kwargs: Any,
310332
) -> Any:
311333
"""Enhanced service decorator that supports Dynamo configuration
@@ -315,8 +337,11 @@ def service(
315337
- enabled: bool (default True)
316338
- name: str (default: class name)
317339
- namespace: str (default: "default")
340+
kubernetes_overwrites: kubernetes overwrites i.e. entrypoint.
318341
**kwargs: Existing BentoML service configuration
319342
"""
343+
if isinstance(kubernetes_overwrites, dict):
344+
kubernetes_overwrites = KubernetesOverwrites(**kubernetes_overwrites)
320345
config = kwargs
321346

322347
# Parse dict into DynamoConfig object
@@ -337,6 +362,7 @@ def decorator(inner: type[T]) -> DynamoService[T]:
337362
envs=envs or [],
338363
dynamo_config=dynamo_config,
339364
app=app,
365+
kubernetes_overwrites=kubernetes_overwrites,
340366
)
341367

342368
return decorator(inner) if inner is not None else decorator

0 commit comments

Comments
 (0)