Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 75 additions & 15 deletions cmd/thv-operator/controllers/mcpserver_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -574,6 +574,8 @@ func (r *MCPServerReconciler) deploymentForMCPServer(m *mcpv1alpha1.MCPServer) *
ReadOnlyRootFilesystem: ptr.To(true),
}

env = ensureRequiredEnvVars(env)

dep := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: m.Name,
Expand Down Expand Up @@ -645,6 +647,36 @@ func (r *MCPServerReconciler) deploymentForMCPServer(m *mcpv1alpha1.MCPServer) *
return dep
}

func ensureRequiredEnvVars(env []corev1.EnvVar) []corev1.EnvVar {
// Check for the existence of the XDG_CONFIG_HOME and HOME environment variables
// and set them to /tmp if they don't exist
xdgConfigHomeFound := false
homeFound := false
for _, envVar := range env {
if envVar.Name == "XDG_CONFIG_HOME" {
xdgConfigHomeFound = true
}
if envVar.Name == "HOME" {
homeFound = true
}
}
if !xdgConfigHomeFound {
logger.Debugf("XDG_CONFIG_HOME not found, setting to /tmp")
env = append(env, corev1.EnvVar{
Name: "XDG_CONFIG_HOME",
Value: "/tmp",
})
}
if !homeFound {
logger.Debugf("HOME not found, setting to /tmp")
env = append(env, corev1.EnvVar{
Name: "HOME",
Value: "/tmp",
})
}
return env
}

func createServiceName(mcpServerName string) string {
return fmt.Sprintf("mcp-%s-proxy", mcpServerName)
}
Expand Down Expand Up @@ -842,20 +874,9 @@ func deploymentNeedsUpdate(deployment *appsv1.Deployment, mcpServer *mcpv1alpha1
return true
}

// Check if the tools filter has changed
if mcpServer.Spec.ToolsFilter == nil {
for _, arg := range container.Args {
if strings.HasPrefix(arg, "--tools=") {
return true
}
}
} else {
slices.Sort(mcpServer.Spec.ToolsFilter)
toolsFilterArg := fmt.Sprintf("--tools=%s", strings.Join(mcpServer.Spec.ToolsFilter, ","))
found = slices.Contains(container.Args, toolsFilterArg)
if !found {
return true
}
// Check if the tools filter has changed (order-independent)
if !equalToolsFilter(mcpServer.Spec.ToolsFilter, container.Args) {
return true
}

// Check if the pod template spec has changed
Expand Down Expand Up @@ -910,6 +931,8 @@ func deploymentNeedsUpdate(deployment *appsv1.Deployment, mcpServer *mcpv1alpha1
})
}
}
// Add default environment variables that are always injected
expectedProxyEnv = ensureRequiredEnvVars(expectedProxyEnv)
if !reflect.DeepEqual(container.Env, expectedProxyEnv) {
return true
}
Expand Down Expand Up @@ -972,8 +995,10 @@ func deploymentNeedsUpdate(deployment *appsv1.Deployment, mcpServer *mcpv1alpha1
}

// Check if the service account name has changed
// ServiceAccountName: treat empty (not yet set) as equal to the expected default
expectedServiceAccountName := proxyRunnerServiceAccountName(mcpServer.Name)
if deployment.Spec.Template.Spec.ServiceAccountName != expectedServiceAccountName {
currentServiceAccountName := deployment.Spec.Template.Spec.ServiceAccountName
if currentServiceAccountName != "" && currentServiceAccountName != expectedServiceAccountName {
return true
}

Expand Down Expand Up @@ -1588,3 +1613,38 @@ func (r *MCPServerReconciler) SetupWithManager(mgr ctrl.Manager) error {
Owns(&corev1.Service{}).
Complete(r)
}

// equalToolsFilter returns true when the desired toolsFilter slice and the
// currently-applied `--tools=` argument in the container args represent the
// same unordered set of tools.
func equalToolsFilter(spec []string, args []string) bool {
// Build canonical form for spec
specCanon := canonicalToolsList(spec)

// Extract current tools argument (if any) from args
var currentArg string
for _, a := range args {
if strings.HasPrefix(a, "--tools=") {
currentArg = strings.TrimPrefix(a, "--tools=")
break
}
}

if specCanon == "" && currentArg == "" {
return true // both unset/empty
}

// Canonicalise current list
currentCanon := canonicalToolsList(strings.Split(strings.TrimSpace(currentArg), ","))
return specCanon == currentCanon
}

// canonicalToolsList sorts a slice and joins it with commas; empty slice yields "".
func canonicalToolsList(list []string) string {
if len(list) == 0 || (len(list) == 1 && list[0] == "") {
return ""
}
cp := slices.Clone(list)
slices.Sort(cp)
return strings.Join(cp, ",")
}
Original file line number Diff line number Diff line change
Expand Up @@ -288,14 +288,18 @@ func TestResourceOverrides(t *testing.T) {
var expectedEnvVars map[string]string
if tt.name == "with proxy environment variables" {
expectedEnvVars = map[string]string{
"HTTP_PROXY": "http://proxy.example.com:8080",
"NO_PROXY": "localhost,127.0.0.1",
"CUSTOM_ENV": "custom-value",
"HTTP_PROXY": "http://proxy.example.com:8080",
"NO_PROXY": "localhost,127.0.0.1",
"CUSTOM_ENV": "custom-value",
"XDG_CONFIG_HOME": "/tmp",
"HOME": "/tmp",
}
} else {
expectedEnvVars = map[string]string{
"LOG_LEVEL": "debug",
"METRICS_ENABLED": "true",
"XDG_CONFIG_HOME": "/tmp",
"HOME": "/tmp",
}
}

Expand Down
4 changes: 2 additions & 2 deletions deploy/charts/operator/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ apiVersion: v2
name: toolhive-operator
description: A Helm chart for deploying the ToolHive Operator into Kubernetes.
type: application
version: 0.2.1
appVersion: "0.2.1"
version: 0.2.2
appVersion: "0.2.2"
2 changes: 1 addition & 1 deletion deploy/charts/operator/README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

# ToolHive Operator Helm Chart

![Version: 0.2.1](https://img.shields.io/badge/Version-0.2.1-informational?style=flat-square)
![Version: 0.2.2](https://img.shields.io/badge/Version-0.2.2-informational?style=flat-square)
![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square)

A Helm chart for deploying the ToolHive Operator into Kubernetes.
Expand Down
180 changes: 180 additions & 0 deletions deploy/charts/operator/values-openshift.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
# -- Override the name of the chart
nameOverride: ""
# -- Provide a fully-qualified name override for resources
fullnameOverride: "toolhive-operator"

# -- All values for the operator deployment and associated resources
operator:

# -- Number of replicas for the operator deployment
replicaCount: 1

# -- List of image pull secrets to use
imagePullSecrets: []
# -- Container image for the operator
image: ghcr.io/stacklok/toolhive/operator:v0.2.0
# -- Image pull policy for the operator container
imagePullPolicy: IfNotPresent

# -- Image to use for Toolhive runners
toolhiveRunnerImage: ghcr.io/stacklok/toolhive/proxyrunner:v0.2.0

# -- Host for the proxy deployed by the operator
proxyHost: 0.0.0.0

# -- Environment variables to set in the operator container
env: {}

# -- List of ports to expose from the operator container
ports:
- containerPort: 8080
name: metrics
protocol: TCP
- containerPort: 8081
name: health
protocol: TCP

# -- Annotations to add to the operator pod
podAnnotations: {}
# -- Labels to add to the operator pod
podLabels: {}

# -- Pod security context settings
podSecurityContext:
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault

# -- Container security context settings for the operator
containerSecurityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser:
capabilities:
drop:
- ALL

# -- Liveness probe configuration for the operator
livenessProbe:
httpGet:
path: /healthz
port: health
initialDelaySeconds: 15
periodSeconds: 20
# -- Readiness probe configuration for the operator
readinessProbe:
httpGet:
path: /readyz
port: health
initialDelaySeconds: 5
periodSeconds: 10

# -- Configuration for horizontal pod autoscaling
autoscaling:
# -- Enable autoscaling for the operator
enabled: false
# -- Minimum number of replicas
minReplicas: 1
# -- Maximum number of replicas
maxReplicas: 100
# -- Target CPU utilization percentage for autoscaling
targetCPUUtilizationPercentage: 80
# -- Target memory utilization percentage for autoscaling (uncomment to enable)
# targetMemoryUtilizationPercentage: 80

# -- Resource requests and limits for the operator container
resources:
limits:
cpu: 500m
memory: 384Mi
requests:
cpu: 10m
memory: 192Mi

# -- RBAC configuration for the operator
rbac:
# -- Scope of the RBAC configuration.
# - cluster: The operator will have cluster-wide permissions via ClusterRole and ClusterRoleBinding.
# - namespace: The operator will have permissions to manage resources in the namespaces specified in `allowedNamespaces`.
# The operator will have a ClusterRole and RoleBinding for each namespace in `allowedNamespaces`.
scope: cluster
# -- List of namespaces that the operator is allowed to have permissions to manage.
# Only used if scope is set to "namespace".
allowedNamespaces: []

# -- Service account configuration for the operator
serviceAccount:
# -- Specifies whether a service account should be created
create: true
# -- Automatically mount a ServiceAccount's API credentials
automountServiceAccountToken: true
# -- Annotations to add to the service account
annotations: {}
# -- Labels to add to the service account
labels: {}
# -- The name of the service account to use. If not set and create is true, a name is generated.
name: "toolhive-operator"

# -- Leader election role configuration
leaderElectionRole:
# -- Name of the role for leader election
name: toolhive-operator-leader-election-role
binding:
# -- Name of the role binding for leader election
name: toolhive-operator-leader-election-rolebinding
# -- Rules for the leader election role
rules:
- apiGroups:
- ""
resources:
- configmaps
verbs:
- get
- list
- watch
- create
- update
- patch
- delete
- apiGroups:
- coordination.k8s.io
resources:
- leases
verbs:
- get
- list
- watch
- create
- update
- patch
- delete
- apiGroups:
- ""
resources:
- events
verbs:
- create
- patch

# -- Additional volumes to mount on the operator pod
volumes: []
# - name: foo
# secret:
# secretName: mysecret
# optional: false

# -- Additional volume mounts on the operator container
volumeMounts: []
# - name: foo
# mountPath: "/etc/foo"
# readOnly: true

# -- Node selector for the operator pod
nodeSelector: {}

# -- Tolerations for the operator pod
tolerations: []

# -- Affinity settings for the operator pod
affinity: {}
Loading
Loading