mirror of
https://github.com/hanzo-ml/kserve.git
synced 2026-10-30 20:53:49 +00:00
585 lines
21 KiB
Go
585 lines
21 KiB
Go
/*
|
|
Copyright 2025 The KServe Authors.
|
|
|
|
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 utils
|
|
|
|
import (
|
|
"fmt"
|
|
"path"
|
|
"strings"
|
|
|
|
corev1 "k8s.io/api/core/v1"
|
|
"k8s.io/apimachinery/pkg/api/resource"
|
|
"k8s.io/utils/ptr"
|
|
|
|
"github.com/kserve/kserve/pkg/constants"
|
|
"github.com/kserve/kserve/pkg/types"
|
|
)
|
|
|
|
type StorageMountParams struct {
|
|
MountPath string
|
|
SubPath string
|
|
VolumeName string
|
|
PVCName string
|
|
ReadOnly bool
|
|
}
|
|
|
|
// ParsePvcURI parses a PVC URI of the form "pvc://<name>[/path]" into its components.
|
|
//
|
|
// Parameters:
|
|
// - srcURI: The source URI string, which must begin with the "pvc://" prefix.
|
|
//
|
|
// Returns:
|
|
// - pvcName: The name of the PVC (<name> part).
|
|
// - pvcPath: The optional <path> component. If not provided, this will be an empty string.
|
|
// - err: An error if strings.Split would return zero.
|
|
//
|
|
// The function expects the input to follow the "pvc://<name>[/path]" format, however the pvc:// prefix is not validated.
|
|
//
|
|
// Examples:
|
|
//
|
|
// "pvc://myclaim" => pvcName: "myclaim", pvcPath: "", err: nil
|
|
// "pvc://myclaim/models" => pvcName: "myclaim", pvcPath: "models", err: nil
|
|
// "pvc://myclaim/models/v1" => pvcName: "myclaim", pvcPath: "models/v1", err: nil
|
|
// "s3://bucket/path" => pvcName: "s3:", pvcPath: "/bucket/path", err: nil
|
|
// "" (empty string) => pvcName: "", pvcPath: "", err: nil
|
|
func ParsePvcURI(srcURI string) (pvcName string, pvcPath string, err error) {
|
|
parts := strings.Split(strings.TrimPrefix(srcURI, constants.PvcURIPrefix), "/")
|
|
switch len(parts) {
|
|
case 0:
|
|
return "", "", fmt.Errorf("invalid URI must be pvc://<pvcname>/[path]: %s", srcURI)
|
|
case 1:
|
|
pvcName = parts[0]
|
|
pvcPath = ""
|
|
default:
|
|
pvcName = parts[0]
|
|
pvcPath = strings.Join(parts[1:], "/")
|
|
}
|
|
|
|
return pvcName, pvcPath, nil
|
|
}
|
|
|
|
// addVolumeMountToContainer adds a volume mount to a specific container
|
|
func addVolumeMountToContainer(container *corev1.Container, storageMountParams StorageMountParams) bool {
|
|
// Check if mount already exists
|
|
for _, mount := range container.VolumeMounts {
|
|
if mount.Name == storageMountParams.VolumeName {
|
|
return false // Mount already exists
|
|
}
|
|
}
|
|
|
|
// Add the volume mount
|
|
sourceVolumeMount := corev1.VolumeMount{
|
|
Name: storageMountParams.VolumeName,
|
|
MountPath: storageMountParams.MountPath,
|
|
SubPath: storageMountParams.SubPath,
|
|
ReadOnly: storageMountParams.ReadOnly,
|
|
}
|
|
|
|
container.VolumeMounts = append(container.VolumeMounts, sourceVolumeMount)
|
|
return true
|
|
}
|
|
|
|
// AddModelMount adds a mount to the specified container in the given PodSpec based on the provided modelUri.
|
|
// If the mount or volume already exists, it will not be duplicated.
|
|
//
|
|
// Parameters:
|
|
// - modelUri: The URI specifying the PVC and optional sub-path to mount.
|
|
// - containerName: The name of the container within the PodSpec to which the model should be mounted.
|
|
// - readOnly: Whether the mount should be read-only.
|
|
// - podSpec: PodSpec to modify.
|
|
//
|
|
// Returns:
|
|
// - error: An error if the modelUri is invalid or if any other issue occurs; otherwise, nil.
|
|
func AddModelMount(storageMountParams StorageMountParams, containerName string, podSpec *corev1.PodSpec) error {
|
|
var volumeSource corev1.VolumeSource
|
|
|
|
if storageMountParams.PVCName != "" {
|
|
volumeSource = corev1.VolumeSource{
|
|
PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
|
|
ClaimName: storageMountParams.PVCName,
|
|
},
|
|
}
|
|
} else {
|
|
volumeSource = corev1.VolumeSource{
|
|
EmptyDir: &corev1.EmptyDirVolumeSource{},
|
|
}
|
|
}
|
|
|
|
mountAdded := false
|
|
|
|
// Check regular containers first
|
|
for idx := range podSpec.Containers {
|
|
if podSpec.Containers[idx].Name == containerName {
|
|
mountAdded = addVolumeMountToContainer(&podSpec.Containers[idx], storageMountParams)
|
|
break
|
|
}
|
|
}
|
|
|
|
// If not found in regular containers, check init containers
|
|
if !mountAdded {
|
|
for idx := range podSpec.InitContainers {
|
|
if podSpec.InitContainers[idx].Name == containerName {
|
|
storageMountParams.ReadOnly = false // init containers need to write to the mount
|
|
mountAdded = addVolumeMountToContainer(&podSpec.InitContainers[idx], storageMountParams)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if mountAdded {
|
|
// add the volume on the pod
|
|
volumeExists := false
|
|
for _, volume := range podSpec.Volumes {
|
|
if volume.Name == storageMountParams.VolumeName {
|
|
volumeExists = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !volumeExists {
|
|
modelVolume := corev1.Volume{
|
|
Name: storageMountParams.VolumeName,
|
|
VolumeSource: volumeSource,
|
|
}
|
|
podSpec.Volumes = append(podSpec.Volumes, modelVolume)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// AddEmptyDirVolumeIfNotPresent adds an emptyDir volume only if not present in the
|
|
// list. pod and pod.Spec must not be nil
|
|
func AddEmptyDirVolumeIfNotPresent(podSpec *corev1.PodSpec, name string) {
|
|
for _, v := range podSpec.Volumes {
|
|
if v.Name == name {
|
|
return
|
|
}
|
|
}
|
|
podSpec.Volumes = append(podSpec.Volumes, corev1.Volume{
|
|
Name: name,
|
|
VolumeSource: corev1.VolumeSource{
|
|
EmptyDir: &corev1.EmptyDirVolumeSource{},
|
|
},
|
|
})
|
|
}
|
|
|
|
// findCommonParentPath finds the common parent directory of multiple paths
|
|
func FindCommonParentPath(paths []string) string {
|
|
if len(paths) == 0 {
|
|
return ""
|
|
}
|
|
|
|
if len(paths) == 1 {
|
|
return paths[0]
|
|
}
|
|
|
|
// Split all paths into components
|
|
pathComponents := make([][]string, len(paths))
|
|
minLength := -1
|
|
for i, path := range paths {
|
|
// Clean the path and split by "/"
|
|
cleanPath := strings.Trim(path, "/")
|
|
if cleanPath == "" {
|
|
pathComponents[i] = []string{}
|
|
} else {
|
|
pathComponents[i] = strings.Split(cleanPath, "/")
|
|
|
|
if minLength == -1 {
|
|
minLength = len(pathComponents[i])
|
|
} else {
|
|
minLength = min(minLength, len(pathComponents[i]))
|
|
}
|
|
}
|
|
}
|
|
|
|
// Find common prefix
|
|
var commonComponents []string
|
|
for i := range minLength {
|
|
levelComponents := make(map[string]struct{})
|
|
var pathComponent string
|
|
|
|
for _, components := range pathComponents {
|
|
pathComponent = components[i]
|
|
levelComponents[pathComponent] = struct{}{}
|
|
}
|
|
|
|
if len(levelComponents) == 1 {
|
|
commonComponents = append(commonComponents, pathComponent)
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
|
|
if len(commonComponents) == 0 {
|
|
return "/"
|
|
}
|
|
|
|
return "/" + strings.Join(commonComponents, "/")
|
|
}
|
|
|
|
// Helper function to generate volume name from path
|
|
func GetVolumeNameFromPath(path string) string {
|
|
// Convert path to valid volume name (remove slashes, etc.)
|
|
return strings.ReplaceAll(strings.Trim(path, "/"), "/", "-")
|
|
}
|
|
|
|
// AddDefaultHuggingFaceEnvVars adds default HuggingFace optimization environment variables
|
|
// to the container if they are not already present. This prevents conflicts when merging
|
|
// container specs where users may have defined these variables with different configurations.
|
|
func AddDefaultHuggingFaceEnvVars(container *corev1.Container) {
|
|
defaultHFEnvVars := []corev1.EnvVar{
|
|
{
|
|
Name: "HF_HUB_ENABLE_HF_TRANSFER",
|
|
Value: "1",
|
|
},
|
|
{
|
|
Name: "HF_XET_HIGH_PERFORMANCE",
|
|
Value: "1",
|
|
},
|
|
{
|
|
Name: "HF_XET_NUM_CONCURRENT_RANGE_GETS",
|
|
Value: "8",
|
|
},
|
|
}
|
|
|
|
AddEnvVars(container, defaultHFEnvVars)
|
|
}
|
|
|
|
func AddEnvVars(container *corev1.Container, vars []corev1.EnvVar) {
|
|
for _, envVar := range vars {
|
|
// Check if the environment variable already exists
|
|
exists := false
|
|
for _, existingEnvVar := range container.Env {
|
|
if existingEnvVar.Name == envVar.Name {
|
|
exists = true
|
|
break
|
|
}
|
|
}
|
|
// Only add if it doesn't already exist
|
|
if !exists {
|
|
container.Env = append(container.Env, envVar)
|
|
}
|
|
}
|
|
}
|
|
|
|
func GetStorageResources(storageURIs []string, storagePaths []string) ([]corev1.VolumeMount, []corev1.Volume, []string, error) {
|
|
initContainerArgs := make([]string, 0, len(storageURIs)*2)
|
|
volumeMounts := make([]corev1.VolumeMount, 0, 1)
|
|
volumes := make([]corev1.Volume, 0, 1)
|
|
mountPaths := make([]string, 0, len(storageURIs))
|
|
|
|
for i := range storageURIs {
|
|
initContainerArgs = append(initContainerArgs, storageURIs[i], storagePaths[i])
|
|
mountPaths = append(mountPaths, storagePaths[i])
|
|
}
|
|
|
|
mountPath := FindCommonParentPath(mountPaths)
|
|
|
|
volumeName := GetVolumeNameFromPath(mountPath)
|
|
|
|
volumeMounts = append(volumeMounts, corev1.VolumeMount{
|
|
Name: volumeName,
|
|
MountPath: mountPath,
|
|
ReadOnly: false,
|
|
})
|
|
|
|
volumes = append(volumes, corev1.Volume{
|
|
Name: volumeName,
|
|
VolumeSource: corev1.VolumeSource{
|
|
EmptyDir: &corev1.EmptyDirVolumeSource{},
|
|
},
|
|
})
|
|
|
|
return volumeMounts, volumes, initContainerArgs, nil
|
|
}
|
|
|
|
func CreateInitContainerWithConfig(storageConfig *types.StorageInitializerConfig, containerArgs []string) *corev1.Container {
|
|
storageInitializerImage := constants.StorageInitializerContainerImage + ":" + constants.StorageInitializerContainerImageVersion
|
|
|
|
if storageConfig.Image != "" {
|
|
storageInitializerImage = storageConfig.Image
|
|
}
|
|
|
|
return &corev1.Container{
|
|
Name: constants.StorageInitializerContainerName,
|
|
Image: storageInitializerImage,
|
|
Args: containerArgs,
|
|
TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError,
|
|
Resources: corev1.ResourceRequirements{
|
|
Limits: map[corev1.ResourceName]resource.Quantity{
|
|
corev1.ResourceCPU: resource.MustParse(storageConfig.CpuLimit),
|
|
corev1.ResourceMemory: resource.MustParse(storageConfig.MemoryLimit),
|
|
},
|
|
Requests: map[corev1.ResourceName]resource.Quantity{
|
|
corev1.ResourceCPU: resource.MustParse(storageConfig.CpuRequest),
|
|
corev1.ResourceMemory: resource.MustParse(storageConfig.MemoryRequest),
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// shellQuote wraps s in single quotes for safe interpolation into a sh -c
|
|
// command string. Any embedded single quotes are escaped using the standard
|
|
// POSIX sequence: close the current quote, emit an escaped literal quote,
|
|
// and re-open a new quoted segment ('\\"). This is safe for all byte values.
|
|
func shellQuote(s string) string {
|
|
return "'" + strings.ReplaceAll(s, "'", "'\\''") + "'"
|
|
}
|
|
|
|
// modelcarCommand returns the shell command for the modelcar container.
|
|
// When modelPath differs from the default, it prepends "mkdir -p" to create
|
|
// the parent directory so that the symlink target exists. For the default path
|
|
// the command is kept identical to the original to avoid unnecessary pod restarts
|
|
// on upgrade.
|
|
//
|
|
// Paths are shell-quoted using shellQuote to prevent metacharacter injection.
|
|
// The upstream validation (validateStorageURISpec) ensures paths are absolute
|
|
// and contain no "..", but quoting provides defense-in-depth.
|
|
func modelcarCommand(modelPath string) string {
|
|
// $$$$ gets escaped by YAML to $$, which is the current PID
|
|
if modelPath != constants.DefaultModelLocalMountPath {
|
|
return fmt.Sprintf("mkdir -p %s && ln -sf /proc/$$$$/root/models %s && sleep infinity",
|
|
shellQuote(path.Dir(modelPath)), shellQuote(modelPath))
|
|
}
|
|
return fmt.Sprintf("ln -sf /proc/$$$$/root/models %s && sleep infinity", shellQuote(modelPath))
|
|
}
|
|
|
|
// CreateModelcarContainer creates the definition of a container holding a model intended to be used as a sidecar (modelcar).
|
|
// The container is configured with CPU, memory, and UID settings from the storage initializer configuration.
|
|
//
|
|
// Parameters:
|
|
// - containerName: The name to assign to the modelcar container.
|
|
// - image: The container image to use for the modelcar.
|
|
// - modelPath: The path where the model should be mounted inside the container.
|
|
// - volumeName: The name of the shared volume for model data.
|
|
// - storageConfig: The storage initializer configuration.
|
|
//
|
|
// Returns:
|
|
// - *corev1.Container: The modelcar container definition.
|
|
func CreateModelcarContainer(containerName string, image string, modelPath string, volumeName string, storageConfig *types.StorageInitializerConfig) *corev1.Container {
|
|
cpu := storageConfig.CpuModelcar
|
|
if cpu == "" {
|
|
cpu = constants.CpuModelcarDefault
|
|
}
|
|
memory := storageConfig.MemoryModelcar
|
|
if memory == "" {
|
|
memory = constants.MemoryModelcarDefault
|
|
}
|
|
|
|
modelContainer := &corev1.Container{
|
|
Name: containerName,
|
|
Image: image,
|
|
VolumeMounts: []corev1.VolumeMount{
|
|
{
|
|
Name: volumeName,
|
|
MountPath: GetParentDirectory(modelPath),
|
|
ReadOnly: false,
|
|
},
|
|
},
|
|
Args: []string{
|
|
"sh",
|
|
"-c",
|
|
modelcarCommand(modelPath),
|
|
},
|
|
Resources: corev1.ResourceRequirements{
|
|
Limits: map[corev1.ResourceName]resource.Quantity{
|
|
// Could possibly be reduced to even less
|
|
corev1.ResourceCPU: resource.MustParse(cpu),
|
|
corev1.ResourceMemory: resource.MustParse(memory),
|
|
},
|
|
Requests: map[corev1.ResourceName]resource.Quantity{
|
|
corev1.ResourceCPU: resource.MustParse(cpu),
|
|
corev1.ResourceMemory: resource.MustParse(memory),
|
|
},
|
|
},
|
|
TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError,
|
|
}
|
|
|
|
if storageConfig.UidModelcar != nil {
|
|
modelContainer.SecurityContext = &corev1.SecurityContext{
|
|
RunAsUser: storageConfig.UidModelcar,
|
|
}
|
|
}
|
|
|
|
return modelContainer
|
|
}
|
|
|
|
// CreateModelcarInitContainer is similar to CreateModelcarContainer but returns an init container definition.
|
|
// This init container is intended to run before the main containers to pre-fetch and validate the modelcar image.
|
|
//
|
|
// Parameters:
|
|
// - containerName: The name to assign to the modelcar init container.
|
|
// - image: The container image to use for the modelcar init container.
|
|
// - storageConfig: The storage initializer configuration.
|
|
//
|
|
// Returns:
|
|
// - *corev1.Container: The modelcar init container definition.
|
|
func CreateModelcarInitContainer(containerName string, image string, storageConfig *types.StorageInitializerConfig) *corev1.Container {
|
|
cpu := storageConfig.CpuModelcar
|
|
if cpu == "" {
|
|
cpu = constants.CpuModelcarDefault
|
|
}
|
|
memory := storageConfig.MemoryModelcar
|
|
if memory == "" {
|
|
memory = constants.MemoryModelcarDefault
|
|
}
|
|
|
|
modelContainer := &corev1.Container{
|
|
Name: containerName,
|
|
Image: image,
|
|
Args: []string{
|
|
"sh",
|
|
"-c",
|
|
// Check that the expected models directory exists
|
|
"echo 'Pre-fetching modelcar " + image + ": ' && [ -d /models ] && [ \"$$(ls -A /models)\" ] && echo 'OK ... Prefetched and valid (/models exists)' || (echo 'NOK ... Prefetched but modelcar is invalid (/models does not exist or is empty)' && exit 1)",
|
|
},
|
|
Resources: corev1.ResourceRequirements{
|
|
Limits: map[corev1.ResourceName]resource.Quantity{
|
|
// Could possibly be reduced to even less
|
|
corev1.ResourceCPU: resource.MustParse(cpu),
|
|
corev1.ResourceMemory: resource.MustParse(memory),
|
|
},
|
|
Requests: map[corev1.ResourceName]resource.Quantity{
|
|
corev1.ResourceCPU: resource.MustParse(cpu),
|
|
corev1.ResourceMemory: resource.MustParse(memory),
|
|
},
|
|
},
|
|
TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError,
|
|
}
|
|
|
|
return modelContainer
|
|
}
|
|
|
|
// MaxOCISourcesPerPod is the maximum number of OCI modelcar sidecars that can be
|
|
// injected into a single pod. This prevents resource exhaustion from unbounded
|
|
// sidecar injection — each OCI URI adds 2 containers (sidecar + init) and 1 volume.
|
|
const MaxOCISourcesPerPod = 10
|
|
|
|
// ModelcarNames generates unique container and volume names for a modelcar at the given OCI index.
|
|
// When ociIndex is 0, the original constant names are returned for backward compatibility.
|
|
func ModelcarNames(ociIndex int) (sidecarName, initName, volumeName string) {
|
|
if ociIndex == 0 {
|
|
return constants.ModelcarContainerName, constants.ModelcarInitContainerName, constants.StorageInitializerVolumeName
|
|
}
|
|
suffix := fmt.Sprintf("-%d", ociIndex)
|
|
return constants.ModelcarContainerName + suffix,
|
|
constants.ModelcarInitContainerName + suffix,
|
|
constants.StorageInitializerVolumeName + suffix
|
|
}
|
|
|
|
// ValidateOCIMountPaths checks that the given OCI mount paths will not cause volume
|
|
// mount shadowing when injected. Each modelcar sidecar mounts an emptyDir at the
|
|
// *parent directory* of its modelPath (via GetParentDirectory). If two OCI URIs share
|
|
// the same parent directory, their volumes would be mounted at the same path on the
|
|
// target container, causing the last mount to shadow all previous ones.
|
|
//
|
|
// Returns an error if a collision is detected.
|
|
func ValidateOCIMountPaths(mountPaths []string) error {
|
|
if len(mountPaths) <= 1 {
|
|
return nil
|
|
}
|
|
parentDirSeen := make(map[string]string, len(mountPaths)) // parentDir -> first modelPath that used it
|
|
for _, mp := range mountPaths {
|
|
parentDir := GetParentDirectory(mp)
|
|
if prev, exists := parentDirSeen[parentDir]; exists {
|
|
return fmt.Errorf(
|
|
"OCI mount paths %q and %q share parent directory %q, which would cause volume mount shadowing; "+
|
|
"use mount paths with distinct parent directories",
|
|
prev, mp, parentDir,
|
|
)
|
|
}
|
|
parentDirSeen[parentDir] = mp
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ConfigureModelcarToContainer configures the OCI image specified in modelUri as a modelcar to the
|
|
// specified target container of a given PodSpec. The configuration includes:
|
|
// - Adding an environment variable `async` to indicate to the runtime that the model directory may not be available immediately.
|
|
// - Setting the user ID for the target container, if specified in storageConfig.
|
|
// - Adding a modelcar and init containers (for pre-fetching the model) if not already present.
|
|
// - Mounting a volume to the target container to access the model directory (via a shared volume).
|
|
// - Enabling process namespace sharing (because of the shared volume).
|
|
//
|
|
// Parameters:
|
|
// - modelUri: The URI specifying the model image location.
|
|
// - podSpec: The PodSpec to modify.
|
|
// - targetContainerName: The name of the container to configure the modelcar for.
|
|
// - modelPath: The path where the model symlink should be created inside the container
|
|
// (e.g. /mnt/models or /mnt/models/my-llama for a model-name subdirectory).
|
|
// - storageConfig: The storage initializer configuration.
|
|
// - ociIndex: The index of this OCI URI within the storageUris list. Used to generate
|
|
// unique container/volume names when multiple OCI URIs are specified. Use 0 for
|
|
// single-URI or legacy scenarios to preserve backward compatibility.
|
|
//
|
|
// Returns:
|
|
// - error: An error if the target container is not found or if configuration fails; otherwise, nil.
|
|
func ConfigureModelcarToContainer(modelUri string, podSpec *corev1.PodSpec, targetContainerName string, modelPath string, storageConfig *types.StorageInitializerConfig, ociIndex int) error {
|
|
targetContainer := GetContainerWithName(podSpec, targetContainerName)
|
|
if targetContainer == nil {
|
|
return fmt.Errorf("no container found with name %s", targetContainerName)
|
|
}
|
|
|
|
sidecarName, initName, volumeName := ModelcarNames(ociIndex)
|
|
|
|
// Indicate to the runtime that it the model directory could be
|
|
// available a bit later only so that it should wait and retry when
|
|
// starting up
|
|
AddOrReplaceEnv(targetContainer, constants.ModelInitModeEnvVarKey, "async")
|
|
|
|
// Mount volume initialized by the modelcar container to the target container.
|
|
// Each OCI URI gets its own emptyDir volume because the modelcar sidecar creates
|
|
// a symlink (via /proc/<PID>/root) that is specific to its container image.
|
|
// Sharing a single volume between multiple modelcar sidecars would cause symlink
|
|
// conflicts.
|
|
modelParentDir := GetParentDirectory(modelPath)
|
|
AddEmptyDirVolumeIfNotPresent(podSpec, volumeName)
|
|
AddVolumeMountIfNotPresent(targetContainer, volumeName, modelParentDir, false)
|
|
|
|
// If configured, run as the given user. There might be certain installations
|
|
// of Kubernetes where sharing the filesystem via the process namespace only works
|
|
// when both containers are running as root
|
|
if storageConfig.UidModelcar != nil {
|
|
if targetContainer.SecurityContext == nil {
|
|
targetContainer.SecurityContext = &corev1.SecurityContext{}
|
|
}
|
|
targetContainer.SecurityContext.RunAsUser = storageConfig.UidModelcar
|
|
}
|
|
|
|
// Create the modelcar that is used as a sidecar in Pod and add it to the end
|
|
// of the containers (but only if not already have been added)
|
|
if GetContainerWithName(podSpec, sidecarName) == nil {
|
|
// Extract image reference for modelcar from URI
|
|
image := strings.TrimPrefix(modelUri, constants.OciURIPrefix)
|
|
|
|
modelContainer := CreateModelcarContainer(sidecarName, image, modelPath, volumeName, storageConfig)
|
|
podSpec.Containers = append(podSpec.Containers, *modelContainer)
|
|
|
|
// Add the model container as an init-container to pre-fetch the model before
|
|
// the runtimes starts.
|
|
modelInitContainer := CreateModelcarInitContainer(initName, image, storageConfig)
|
|
podSpec.InitContainers = append(podSpec.InitContainers, *modelInitContainer)
|
|
}
|
|
|
|
// Enable process namespace sharing so that the modelcar's root filesystem
|
|
// can be reached by the user container
|
|
podSpec.ShareProcessNamespace = ptr.To(true)
|
|
|
|
return nil
|
|
}
|