Skip to content

Commit 256c9d5

Browse files
committed
Deploy ToolHive Operator into OpenShift (#1063)
* Update helm chart resources and seccompProfile type values for OKD environment. * Update MCPServer Deployment with check for XDG_CONFIG_HOME and HOME env vars being set. * Add OpenShift environment detection by way of route v1 API availability or OPENSHIFT_OPERATOR env var override. Signed-off-by: Roddie Kieley <rkieley@redhat.com>
1 parent d308a74 commit 256c9d5

File tree

5 files changed

+195
-8
lines changed

5 files changed

+195
-8
lines changed

cmd/thv-operator/controllers/mcpserver_controller.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -552,6 +552,8 @@ func (r *MCPServerReconciler) deploymentForMCPServer(m *mcpv1alpha1.MCPServer) *
552552
}
553553
}
554554

555+
env = ensureRequiredEnvVars(env)
556+
555557
dep := &appsv1.Deployment{
556558
ObjectMeta: metav1.ObjectMeta{
557559
Name: m.Name,
@@ -621,6 +623,36 @@ func (r *MCPServerReconciler) deploymentForMCPServer(m *mcpv1alpha1.MCPServer) *
621623
return dep
622624
}
623625

626+
func ensureRequiredEnvVars(env []corev1.EnvVar) []corev1.EnvVar {
627+
// Check for the existence of the XDG_CONFIG_HOME and HOME environment variables
628+
// and set them to /tmp if they don't exist
629+
xdgConfigHomeFound := false
630+
homeFound := false
631+
for _, envVar := range env {
632+
if envVar.Name == "XDG_CONFIG_HOME" {
633+
xdgConfigHomeFound = true
634+
}
635+
if envVar.Name == "HOME" {
636+
homeFound = true
637+
}
638+
}
639+
if !xdgConfigHomeFound {
640+
logger.Debugf("XDG_CONFIG_HOME not found, setting to /tmp")
641+
env = append(env, corev1.EnvVar{
642+
Name: "XDG_CONFIG_HOME",
643+
Value: "/tmp",
644+
})
645+
}
646+
if !homeFound {
647+
logger.Debugf("HOME not found, setting to /tmp")
648+
env = append(env, corev1.EnvVar{
649+
Name: "HOME",
650+
Value: "/tmp",
651+
})
652+
}
653+
return env
654+
}
655+
624656
func createServiceName(mcpServerName string) string {
625657
return fmt.Sprintf("mcp-%s-proxy", mcpServerName)
626658
}

deploy/charts/operator/values.yaml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,14 @@ operator:
4242
# -- Pod security context settings
4343
podSecurityContext:
4444
runAsNonRoot: true
45+
seccompProfile:
46+
type: RuntimeDefault
4547

4648
# -- Container security context settings for the operator
4749
containerSecurityContext:
4850
allowPrivilegeEscalation: false
4951
readOnlyRootFilesystem: true
5052
runAsNonRoot: true
51-
runAsUser: 1000
5253
capabilities:
5354
drop:
5455
- ALL
@@ -85,10 +86,10 @@ operator:
8586
resources:
8687
limits:
8788
cpu: 500m
88-
memory: 128Mi
89+
memory: 384Mi
8990
requests:
9091
cpu: 10m
91-
memory: 64Mi
92+
memory: 192Mi
9293

9394
# -- RBAC configuration for the operator
9495
rbac:

pkg/container/kubernetes/client.go

Lines changed: 84 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
"k8s.io/client-go/rest"
2828
"k8s.io/client-go/tools/remotecommand"
2929
"k8s.io/client-go/tools/watch"
30+
clientconfig "sigs.k8s.io/controller-runtime/pkg/client/config"
3031

3132
"github.com/stacklok/toolhive/pkg/container/runtime"
3233
"github.com/stacklok/toolhive/pkg/logger"
@@ -246,17 +247,30 @@ func (c *Client) DeployWorkload(ctx context.Context,
246247
}
247248

248249
// Ensure the pod template has required configuration (labels, etc.)
249-
podTemplateSpec = ensurePodTemplateConfig(podTemplateSpec, containerLabels)
250+
isOpenShift := false
251+
// Get a config to talk to the apiserver
252+
cfg, err := clientconfig.GetConfig()
253+
if err != nil {
254+
return 0, fmt.Errorf("error getting config for APIServer: %w", err)
255+
}
256+
257+
isOpenShift, err = DetectOpenShiftWith(cfg)
258+
if err != nil {
259+
return 0, fmt.Errorf("can't determine api server type: %w", err)
260+
}
261+
262+
podTemplateSpec = ensurePodTemplateConfig(podTemplateSpec, containerLabels, isOpenShift)
250263

251264
// Configure the MCP container
252-
err := configureMCPContainer(
265+
err = configureMCPContainer(
253266
podTemplateSpec,
254267
image,
255268
command,
256269
attachStdio,
257270
envVarList,
258271
transportType,
259272
options,
273+
isOpenShift,
260274
)
261275
if err != nil {
262276
return 0, err
@@ -891,6 +905,7 @@ func createPodTemplateFromPatch(patchJSON string) (*corev1apply.PodTemplateSpecA
891905
func ensurePodTemplateConfig(
892906
podTemplateSpec *corev1apply.PodTemplateSpecApplyConfiguration,
893907
containerLabels map[string]string,
908+
isOpenShift bool,
894909
) *corev1apply.PodTemplateSpecApplyConfiguration {
895910
podTemplateSpec = ensureObjectMetaApplyConfigurationExists(podTemplateSpec)
896911
// Ensure the pod template has labels
@@ -940,6 +955,31 @@ func ensurePodTemplateConfig(
940955
podTemplateSpec.Spec.SecurityContext = podTemplateSpec.Spec.SecurityContext.WithRunAsGroup(int64(1000))
941956
}
942957
}
958+
959+
if isOpenShift {
960+
if podTemplateSpec.Spec.SecurityContext.RunAsUser != nil {
961+
podTemplateSpec.Spec.SecurityContext.RunAsUser = nil
962+
}
963+
964+
if podTemplateSpec.Spec.SecurityContext.RunAsGroup != nil {
965+
podTemplateSpec.Spec.SecurityContext.RunAsGroup = nil
966+
}
967+
968+
if podTemplateSpec.Spec.SecurityContext.FSGroup != nil {
969+
podTemplateSpec.Spec.SecurityContext.FSGroup = nil
970+
}
971+
972+
if podTemplateSpec.Spec.SecurityContext.SeccompProfile == nil {
973+
podTemplateSpec.Spec.SecurityContext.SeccompProfile =
974+
corev1apply.SeccompProfile().WithType(
975+
corev1.SeccompProfileTypeRuntimeDefault)
976+
} else {
977+
podTemplateSpec.Spec.SecurityContext.SeccompProfile =
978+
podTemplateSpec.Spec.SecurityContext.SeccompProfile.WithType(
979+
corev1.SeccompProfileTypeRuntimeDefault)
980+
}
981+
}
982+
943983
return podTemplateSpec
944984
}
945985

@@ -985,7 +1025,18 @@ func configureContainer(
9851025
command []string,
9861026
attachStdio bool,
9871027
envVars []*corev1apply.EnvVarApplyConfiguration,
1028+
isOpenShift bool,
9881029
) {
1030+
logger.Infof("Configuring container %s with image %s", *container.Name, image)
1031+
logger.Infof("Command: ")
1032+
for _, arg := range command {
1033+
logger.Infof("Arg: %s", arg)
1034+
}
1035+
logger.Infof("AttachStdio: %v", attachStdio)
1036+
for _, envVar := range envVars {
1037+
logger.Infof("EnvVar: %s=%s", *envVar.Name, *envVar.Value)
1038+
}
1039+
9891040
container.WithImage(image).
9901041
WithArgs(command...).
9911042
WithStdin(attachStdio).
@@ -1029,6 +1080,34 @@ func configureContainer(
10291080
container.SecurityContext = container.SecurityContext.WithAllowPrivilegeEscalation(false)
10301081
}
10311082
}
1083+
1084+
if isOpenShift {
1085+
logger.Infof("Setting OpenShift security context requirements to container %s", *container.Name)
1086+
1087+
if container.SecurityContext.RunAsUser != nil {
1088+
container.SecurityContext.RunAsUser = nil
1089+
}
1090+
1091+
if container.SecurityContext.RunAsGroup != nil {
1092+
container.SecurityContext.RunAsGroup = nil
1093+
}
1094+
1095+
if container.SecurityContext.SeccompProfile == nil {
1096+
container.SecurityContext.SeccompProfile =
1097+
corev1apply.SeccompProfile().WithType(
1098+
corev1.SeccompProfileTypeRuntimeDefault)
1099+
} else {
1100+
container.SecurityContext.SeccompProfile =
1101+
container.SecurityContext.SeccompProfile.WithType(
1102+
corev1.SeccompProfileTypeRuntimeDefault)
1103+
}
1104+
1105+
if container.SecurityContext.Capabilities == nil {
1106+
container.SecurityContext.Capabilities = &corev1apply.CapabilitiesApplyConfiguration{
1107+
Drop: []corev1.Capability{"ALL"},
1108+
}
1109+
}
1110+
}
10321111
}
10331112

10341113
// configureMCPContainer configures the MCP container in the pod template
@@ -1040,6 +1119,7 @@ func configureMCPContainer(
10401119
envVarList []*corev1apply.EnvVarApplyConfiguration,
10411120
transportType string,
10421121
options *runtime.DeployWorkloadOptions,
1122+
isOpenShift bool,
10431123
) error {
10441124
// Get the "mcp" container if it exists
10451125
mcpContainer := getMCPContainer(podTemplateSpec)
@@ -1049,7 +1129,7 @@ func configureMCPContainer(
10491129
mcpContainer = corev1apply.Container().WithName("mcp")
10501130

10511131
// Configure the container
1052-
configureContainer(mcpContainer, image, command, attachStdio, envVarList)
1132+
configureContainer(mcpContainer, image, command, attachStdio, envVarList, isOpenShift)
10531133

10541134
// Configure ports if needed
10551135
if options != nil && transportType == string(transtypes.TransportTypeSSE) {
@@ -1064,7 +1144,7 @@ func configureMCPContainer(
10641144
podTemplateSpec.Spec.WithContainers(mcpContainer)
10651145
} else {
10661146
// Configure the existing container
1067-
configureContainer(mcpContainer, image, command, attachStdio, envVarList)
1147+
configureContainer(mcpContainer, image, command, attachStdio, envVarList, isOpenShift)
10681148

10691149
// Configure ports if needed
10701150
if options != nil && transportType == string(transtypes.TransportTypeSSE) {

pkg/container/kubernetes/client_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,7 @@ func TestEnsurePodTemplateConfig(t *testing.T) {
346346
t.Run(tc.name, func(t *testing.T) {
347347
t.Parallel()
348348
// Call the function
349-
result := ensurePodTemplateConfig(tc.podTemplateSpec, tc.containerLabels)
349+
result := ensurePodTemplateConfig(tc.podTemplateSpec, tc.containerLabels, false)
350350

351351
// Check the result
352352
assert.NotNil(t, result)
@@ -521,6 +521,7 @@ func TestConfigureMCPContainer(t *testing.T) {
521521
tc.envVars,
522522
tc.transportType,
523523
tc.options,
524+
false,
524525
)
525526

526527
// Check that there was no error

pkg/container/kubernetes/common.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package kubernetes
2+
3+
import (
4+
"os"
5+
"strings"
6+
"time"
7+
8+
"k8s.io/apimachinery/pkg/runtime/schema"
9+
"k8s.io/client-go/discovery"
10+
"k8s.io/client-go/rest"
11+
12+
"github.com/stacklok/toolhive/pkg/logger"
13+
)
14+
15+
// extra kinds
16+
const (
17+
// defaultRetries is the number of times a resource discovery is retried
18+
defaultRetries = 10
19+
20+
//defaultRetryInterval is the interval to wait before retring a resource discovery
21+
defaultRetryInterval = 3 * time.Second
22+
)
23+
24+
var isOpenShift *bool = nil
25+
26+
// DetectOpenShiftWith determines whether the current cluster is an OpenShift
27+
// cluster by attempting to discover the Route resource (route.openshift.io/v1).
28+
// It returns true when the resource is present. The provided REST config is used
29+
// by the discovery client to query the API server.
30+
func DetectOpenShiftWith(config *rest.Config) (bool, error) {
31+
// If first time, check if we are running on OpenShift
32+
if isOpenShift == nil {
33+
value, ok := os.LookupEnv("OPERATOR_OPENSHIFT")
34+
if ok {
35+
//ctrl.Log.V(1).Info("Set by env-var 'OPERATOR_OPENSHIFT': " + value)
36+
logger.Infof("OpenShift set by env var 'OPERATOR_OPENSHIFT': " + value)
37+
return strings.ToLower(value) == "true", nil
38+
}
39+
40+
discoveryClient, err := discovery.NewDiscoveryClientForConfig(config)
41+
if err != nil {
42+
return false, err
43+
}
44+
45+
isOpenShiftResourcePresent := false
46+
for i := 0; i < defaultRetries; i++ {
47+
isOpenShiftResourcePresent, err = discovery.IsResourceEnabled(discoveryClient,
48+
schema.GroupVersionResource{
49+
Group: "route.openshift.io",
50+
Version: "v1",
51+
Resource: "routes",
52+
})
53+
54+
if err == nil {
55+
break
56+
}
57+
58+
time.Sleep(defaultRetryInterval)
59+
}
60+
61+
if err != nil {
62+
return false, err
63+
}
64+
65+
isOpenShift = &isOpenShiftResourcePresent
66+
if isOpenShiftResourcePresent {
67+
logger.Infof("OpenShift detected by route resource check.")
68+
}
69+
}
70+
71+
// Otherwise, return the cached value
72+
return *isOpenShift, nil
73+
}

0 commit comments

Comments
 (0)