Skip to content

Extend Volume type beyond HostPath-only #4548

@ChrisJBurns

Description

@ChrisJBurns

Summary

The operator defines a custom Volume type that only supports HostPath mounts. Users cannot mount ConfigMaps, Secrets, EmptyDir, or PVCs as volumes in MCP server containers. HostPath is generally discouraged in production Kubernetes due to node-affinity and security concerns.

Current custom type (cmd/thv-operator/api/v1alpha1/mcpserver_types.go, lines ~381-399)

type Volume struct {
	Name      string `json:"name"`
	HostPath  string `json:"hostPath"`
	MountPath string `json:"mountPath"`
	ReadOnly  bool   `json:"readOnly,omitempty"`
}

Controller usage (cmd/thv-operator/controllers/mcpserver_controller.go, lines ~1150-1166)

The controller hardcodes corev1.HostPathVolumeSource:

for _, v := range m.Spec.Volumes {
	volumes = append(volumes, corev1.Volume{
		Name: v.Name,
		VolumeSource: corev1.VolumeSource{
			HostPath: &corev1.HostPathVolumeSource{
				Path: v.HostPath,
			},
		},
	})
}

Why this matters

  • HostPath ties pods to specific nodes and is a security risk (host filesystem access)
  • Users cannot mount Secrets as files (common for TLS certs, credentials)
  • Users cannot mount ConfigMaps as configuration files
  • Users cannot use EmptyDir for scratch space or sidecar communication
  • Users cannot mount PVCs for persistent data
  • PodTemplateSpec can work around this but common volume patterns should be first-class

What to change

Option A (recommended): Extend the custom Volume type

Replace the single HostPath field with multiple optional source fields, enforced by a CEL mutual exclusivity rule:

// +kubebuilder:validation:XValidation:rule="(has(self.hostPath) ? 1 : 0) + (has(self.configMap) ? 1 : 0) + (has(self.secret) ? 1 : 0) + (has(self.emptyDir) ? 1 : 0) + (has(self.persistentVolumeClaim) ? 1 : 0) == 1",message="exactly one volume source must be specified"
type Volume struct {
	// Name is the name of the volume
	// +kubebuilder:validation:Required
	Name string `json:"name"`

	// MountPath is the path in the container to mount to
	// +kubebuilder:validation:Required
	MountPath string `json:"mountPath"`

	// ReadOnly specifies whether the volume should be mounted read-only
	// +kubebuilder:default=false
	// +optional
	ReadOnly bool `json:"readOnly,omitempty"`

	// HostPath mounts a path from the host node's filesystem
	// +optional
	HostPath *string `json:"hostPath,omitempty"`

	// ConfigMap mounts a ConfigMap as files
	// +optional
	ConfigMap *corev1.ConfigMapVolumeSource `json:"configMap,omitempty"`

	// Secret mounts a Secret as files
	// +optional
	Secret *corev1.SecretVolumeSource `json:"secret,omitempty"`

	// EmptyDir provides an empty directory for scratch space
	// +optional
	EmptyDir *corev1.EmptyDirVolumeSource `json:"emptyDir,omitempty"`

	// PersistentVolumeClaim mounts a PVC
	// +optional
	PersistentVolumeClaim *corev1.PersistentVolumeClaimVolumeSource `json:"persistentVolumeClaim,omitempty"`
}

Controller update (cmd/thv-operator/controllers/mcpserver_controller.go)

Replace the hardcoded HostPath conversion (~lines 1150-1166) with a switch/if-chain that creates the appropriate corev1.VolumeSource based on which field is set.

Tests

Update test files that construct Volume{Name: "x", HostPath: "/path", MountPath: "/mnt"} to use the new pointer field: Volume{Name: "x", HostPath: ptr("/path"), MountPath: "/mnt"}. Add test cases for ConfigMap and Secret volume sources.

Option B (simpler alternative)

Remove the custom Volume type entirely and direct users to PodTemplateSpec for all volume needs. Document this as the intended pattern. This is less user-friendly but avoids maintaining a custom volume abstraction.

Regenerate and verify

task gen
task crdref-gen
task lint-fix
task lint
task test

Notes

  • The HostPath field changes from string (required) to *string (optional) — this is a breaking change for existing manifests but acceptable for v1alpha1.
  • The CEL rule ensures exactly one source is set, preventing ambiguous configurations.

Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    apiItems related to the APIbreaking-changeenhancementNew feature or requestgoPull requests that update go codekubernetesItems related to Kubernetesoperator

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions