Skip to content

Commit bf4a82f

Browse files
committed
Reduce MCPServer CRD size by using runtime.RawExtension for PodTemplateSpec
The MCPServer CRD was too large (~9500 lines) to apply without server-side apply due to the embedded PodTemplateSpec taking up ~8500 lines. This was causing issues as reported in GitHub issue #2013. Changed the PodTemplateSpec field from a strongly-typed corev1.PodTemplateSpec to runtime.RawExtension, which stores the raw JSON without schema validation at the CRD level. This reduces the CRD size from ~9500 lines to 651 lines (93% reduction). The solution maintains full backwards compatibility - users can still use the same YAML structure. Validation now happens at runtime in the operator, with proper error handling via Kubernetes events and status conditions to notify users when invalid PodTemplateSpec data is provided. Key changes: - Modified MCPServer type to use runtime.RawExtension for PodTemplateSpec - Updated PodTemplateSpecBuilder to unmarshal and validate at runtime - Added event recording and status conditions for validation errors - Added comprehensive tests for invalid PodTemplateSpec scenarios - Fixed race conditions in parallel tests Fixes #2013
1 parent 417c167 commit bf4a82f

12 files changed

+577
-8523
lines changed

cmd/thv-operator/api/v1alpha1/mcpserver_types.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
package v1alpha1
22

33
import (
4-
corev1 "k8s.io/api/core/v1"
54
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
5+
"k8s.io/apimachinery/pkg/runtime"
66
)
77

88
// Condition types for MCPServer
@@ -85,8 +85,11 @@ type MCPServerSpec struct {
8585
// This allows for customizing the pod configuration beyond what is provided by the other fields.
8686
// Note that to modify the specific container the MCP server runs in, you must specify
8787
// the `mcp` container name in the PodTemplateSpec.
88+
// This field accepts a PodTemplateSpec object as JSON/YAML.
8889
// +optional
89-
PodTemplateSpec *corev1.PodTemplateSpec `json:"podTemplateSpec,omitempty"`
90+
// +kubebuilder:pruning:PreserveUnknownFields
91+
// +kubebuilder:validation:Type=object
92+
PodTemplateSpec *runtime.RawExtension `json:"podTemplateSpec,omitempty"`
9093

9194
// ResourceOverrides allows overriding annotations and labels for resources created by the operator
9295
// +optional

cmd/thv-operator/api/v1alpha1/zz_generated.deepcopy.go

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package controllers
2+
3+
import (
4+
"encoding/json"
5+
"testing"
6+
7+
"github.com/stretchr/testify/require"
8+
corev1 "k8s.io/api/core/v1"
9+
"k8s.io/apimachinery/pkg/runtime"
10+
)
11+
12+
// podTemplateSpecToRawExtension is a test helper to convert PodTemplateSpec to RawExtension
13+
func podTemplateSpecToRawExtension(t *testing.T, pts *corev1.PodTemplateSpec) *runtime.RawExtension {
14+
t.Helper()
15+
if pts == nil {
16+
return nil
17+
}
18+
raw, err := json.Marshal(pts)
19+
require.NoError(t, err, "Failed to marshal PodTemplateSpec")
20+
return &runtime.RawExtension{Raw: raw}
21+
}

cmd/thv-operator/controllers/mcpserver_controller.go

Lines changed: 104 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"k8s.io/apimachinery/pkg/types"
2727
"k8s.io/apimachinery/pkg/util/intstr"
2828
"k8s.io/client-go/rest"
29+
"k8s.io/client-go/tools/record"
2930
ctrl "sigs.k8s.io/controller-runtime"
3031
"sigs.k8s.io/controller-runtime/pkg/client"
3132
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
@@ -40,6 +41,7 @@ import (
4041
type MCPServerReconciler struct {
4142
client.Client
4243
Scheme *runtime.Scheme
44+
Recorder record.EventRecorder
4345
platformDetector kubernetes.PlatformDetector
4446
detectedPlatform kubernetes.Platform
4547
platformOnce sync.Once
@@ -295,6 +297,9 @@ func (r *MCPServerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (
295297
deployment := &appsv1.Deployment{}
296298
err = r.Get(ctx, types.NamespacedName{Name: mcpServer.Name, Namespace: mcpServer.Namespace}, deployment)
297299
if err != nil && errors.IsNotFound(err) {
300+
// Validate PodTemplateSpec and update status
301+
r.validateAndUpdatePodTemplateStatus(ctx, mcpServer)
302+
298303
// Define a new deployment
299304
dep := r.deploymentForMCPServer(ctx, mcpServer)
300305
if dep == nil {
@@ -364,6 +369,9 @@ func (r *MCPServerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (
364369

365370
// Check if the deployment spec changed
366371
if r.deploymentNeedsUpdate(ctx, deployment, mcpServer) {
372+
// Validate PodTemplateSpec and update status
373+
r.validateAndUpdatePodTemplateStatus(ctx, mcpServer)
374+
367375
// Update the deployment
368376
newDeployment := r.deploymentForMCPServer(ctx, mcpServer)
369377
deployment.Spec = newDeployment.Spec
@@ -406,6 +414,47 @@ func setImageValidationCondition(mcpServer *mcpv1alpha1.MCPServer, status metav1
406414
})
407415
}
408416

417+
// validateAndUpdatePodTemplateStatus validates the PodTemplateSpec and updates the MCPServer status
418+
// with appropriate conditions and events
419+
func (r *MCPServerReconciler) validateAndUpdatePodTemplateStatus(ctx context.Context, mcpServer *mcpv1alpha1.MCPServer) {
420+
ctxLogger := log.FromContext(ctx)
421+
422+
// Only validate if PodTemplateSpec is provided
423+
if mcpServer.Spec.PodTemplateSpec == nil || mcpServer.Spec.PodTemplateSpec.Raw == nil {
424+
return
425+
}
426+
427+
_, err := NewMCPServerPodTemplateSpecBuilder(mcpServer.Spec.PodTemplateSpec)
428+
if err != nil {
429+
// Record event for invalid PodTemplateSpec
430+
r.Recorder.Eventf(mcpServer, corev1.EventTypeWarning, "InvalidPodTemplateSpec",
431+
"Failed to parse PodTemplateSpec: %v. Deployment will continue without pod customizations.", err)
432+
433+
// Set condition for invalid PodTemplateSpec
434+
meta.SetStatusCondition(&mcpServer.Status.Conditions, metav1.Condition{
435+
Type: "PodTemplateValid",
436+
Status: metav1.ConditionFalse,
437+
ObservedGeneration: mcpServer.Generation,
438+
Reason: "InvalidPodTemplateSpec",
439+
Message: fmt.Sprintf("Failed to parse PodTemplateSpec: %v. Deployment continues without customizations.", err),
440+
})
441+
} else {
442+
// Set condition for valid PodTemplateSpec
443+
meta.SetStatusCondition(&mcpServer.Status.Conditions, metav1.Condition{
444+
Type: "PodTemplateValid",
445+
Status: metav1.ConditionTrue,
446+
ObservedGeneration: mcpServer.Generation,
447+
Reason: "ValidPodTemplateSpec",
448+
Message: "PodTemplateSpec is valid",
449+
})
450+
}
451+
452+
// Update status with the condition
453+
if statusErr := r.Status().Update(ctx, mcpServer); statusErr != nil {
454+
ctxLogger.Error(statusErr, "Failed to update MCPServer status with PodTemplateSpec validation")
455+
}
456+
}
457+
409458
// handleRestartAnnotation checks if the restart annotation has been updated and triggers a restart if needed
410459
// Returns true if a restart was triggered and the reconciliation should be requeued
411460
func (r *MCPServerReconciler) handleRestartAnnotation(ctx context.Context, mcpServer *mcpv1alpha1.MCPServer) (bool, error) {
@@ -756,17 +805,22 @@ func (r *MCPServerReconciler) deploymentForMCPServer(ctx context.Context, m *mcp
756805
args = append(args, fmt.Sprintf("--from-configmap=%s", configMapRef))
757806

758807
// Also add pod template patch for secrets (same as regular flags approach)
759-
finalPodTemplateSpec := NewMCPServerPodTemplateSpecBuilder(m.Spec.PodTemplateSpec).
760-
WithSecrets(m.Spec.Secrets).
761-
Build()
762-
// Add pod template patch if we have one
763-
if finalPodTemplateSpec != nil {
764-
podTemplatePatch, err := json.Marshal(finalPodTemplateSpec)
765-
if err != nil {
766-
ctxLogger := log.FromContext(ctx)
767-
ctxLogger.Error(err, "Failed to marshal pod template spec")
768-
} else {
769-
args = append(args, fmt.Sprintf("--k8s-pod-patch=%s", string(podTemplatePatch)))
808+
builder, err := NewMCPServerPodTemplateSpecBuilder(m.Spec.PodTemplateSpec)
809+
if err != nil {
810+
ctxLogger := log.FromContext(ctx)
811+
ctxLogger.Error(err, "Invalid PodTemplateSpec in MCPServer spec, continuing without customizations")
812+
// Continue without pod template patch - the deployment will still work
813+
} else {
814+
finalPodTemplateSpec := builder.WithSecrets(m.Spec.Secrets).Build()
815+
// Add pod template patch if we have one
816+
if finalPodTemplateSpec != nil {
817+
podTemplatePatch, err := json.Marshal(finalPodTemplateSpec)
818+
if err != nil {
819+
ctxLogger := log.FromContext(ctx)
820+
ctxLogger.Error(err, "Failed to marshal pod template spec")
821+
} else {
822+
args = append(args, fmt.Sprintf("--k8s-pod-patch=%s", string(podTemplatePatch)))
823+
}
770824
}
771825
}
772826
} else {
@@ -794,18 +848,26 @@ func (r *MCPServerReconciler) deploymentForMCPServer(ctx context.Context, m *mcp
794848
defaultSA := mcpServerServiceAccountName(m.Name)
795849
serviceAccount = &defaultSA
796850
}
797-
finalPodTemplateSpec := NewMCPServerPodTemplateSpecBuilder(m.Spec.PodTemplateSpec).
798-
WithServiceAccount(serviceAccount).
799-
WithSecrets(m.Spec.Secrets).
800-
Build()
801-
// Add pod template patch if we have one
802-
if finalPodTemplateSpec != nil {
803-
podTemplatePatch, err := json.Marshal(finalPodTemplateSpec)
804-
if err != nil {
805-
ctxLogger := log.FromContext(ctx)
806-
ctxLogger.Error(err, "Failed to marshal pod template spec")
807-
} else {
808-
args = append(args, fmt.Sprintf("--k8s-pod-patch=%s", string(podTemplatePatch)))
851+
852+
builder, err := NewMCPServerPodTemplateSpecBuilder(m.Spec.PodTemplateSpec)
853+
if err != nil {
854+
ctxLogger := log.FromContext(ctx)
855+
ctxLogger.Error(err, "Invalid PodTemplateSpec in MCPServer spec, continuing without customizations")
856+
// Continue without pod template patch - the deployment will still work
857+
} else {
858+
finalPodTemplateSpec := builder.
859+
WithServiceAccount(serviceAccount).
860+
WithSecrets(m.Spec.Secrets).
861+
Build()
862+
// Add pod template patch if we have one
863+
if finalPodTemplateSpec != nil {
864+
podTemplatePatch, err := json.Marshal(finalPodTemplateSpec)
865+
if err != nil {
866+
ctxLogger := log.FromContext(ctx)
867+
ctxLogger.Error(err, "Failed to marshal pod template spec")
868+
} else {
869+
args = append(args, fmt.Sprintf("--k8s-pod-patch=%s", string(podTemplatePatch)))
870+
}
809871
}
810872
}
811873

@@ -1379,21 +1441,19 @@ func (r *MCPServerReconciler) deploymentNeedsUpdate(
13791441
// TODO: Add more comprehensive checks for PodTemplateSpec changes beyond just the args
13801442
// This would involve comparing the actual pod template spec fields with what would be
13811443
// generated by the operator, rather than just checking the command-line arguments.
1382-
if mcpServer.Spec.PodTemplateSpec != nil {
1383-
podTemplatePatch, err := json.Marshal(mcpServer.Spec.PodTemplateSpec)
1384-
if err == nil {
1385-
podTemplatePatchArg := fmt.Sprintf("--k8s-pod-patch=%s", string(podTemplatePatch))
1386-
found := false
1387-
for _, arg := range container.Args {
1388-
if arg == podTemplatePatchArg {
1389-
found = true
1390-
break
1391-
}
1392-
}
1393-
if !found {
1394-
return true
1444+
if mcpServer.Spec.PodTemplateSpec != nil && mcpServer.Spec.PodTemplateSpec.Raw != nil {
1445+
// Use the raw bytes directly since PodTemplateSpec is now a RawExtension
1446+
podTemplatePatchArg := fmt.Sprintf("--k8s-pod-patch=%s", string(mcpServer.Spec.PodTemplateSpec.Raw))
1447+
found := false
1448+
for _, arg := range container.Args {
1449+
if arg == podTemplatePatchArg {
1450+
found = true
1451+
break
13951452
}
13961453
}
1454+
if !found {
1455+
return true
1456+
}
13971457
}
13981458

13991459
// Check if the container port has changed
@@ -1447,7 +1507,14 @@ func (r *MCPServerReconciler) deploymentNeedsUpdate(
14471507
defaultSA := mcpServerServiceAccountName(mcpServer.Name)
14481508
serviceAccount = &defaultSA
14491509
}
1450-
expectedPodTemplateSpec := NewMCPServerPodTemplateSpecBuilder(mcpServer.Spec.PodTemplateSpec).
1510+
1511+
builder, err := NewMCPServerPodTemplateSpecBuilder(mcpServer.Spec.PodTemplateSpec)
1512+
if err != nil {
1513+
// If we can't parse the PodTemplateSpec, consider it as needing update
1514+
return true
1515+
}
1516+
1517+
expectedPodTemplateSpec := builder.
14511518
WithServiceAccount(serviceAccount).
14521519
WithSecrets(mcpServer.Spec.Secrets).
14531520
Build()
@@ -1513,7 +1580,6 @@ func (r *MCPServerReconciler) deploymentNeedsUpdate(
15131580
if !equalOpenTelemetryArgs(otelConfig, container.Args) {
15141581
return true
15151582
}
1516-
15171583
}
15181584

15191585
// Check if the service account name has changed

0 commit comments

Comments
 (0)