Skip to content

feat(localmodel): replace hostPath with PVC-backed volumes in LocalModel node agent - #3

Draft
tmvfb wants to merge 13 commits into
masterfrom
feat/localmodelnodegroup-controller
Draft

feat(localmodel): replace hostPath with PVC-backed volumes in LocalModel node agent#3
tmvfb wants to merge 13 commits into
masterfrom
feat/localmodelnodegroup-controller

Conversation

@tmvfb

@tmvfb tmvfb commented Mar 8, 2026

Copy link
Copy Markdown
Member

Summary

Replace the static localmodelnode-agent DaemonSet (which used raw hostPath volumes) with a new LocalModelNodeGroup controller that dynamically creates a PV, PVC, and DaemonSet per node group with PVC-backed volumes.

  • New controller: LocalModelNodeGroupReconciler watches LocalModelNodeGroup CRs and reconciles PV + PVC + DaemonSet per node group
  • PSS-compliant: Agent containers use restricted security context (non-root, read-only rootfs, dropped capabilities, seccomp)
  • Configurable: Agent image, pull policy, and resource limits read from LocalModelConfig ConfigMap (6 new fields)
  • Node affinity: Derived from PersistentVolumeSpec.NodeAffinity — no kserve/localmodel=worker label needed
  • Cleanup: Owner references + finalizer ensure orderly deletion when LocalModelNodeGroup is removed
  • Static DaemonSet removed: Deleted from kustomize manifests, Helm charts, CI workflows, and dev scripts

Changes

New files

  • pkg/controller/v1alpha1/localmodelnodegroup/controller.go — the controller
  • pkg/controller/v1alpha1/localmodelnodegroup/suite_test.go — test suite
  • pkg/controller/v1alpha1/localmodelnodegroup/controller_test.go — envtest-based tests

Modified files

  • pkg/apis/serving/v1beta1/configmap.go — 6 new LocalModelConfig fields
  • config/configmap/inferenceservice.yaml — defaults for new fields
  • cmd/localmodel/main.go — register new reconciler
  • config/rbac/localmodel/role.yaml — DaemonSet, PV, PVC, finalizer perms
  • Makefile — RBAC generation path
  • Helm charts (kserve-resources, kserve-localmodel-resources) — values, configmap, RBAC
  • Overlays, CI workflow, dev scripts — removed static DaemonSet references

Deleted files

  • config/localmodelnodes/manager.yaml — static DaemonSet
  • config/localmodelnodes/localmodelnode_agent_image_patch.yaml
  • charts/kserve-localmodel-resources/files/daemonset-patch.yaml
  • config/overlays/test/localmodelnode_agent_image_patch.yaml

Closes #2

tmvfb added 4 commits March 8, 2026 19:28
…volumes

Implement a new controller that watches LocalModelNodeGroup CRs and
dynamically creates a PV, PVC, and DaemonSet per node group. The
DaemonSet uses PVC-backed volumes instead of raw hostPath, eliminating
Pod Security Standards (PSS) violations.

- New LocalModelNodeGroupReconciler in pkg/controller/v1alpha1/localmodelnodegroup/
- Add 6 agent config fields to LocalModelConfig (image, pull policy, resources)
- Wire up the new controller in cmd/localmodel/main.go
- PSS-compliant security context on agent containers
- Finalizer for orderly cleanup on CR deletion
- Semantic equality checks to avoid unnecessary DaemonSet updates
- Node affinity derived from PV spec (no kserve/localmodel label needed)

Closes: #2
…arts

Remove the static localmodelnode-agent DaemonSet that was deployed via
kustomize and Helm. The new LocalModelNodeGroup controller now creates
DaemonSets dynamically per node group.

- Delete config/localmodelnodes/manager.yaml (static DaemonSet)
- Delete config/localmodelnodes/localmodelnode_agent_image_patch.yaml
- Delete charts/kserve-localmodel-resources/files/daemonset-patch.yaml
- Simplify config/localmodelnodes/kustomization.yaml to only reference RBAC
- Update config/rbac/localmodel/role.yaml with DaemonSet/PV/PVC/finalizer perms
- Update Helm chart RBAC, values, and configmap templates for new agent fields
- Update Makefile RBAC generation path to include localmodelnodegroup
Remove all references to the static localmodelnode-agent DaemonSet from
test/development overlays, CI workflows, and developer scripts.

- Remove localmodelnode_agent_image_patch.yaml from test overlay
- Remove localmodelnode_image_patch.yaml reference from development overlay
- Remove 'Create model root directory' step from e2e-test.yml
- Remove 'Label worker nodes for modelcache' step from e2e-test.yml
- Remove sed line for agent patch from update-test-overlays.sh
- Remove ko resolve block for DaemonSet from hack/image_patch_dev.sh
Add Ginkgo/Gomega envtest-based tests for the new controller:
- suite_test.go: test suite setup with envtest
- controller_test.go: tests for PV/PVC/DaemonSet creation, label
  verification, PSS-compliant security context, PVC volume usage,
  node affinity propagation, finalizer lifecycle, and deletion cleanup

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Replaces the static LocalModel node agent DaemonSet (previously using hostPath) with a new LocalModelNodeGroup controller that provisions a PV + PVC + DaemonSet per node group, and wires configuration/RBAC/chart changes to support the new model.

Changes:

  • Added LocalModelNodeGroupReconciler plus envtest coverage to create PV/PVC/DaemonSet per node group.
  • Extended LocalModelConfig (ConfigMap-backed) with agent image/pull policy/resource fields and added defaults in manifests/charts.
  • Removed static localmodelnode-agent DaemonSet + related overlays/CI/dev script patching; updated RBAC and build generation.

Reviewed changes

Copilot reviewed 23 out of 23 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
test/scripts/gh-actions/update-test-overlays.sh Stops patching a now-removed static localmodel node agent image patch in GH Actions.
pkg/controller/v1alpha1/localmodelnodegroup/suite_test.go New Ginkgo envtest suite bootstrap for the LocalModelNodeGroup controller tests.
pkg/controller/v1alpha1/localmodelnodegroup/controller_test.go New envtest-based tests for PV/PVC/DaemonSet creation and finalizer behavior.
pkg/controller/v1alpha1/localmodelnodegroup/controller.go New controller that reconciles PV/PVC and a PVC-backed DaemonSet per LocalModelNodeGroup.
pkg/apis/serving/v1beta1/configmap.go Adds new LocalModel agent config fields and validates image pull policy.
hack/image_patch_dev.sh Removes localmodel node agent DaemonSet image patching; normalizes heredoc formatting.
config/rbac/localmodel/role.yaml Grants localmodel manager permissions for daemonsets and localmodelnodegroup patch/update + finalizers.
config/overlays/test/localmodelnode_agent_image_patch.yaml Deletes obsolete static DaemonSet image patch.
config/overlays/test/kustomization.yaml Removes reference to deleted localmodel node agent patch.
config/overlays/development/kustomization.yaml Removes reference to deleted localmodelnode DaemonSet patch.
config/localmodelnodes/manager.yaml Deletes the static localmodel node agent DaemonSet manifest.
config/localmodelnodes/localmodelnode_agent_image_patch.yaml Deletes obsolete DaemonSet image patch.
config/localmodelnodes/kustomization.yaml Removes static DaemonSet resource and patch wiring (leaving RBAC resource).
config/configmap/inferenceservice.yaml Adds default values for new LocalModel agent config fields.
cmd/localmodel/main.go Registers the new LocalModelNodeGroup controller with the localmodel manager.
charts/kserve-resources/values.yaml Updates Helm values: removes old scheduling/hostPath knobs; adds agent pull policy and resources.
charts/kserve-resources/files/common/configmap.yaml Adds new LocalModel agent config defaults into the chart’s configmap content.
charts/kserve-resources/files/common/configmap-patch.yaml Templates new LocalModel agent config fields from Helm values into the configmap.
charts/kserve-localmodel-resources/values.yaml Removes values for the deleted static localmodelnode DaemonSet.
charts/kserve-localmodel-resources/files/resources.yaml Removes static DaemonSet from rendered resources; extends RBAC for new controller needs.
charts/kserve-localmodel-resources/files/daemonset-patch.yaml Deletes obsolete DaemonSet patch template.
Makefile Includes localmodelnodegroup controller path in RBAC generation for the localmodel manager role.
.github/workflows/e2e-test.yml Removes node labeling and /models directory setup steps tied to the old hostPath DaemonSet.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread pkg/apis/serving/v1beta1/configmap.go
Comment thread pkg/controller/v1alpha1/localmodelnodegroup/controller.go
Comment thread pkg/controller/v1alpha1/localmodelnodegroup/controller.go
Comment thread pkg/controller/v1alpha1/localmodelnodegroup/controller.go
Comment thread pkg/controller/v1alpha1/localmodelnodegroup/controller_test.go Outdated
Comment thread pkg/controller/v1alpha1/localmodelnodegroup/controller.go Outdated
Comment thread pkg/controller/v1alpha1/localmodelnodegroup/controller.go
…up controller

- Validate agent CPU/memory resource quantities in NewLocalModelConfig
  using resource.ParseQuantity, following the GetStorageInitializerConfigs
  pattern in the same file
- Replace resource.MustParse with resource.ParseQuantity in
  createLocalModelAgentDaemonSet to avoid panics on invalid config
- Guard against nil NodeAffinity/Required before dereferencing PV spec
- Add Owns(PVC) and Owns(PV) to SetupWithManager for self-healing on
  accidental deletion, consistent with the localmodel controller
- Pass explicit timeout/interval to the initializeManager Eventually block

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 23 out of 23 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +175 to +192
// Parse resource quantities safely instead of using MustParse
cpuRequest, err := resource.ParseQuantity(localModelConfig.LocalModelAgentCpuRequest)
if err != nil {
return nil, fmt.Errorf("failed to parse localModelAgentCpuRequest %q: %w", localModelConfig.LocalModelAgentCpuRequest, err)
}
memoryRequest, err := resource.ParseQuantity(localModelConfig.LocalModelAgentMemoryRequest)
if err != nil {
return nil, fmt.Errorf("failed to parse localModelAgentMemoryRequest %q: %w", localModelConfig.LocalModelAgentMemoryRequest, err)
}
cpuLimit, err := resource.ParseQuantity(localModelConfig.LocalModelAgentCpuLimit)
if err != nil {
return nil, fmt.Errorf("failed to parse localModelAgentCpuLimit %q: %w", localModelConfig.LocalModelAgentCpuLimit, err)
}
memoryLimit, err := resource.ParseQuantity(localModelConfig.LocalModelAgentMemoryLimit)
if err != nil {
return nil, fmt.Errorf("failed to parse localModelAgentMemoryLimit %q: %w", localModelConfig.LocalModelAgentMemoryLimit, err)
}

Copilot AI Mar 8, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

createLocalModelAgentDaemonSet unconditionally parses LocalModelAgentCpuRequest/MemoryRequest/CpuLimit/MemoryLimit and uses LocalModelAgentImage from LocalModelConfig. If these fields are omitted (e.g., older inferenceservice-config during upgrade, or a custom config that didn’t add the new keys), reconciliation will fail on empty strings (quantity parse error) or produce an invalid DaemonSet (empty image), leaving node groups permanently unreconciled. Consider applying defaults in NewLocalModelConfig (matching the manifest defaults) and/or only parsing/applying resources when the fields are non-empty (with a clear error if image is empty).

Suggested change
// Parse resource quantities safely instead of using MustParse
cpuRequest, err := resource.ParseQuantity(localModelConfig.LocalModelAgentCpuRequest)
if err != nil {
return nil, fmt.Errorf("failed to parse localModelAgentCpuRequest %q: %w", localModelConfig.LocalModelAgentCpuRequest, err)
}
memoryRequest, err := resource.ParseQuantity(localModelConfig.LocalModelAgentMemoryRequest)
if err != nil {
return nil, fmt.Errorf("failed to parse localModelAgentMemoryRequest %q: %w", localModelConfig.LocalModelAgentMemoryRequest, err)
}
cpuLimit, err := resource.ParseQuantity(localModelConfig.LocalModelAgentCpuLimit)
if err != nil {
return nil, fmt.Errorf("failed to parse localModelAgentCpuLimit %q: %w", localModelConfig.LocalModelAgentCpuLimit, err)
}
memoryLimit, err := resource.ParseQuantity(localModelConfig.LocalModelAgentMemoryLimit)
if err != nil {
return nil, fmt.Errorf("failed to parse localModelAgentMemoryLimit %q: %w", localModelConfig.LocalModelAgentMemoryLimit, err)
}
// Parse resource quantities conditionally; empty strings are treated as "no value"
var (
cpuRequest resource.Quantity
memoryRequest resource.Quantity
cpuLimit resource.Quantity
memoryLimit resource.Quantity
err error
)
if localModelConfig.LocalModelAgentCpuRequest != "" {
cpuRequest, err = resource.ParseQuantity(localModelConfig.LocalModelAgentCpuRequest)
if err != nil {
return nil, fmt.Errorf("failed to parse localModelAgentCpuRequest %q: %w", localModelConfig.LocalModelAgentCpuRequest, err)
}
}
if localModelConfig.LocalModelAgentMemoryRequest != "" {
memoryRequest, err = resource.ParseQuantity(localModelConfig.LocalModelAgentMemoryRequest)
if err != nil {
return nil, fmt.Errorf("failed to parse localModelAgentMemoryRequest %q: %w", localModelConfig.LocalModelAgentMemoryRequest, err)
}
}
if localModelConfig.LocalModelAgentCpuLimit != "" {
cpuLimit, err = resource.ParseQuantity(localModelConfig.LocalModelAgentCpuLimit)
if err != nil {
return nil, fmt.Errorf("failed to parse localModelAgentCpuLimit %q: %w", localModelConfig.LocalModelAgentCpuLimit, err)
}
}
if localModelConfig.LocalModelAgentMemoryLimit != "" {
memoryLimit, err = resource.ParseQuantity(localModelConfig.LocalModelAgentMemoryLimit)
if err != nil {
return nil, fmt.Errorf("failed to parse localModelAgentMemoryLimit %q: %w", localModelConfig.LocalModelAgentMemoryLimit, err)
}
}
// Ensure a valid container image is configured
if localModelConfig.LocalModelAgentImage == "" {
return nil, fmt.Errorf("localModelAgentImage in LocalModelConfig must not be empty")
}

Copilot uses AI. Check for mistakes.
Comment on lines +372 to +398
// Step 1 - Checks if the CR is in the deletion process
if nodeGroup.DeletionTimestamp.IsZero() {
// The object is not being deleted, so if it does not have our finalizer,
// then lets add the finalizer and update the object. This is equivalent
// registering our finalizer.
if !utils.Includes(nodeGroup.GetFinalizers(), finalizerName) {
patch := client.MergeFrom(nodeGroup.DeepCopy())
nodeGroup.SetFinalizers(append(nodeGroup.GetFinalizers(), finalizerName))
if err := r.Patch(ctx, nodeGroup, patch); err != nil {
r.Log.Error(err, "Unable to patch LocalModelNodeGroup with finalizer")
return ctrl.Result{}, err
}
}
} else {
// The object is being deleted, so if it has our finalizer, then lets
// remove it and update the object. This is equivalent to unregistering
// our finalizer.
if utils.Includes(nodeGroup.GetFinalizers(), finalizerName) {
patch := client.MergeFrom(nodeGroup.DeepCopy())
nodeGroup.SetFinalizers(utils.RemoveString(nodeGroup.GetFinalizers(), finalizerName))
if err := r.Patch(ctx, nodeGroup, patch); err != nil {
r.Log.Error(err, "Unable to patch LocalModelNodeGroup without finalizer")
return ctrl.Result{}, err
}
}
return ctrl.Result{}, nil
}

Copilot AI Mar 8, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reconciler adds a finalizer on create, but on deletion it immediately removes the finalizer without performing any cleanup or ordering (no explicit deletion/waiting for PV/PVC/DaemonSet). As written, the finalizer is effectively a no-op and doesn’t provide the “orderly deletion” described in the PR summary. Either implement the intended cleanup sequence before removing the finalizer, or remove the finalizer entirely and rely on ownerReferences/GC.

Copilot uses AI. Check for mistakes.
tmvfb added 8 commits March 8, 2026 20:26
Add the 6 new localModelAgent config fields to all test ConfigMap
fixtures in localmodel/controller_test.go and localmodelnode/controller_test.go.
These fields are now validated by NewLocalModelConfig() and their
absence causes test failures.
…stale TODOs

Replace local MountPath constant with constants.DefaultModelLocalMountPath
to avoid duplication. Remove 3 stale TODO comments that were leftover
from the original upstream review.
Port unit tests from upstream PR kserve#4431 for createPV,
createPVC, createLocalModelAgentDaemonSet, and reconcileDaemonSet.
Adapted to handle our error-returning createLocalModelAgentDaemonSet
signature (uses ParseQuantity instead of MustParse). Added an extra
test case for invalid resource quantities.
Add tests for ImagePullPolicy validation (Always, IfNotPresent, Never,
invalid, empty, missing) and resource quantity validation (valid values,
invalid CPU request, invalid memory limit, empty values).
Add the 6 localModelAgent config fields to both the _example and
functional localModel sections in the test overlay ConfigMap. Tests
running against this overlay will now get a complete config that
passes NewLocalModelConfig() validation.
The controller-managed DaemonSets use app.kubernetes.io/component
labels instead of control-plane labels. Update the selector from
control-plane=kserve-localmodelnode-agent to
app.kubernetes.io/component=localmodelnode-agent so CI correctly
finds agent pods.
Add ko_resolve_localmodelnode_agent file for the development overlay
and update image_patch_dev.sh to resolve the localmodel agent image
and patch it into the localModel ConfigMap section during local
development.
…Config agent fields

Regenerate openapi_generated.go, swagger.json, and the Python SDK model
to reflect the 6 new LocalModelConfig fields: localModelAgentImage,
localModelAgentImagePullPolicy, localModelAgentCpuRequest,
localModelAgentMemoryRequest, localModelAgentCpuLimit, and
localModelAgentMemoryLimit.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Replace hostPath with PVC-backed volumes in LocalModel node agent DaemonSet

2 participants