@@ -11,6 +11,7 @@ import (
1111 "reflect"
1212 "slices"
1313 "strings"
14+ "sync"
1415 "time"
1516
1617 appsv1 "k8s.io/api/apps/v1"
@@ -22,20 +23,24 @@ import (
2223 "k8s.io/apimachinery/pkg/runtime"
2324 "k8s.io/apimachinery/pkg/types"
2425 "k8s.io/apimachinery/pkg/util/intstr"
25- "k8s.io/utils/ptr "
26+ "k8s.io/client-go/rest "
2627 ctrl "sigs.k8s.io/controller-runtime"
2728 "sigs.k8s.io/controller-runtime/pkg/client"
2829 "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
2930 "sigs.k8s.io/controller-runtime/pkg/log"
3031
3132 mcpv1alpha1 "github.com/stacklok/toolhive/cmd/thv-operator/api/v1alpha1"
33+ "github.com/stacklok/toolhive/pkg/container/kubernetes"
3234 "github.com/stacklok/toolhive/pkg/logger"
3335)
3436
3537// MCPServerReconciler reconciles a MCPServer object
3638type MCPServerReconciler struct {
3739 client.Client
38- Scheme * runtime.Scheme
40+ Scheme * runtime.Scheme
41+ platformDetector kubernetes.PlatformDetector
42+ detectedPlatform kubernetes.Platform
43+ platformOnce sync.Once
3944}
4045
4146// defaultRBACRules are the default RBAC rules that the
@@ -82,6 +87,35 @@ const (
8287 authzLabelValueInline = "inline"
8388)
8489
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+
85119// Reconcile is part of the main kubernetes reconciliation loop which aims to
86120// move the current state of the cluster closer to the desired state.
87121//
@@ -156,7 +190,7 @@ func (r *MCPServerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (
156190 err = r .Get (ctx , types.NamespacedName {Name : mcpServer .Name , Namespace : mcpServer .Namespace }, deployment )
157191 if err != nil && errors .IsNotFound (err ) {
158192 // Define a new deployment
159- dep := r .deploymentForMCPServer (mcpServer )
193+ dep := r .deploymentForMCPServer (ctx , mcpServer )
160194 if dep == nil {
161195 ctxLogger .Error (nil , "Failed to create Deployment object" )
162196 return ctrl.Result {}, fmt .Errorf ("failed to create Deployment object" )
@@ -225,7 +259,7 @@ func (r *MCPServerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (
225259 // Check if the deployment spec changed
226260 if deploymentNeedsUpdate (deployment , mcpServer ) {
227261 // Update the deployment
228- newDeployment := r .deploymentForMCPServer (mcpServer )
262+ newDeployment := r .deploymentForMCPServer (ctx , mcpServer )
229263 deployment .Spec = newDeployment .Spec
230264 err = r .Update (ctx , deployment )
231265 if err != nil {
@@ -401,7 +435,7 @@ func (r *MCPServerReconciler) ensureRBACResources(ctx context.Context, mcpServer
401435// deploymentForMCPServer returns a MCPServer Deployment object
402436//
403437//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 {
405439 ls := labelsForMCPServer (m .Name )
406440 replicas := int32 (1 )
407441
@@ -581,22 +615,18 @@ func (r *MCPServerReconciler) deploymentForMCPServer(m *mcpv1alpha1.MCPServer) *
581615 }
582616 }
583617
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+ platform , 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 )
623+ platform = kubernetes . PlatformKubernetes
590624 }
591625
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- }
626+ // Use SecurityContextBuilder for platform-aware security context
627+ securityBuilder := kubernetes .NewSecurityContextBuilder (platform )
628+ proxyRunnerPodSecurityContext := securityBuilder .BuildPodSecurityContext ()
629+ proxyRunnerContainerSecurityContext := securityBuilder .BuildContainerSecurityContext ()
600630
601631 env = ensureRequiredEnvVars (env )
602632
0 commit comments