@@ -11,6 +11,7 @@ import (
11
11
"reflect"
12
12
"slices"
13
13
"strings"
14
+ "sync"
14
15
"time"
15
16
16
17
appsv1 "k8s.io/api/apps/v1"
@@ -22,20 +23,24 @@ import (
22
23
"k8s.io/apimachinery/pkg/runtime"
23
24
"k8s.io/apimachinery/pkg/types"
24
25
"k8s.io/apimachinery/pkg/util/intstr"
25
- "k8s.io/utils/ptr "
26
+ "k8s.io/client-go/rest "
26
27
ctrl "sigs.k8s.io/controller-runtime"
27
28
"sigs.k8s.io/controller-runtime/pkg/client"
28
29
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
29
30
"sigs.k8s.io/controller-runtime/pkg/log"
30
31
31
32
mcpv1alpha1 "github.com/stacklok/toolhive/cmd/thv-operator/api/v1alpha1"
33
+ "github.com/stacklok/toolhive/pkg/container/kubernetes"
32
34
"github.com/stacklok/toolhive/pkg/logger"
33
35
)
34
36
35
37
// MCPServerReconciler reconciles a MCPServer object
36
38
type MCPServerReconciler struct {
37
39
client.Client
38
- Scheme * runtime.Scheme
40
+ Scheme * runtime.Scheme
41
+ platformDetector kubernetes.PlatformDetector
42
+ detectedPlatform kubernetes.Platform
43
+ platformOnce sync.Once
39
44
}
40
45
41
46
// defaultRBACRules are the default RBAC rules that the
@@ -82,6 +87,35 @@ const (
82
87
authzLabelValueInline = "inline"
83
88
)
84
89
90
+ // detectPlatform detects the Kubernetes platform type (Kubernetes vs OpenShift)
91
+ // It uses sync.Once to ensure the detection is only performed once and cached
92
+ func (r * MCPServerReconciler ) detectPlatform (ctx context.Context ) (kubernetes.Platform , error ) {
93
+ var err error
94
+ r .platformOnce .Do (func () {
95
+ // Initialize platform detector if not already done
96
+ if r .platformDetector == nil {
97
+ r .platformDetector = kubernetes .NewDefaultPlatformDetector ()
98
+ }
99
+
100
+ cfg , configErr := rest .InClusterConfig ()
101
+ if configErr != nil {
102
+ err = fmt .Errorf ("failed to get in-cluster config for platform detection: %w" , configErr )
103
+ return
104
+ }
105
+
106
+ r .detectedPlatform , err = r .platformDetector .DetectPlatform (cfg )
107
+ if err != nil {
108
+ err = fmt .Errorf ("failed to detect platform: %w" , err )
109
+ return
110
+ }
111
+
112
+ ctxLogger := log .FromContext (ctx )
113
+ ctxLogger .Info ("Platform detected for MCPServer controller" , "platform" , r .detectedPlatform .String ())
114
+ })
115
+
116
+ return r .detectedPlatform , err
117
+ }
118
+
85
119
// Reconcile is part of the main kubernetes reconciliation loop which aims to
86
120
// move the current state of the cluster closer to the desired state.
87
121
//
@@ -156,7 +190,7 @@ func (r *MCPServerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (
156
190
err = r .Get (ctx , types.NamespacedName {Name : mcpServer .Name , Namespace : mcpServer .Namespace }, deployment )
157
191
if err != nil && errors .IsNotFound (err ) {
158
192
// Define a new deployment
159
- dep := r .deploymentForMCPServer (mcpServer )
193
+ dep := r .deploymentForMCPServer (ctx , mcpServer )
160
194
if dep == nil {
161
195
ctxLogger .Error (nil , "Failed to create Deployment object" )
162
196
return ctrl.Result {}, fmt .Errorf ("failed to create Deployment object" )
@@ -225,7 +259,7 @@ func (r *MCPServerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (
225
259
// Check if the deployment spec changed
226
260
if deploymentNeedsUpdate (deployment , mcpServer ) {
227
261
// Update the deployment
228
- newDeployment := r .deploymentForMCPServer (mcpServer )
262
+ newDeployment := r .deploymentForMCPServer (ctx , mcpServer )
229
263
deployment .Spec = newDeployment .Spec
230
264
err = r .Update (ctx , deployment )
231
265
if err != nil {
@@ -401,7 +435,7 @@ func (r *MCPServerReconciler) ensureRBACResources(ctx context.Context, mcpServer
401
435
// deploymentForMCPServer returns a MCPServer Deployment object
402
436
//
403
437
//nolint:gocyclo
404
- func (r * MCPServerReconciler ) deploymentForMCPServer (m * mcpv1alpha1.MCPServer ) * appsv1.Deployment {
438
+ func (r * MCPServerReconciler ) deploymentForMCPServer (ctx context. Context , m * mcpv1alpha1.MCPServer ) * appsv1.Deployment {
405
439
ls := labelsForMCPServer (m .Name )
406
440
replicas := int32 (1 )
407
441
@@ -581,22 +615,17 @@ func (r *MCPServerReconciler) deploymentForMCPServer(m *mcpv1alpha1.MCPServer) *
581
615
}
582
616
}
583
617
584
- // Prepare ProxyRunner's pod and container security context
585
- proxyRunnerPodSecurityContext := & corev1.PodSecurityContext {
586
- RunAsNonRoot : ptr .To (true ),
587
- RunAsUser : ptr .To (int64 (1000 )),
588
- RunAsGroup : ptr .To (int64 (1000 )),
589
- FSGroup : ptr .To (int64 (1000 )),
618
+ // Detect platform and prepare ProxyRunner's pod and container security context
619
+ _ , err := r .detectPlatform (ctx )
620
+ if err != nil {
621
+ ctxLogger := log .FromContext (ctx )
622
+ ctxLogger .Error (err , "Failed to detect platform, defaulting to Kubernetes" , "mcpserver" , m .Name )
590
623
}
591
624
592
- proxyRunnerContainerSecurityContext := & corev1.SecurityContext {
593
- Privileged : ptr .To (false ),
594
- RunAsNonRoot : ptr .To (true ),
595
- RunAsUser : ptr .To (int64 (1000 )),
596
- RunAsGroup : ptr .To (int64 (1000 )),
597
- AllowPrivilegeEscalation : ptr .To (false ),
598
- ReadOnlyRootFilesystem : ptr .To (true ),
599
- }
625
+ // Use SecurityContextBuilder for platform-aware security context
626
+ securityBuilder := kubernetes .NewSecurityContextBuilder (r .detectedPlatform )
627
+ proxyRunnerPodSecurityContext := securityBuilder .BuildPodSecurityContext ()
628
+ proxyRunnerContainerSecurityContext := securityBuilder .BuildContainerSecurityContext ()
600
629
601
630
env = ensureRequiredEnvVars (env )
602
631
0 commit comments