Skip to content

Commit 1927930

Browse files
4t8dddmjbjhrozek
authored
Add log level configuration support to VirtualMCPServer (#2837)
Add debug log level support to VirtualMCPServer Adds LogLevel field to VirtualMCPServer CRD OperationalConfig to allow enabling debug logging for vMCP instances. The field accepts only 'debug' as a valid value. When not set, the vmcp binary defaults to info level. When LogLevel is set to 'debug', the --debug flag is passed to the vmcp container. The containerNeedsUpdate function now tracks container args changes to ensure deployments are updated when debug is enabled or disabled. This is necessary because the vmcp binary's logger initializes before the config file is loaded, requiring the flag at startup. Update existing test cases to include Args field checking. Add new test cases for both log level change scenarios: enabling debug and removing debug from the CR. Remove completed TODO for operational log level env var. Bumps CRD chart version from 0.0.74 to 0.0.75. Fixes #2781 Signed-off-by: 4t8dd <wanger.xyz@gmail.com> Co-authored-by: Don Browne <dmjb@users.noreply.github.com> Co-authored-by: Jakub Hrozek <jakub.hrozek@posteo.se>
1 parent 35bf749 commit 1927930

File tree

9 files changed

+159
-19
lines changed

9 files changed

+159
-19
lines changed

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,12 @@ type ErrorHandling struct {
330330

331331
// OperationalConfig defines operational settings
332332
type OperationalConfig struct {
333+
// LogLevel sets the logging level for the Virtual MCP server.
334+
// Set to "debug" to enable debug logging. When not set, defaults to info level.
335+
// +kubebuilder:validation:Enum=debug
336+
// +optional
337+
LogLevel string `json:"logLevel,omitempty"`
338+
333339
// Timeouts configures timeout settings
334340
// +optional
335341
Timeouts *TimeoutConfig `json:"timeouts,omitempty"`

cmd/thv-operator/controllers/virtualmcpserver_controller.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -812,6 +812,12 @@ func (r *VirtualMCPServerReconciler) containerNeedsUpdate(
812812
return true
813813
}
814814

815+
// Check if container args have changed (includes --debug flag from logLevel)
816+
expectedArgs := r.buildContainerArgsForVmcp(vmcp)
817+
if !reflect.DeepEqual(container.Args, expectedArgs) {
818+
return true
819+
}
820+
815821
// Check if environment variables have changed
816822
expectedEnv := r.buildEnvVarsForVmcp(ctx, vmcp, workloadNames)
817823
if !reflect.DeepEqual(container.Env, expectedEnv) {

cmd/thv-operator/controllers/virtualmcpserver_controller_test.go

Lines changed: 71 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1401,7 +1401,8 @@ func TestVirtualMCPServerContainerNeedsUpdate(t *testing.T) {
14011401
Ports: []corev1.ContainerPort{
14021402
{ContainerPort: 4483},
14031403
},
1404-
Env: reconciler.buildEnvVarsForVmcp(context.Background(), vmcp, []string{}),
1404+
Args: reconciler.buildContainerArgsForVmcp(vmcp),
1405+
Env: reconciler.buildEnvVarsForVmcp(context.Background(), vmcp, []string{}),
14051406
},
14061407
},
14071408
ServiceAccountName: "wrong-service-account",
@@ -1412,6 +1413,69 @@ func TestVirtualMCPServerContainerNeedsUpdate(t *testing.T) {
14121413
vmcp: vmcp,
14131414
expectedUpdate: true,
14141415
},
1416+
{
1417+
name: "log level change to debug needs update",
1418+
deployment: &appsv1.Deployment{
1419+
Spec: appsv1.DeploymentSpec{
1420+
Template: corev1.PodTemplateSpec{
1421+
Spec: corev1.PodSpec{
1422+
Containers: []corev1.Container{
1423+
{
1424+
Name: "vmcp",
1425+
Image: getVmcpImage(),
1426+
Ports: []corev1.ContainerPort{
1427+
{ContainerPort: 4483},
1428+
},
1429+
Args: []string{"serve", "--config=/etc/vmcp-config/config.yaml", "--host=0.0.0.0", "--port=4483"},
1430+
Env: reconciler.buildEnvVarsForVmcp(context.Background(), vmcp, []string{}),
1431+
},
1432+
},
1433+
ServiceAccountName: vmcpServiceAccountName(vmcp.Name),
1434+
},
1435+
},
1436+
},
1437+
},
1438+
vmcp: &mcpv1alpha1.VirtualMCPServer{
1439+
ObjectMeta: metav1.ObjectMeta{
1440+
Name: testVmcpName,
1441+
Namespace: "default",
1442+
},
1443+
Spec: mcpv1alpha1.VirtualMCPServerSpec{
1444+
GroupRef: mcpv1alpha1.GroupRef{
1445+
Name: testGroupName,
1446+
},
1447+
Operational: &mcpv1alpha1.OperationalConfig{
1448+
LogLevel: "debug",
1449+
},
1450+
},
1451+
},
1452+
expectedUpdate: true,
1453+
},
1454+
{
1455+
name: "log level removed from debug needs update",
1456+
deployment: &appsv1.Deployment{
1457+
Spec: appsv1.DeploymentSpec{
1458+
Template: corev1.PodTemplateSpec{
1459+
Spec: corev1.PodSpec{
1460+
Containers: []corev1.Container{
1461+
{
1462+
Name: "vmcp",
1463+
Image: getVmcpImage(),
1464+
Ports: []corev1.ContainerPort{
1465+
{ContainerPort: 4483},
1466+
},
1467+
Args: []string{"serve", "--config=/etc/vmcp-config/config.yaml", "--host=0.0.0.0", "--port=4483", "--debug"},
1468+
Env: reconciler.buildEnvVarsForVmcp(context.Background(), vmcp, []string{}),
1469+
},
1470+
},
1471+
ServiceAccountName: vmcpServiceAccountName(vmcp.Name),
1472+
},
1473+
},
1474+
},
1475+
},
1476+
vmcp: vmcp,
1477+
expectedUpdate: true,
1478+
},
14151479
{
14161480
name: "no changes - no update needed",
14171481
deployment: &appsv1.Deployment{
@@ -1425,7 +1489,8 @@ func TestVirtualMCPServerContainerNeedsUpdate(t *testing.T) {
14251489
Ports: []corev1.ContainerPort{
14261490
{ContainerPort: 4483},
14271491
},
1428-
Env: reconciler.buildEnvVarsForVmcp(context.Background(), vmcp, []string{}),
1492+
Args: reconciler.buildContainerArgsForVmcp(vmcp),
1493+
Env: reconciler.buildEnvVarsForVmcp(context.Background(), vmcp, []string{}),
14291494
},
14301495
},
14311496
ServiceAccountName: vmcpServiceAccountName(vmcp.Name),
@@ -1786,7 +1851,8 @@ func TestVirtualMCPServerDeploymentNeedsUpdate(t *testing.T) {
17861851
Ports: []corev1.ContainerPort{
17871852
{ContainerPort: 4483},
17881853
},
1789-
Env: reconciler.buildEnvVarsForVmcp(context.Background(), vmcp, []string{}),
1854+
Args: reconciler.buildContainerArgsForVmcp(vmcp),
1855+
Env: reconciler.buildEnvVarsForVmcp(context.Background(), vmcp, []string{}),
17901856
},
17911857
},
17921858
ServiceAccountName: vmcpServiceAccountName(vmcp.Name),
@@ -1817,7 +1883,8 @@ func TestVirtualMCPServerDeploymentNeedsUpdate(t *testing.T) {
18171883
Ports: []corev1.ContainerPort{
18181884
{ContainerPort: 4483},
18191885
},
1820-
Env: reconciler.buildEnvVarsForVmcp(context.Background(), vmcp, []string{}),
1886+
Args: reconciler.buildContainerArgsForVmcp(vmcp),
1887+
Env: reconciler.buildEnvVarsForVmcp(context.Background(), vmcp, []string{}),
18211888
},
18221889
},
18231890
ServiceAccountName: vmcpServiceAccountName(vmcp.Name),

cmd/thv-operator/controllers/virtualmcpserver_deployment.go

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ import (
2424
)
2525

2626
const (
27+
// Log level configuration
28+
logLevelDebug = "debug" // Debug log level value
29+
2730
// Network configuration
2831
vmcpDefaultPort = int32(4483) // Default port for VirtualMCPServer service (matches vmcp server port)
2932

@@ -71,7 +74,7 @@ func (r *VirtualMCPServerReconciler) deploymentForVirtualMCPServer(
7174
replicas := int32(1)
7275

7376
// Build deployment components using helper functions
74-
args := r.buildContainerArgsForVmcp()
77+
args := r.buildContainerArgsForVmcp(vmcp)
7578
volumeMounts, volumes := r.buildVolumesForVmcp(vmcp)
7679
env := r.buildEnvVarsForVmcp(ctx, vmcp, workloadNames)
7780
deploymentLabels, deploymentAnnotations := r.buildDeploymentMetadataForVmcp(ls, vmcp)
@@ -141,13 +144,24 @@ func (r *VirtualMCPServerReconciler) deploymentForVirtualMCPServer(
141144
}
142145

143146
// buildContainerArgsForVmcp builds the container arguments for vmcp
144-
func (*VirtualMCPServerReconciler) buildContainerArgsForVmcp() []string {
145-
return []string{
147+
func (*VirtualMCPServerReconciler) buildContainerArgsForVmcp(
148+
vmcp *mcpv1alpha1.VirtualMCPServer,
149+
) []string {
150+
args := []string{
146151
"serve",
147152
"--config=/etc/vmcp-config/config.yaml",
148153
"--host=0.0.0.0", // Listen on all interfaces for Kubernetes service routing
149154
"--port=4483", // Standard vmcp port
150155
}
156+
157+
// Add --debug flag if log level is set to debug
158+
// Note: vmcp binary currently only supports --debug flag, not other log levels
159+
// The flag must be passed at startup because logger.Initialize() runs before config is loaded
160+
if vmcp.Spec.Operational != nil && vmcp.Spec.Operational.LogLevel == logLevelDebug {
161+
args = append(args, "--debug")
162+
}
163+
164+
return args
151165
}
152166

153167
// buildVolumesForVmcp builds volumes and volume mounts for vmcp
@@ -200,12 +214,6 @@ func (r *VirtualMCPServerReconciler) buildEnvVarsForVmcp(
200214
Value: vmcp.Namespace,
201215
})
202216

203-
// TODO: Add log level from operational config when Operational is not nil
204-
//nolint:staticcheck // Empty branch reserved for future log level configuration
205-
if vmcp.Spec.Operational != nil {
206-
// Log level env var will be added here
207-
}
208-
209217
// Mount OIDC client secret
210218
env = append(env, r.buildOIDCEnvVars(vmcp)...)
211219

cmd/thv-operator/controllers/virtualmcpserver_deployment_test.go

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,11 +79,56 @@ func TestDeploymentForVirtualMCPServer(t *testing.T) {
7979
func TestBuildContainerArgsForVmcp(t *testing.T) {
8080
t.Parallel()
8181

82-
r := &VirtualMCPServerReconciler{}
83-
args := r.buildContainerArgsForVmcp()
82+
tests := []struct {
83+
name string
84+
vmcp *mcpv1alpha1.VirtualMCPServer
85+
wantArgs []string
86+
}{
87+
{
88+
name: "without log level",
89+
vmcp: &mcpv1alpha1.VirtualMCPServer{
90+
ObjectMeta: metav1.ObjectMeta{
91+
Name: "test-vmcp",
92+
Namespace: "default",
93+
},
94+
Spec: mcpv1alpha1.VirtualMCPServerSpec{
95+
GroupRef: mcpv1alpha1.GroupRef{
96+
Name: "test-group",
97+
},
98+
},
99+
},
100+
wantArgs: []string{"serve", "--config=/etc/vmcp-config/config.yaml", "--host=0.0.0.0", "--port=4483"},
101+
},
102+
{
103+
name: "with log level debug",
104+
vmcp: &mcpv1alpha1.VirtualMCPServer{
105+
ObjectMeta: metav1.ObjectMeta{
106+
Name: "test-vmcp",
107+
Namespace: "default",
108+
},
109+
Spec: mcpv1alpha1.VirtualMCPServerSpec{
110+
GroupRef: mcpv1alpha1.GroupRef{
111+
Name: "test-group",
112+
},
113+
Operational: &mcpv1alpha1.OperationalConfig{
114+
LogLevel: "debug",
115+
},
116+
},
117+
},
118+
wantArgs: []string{"serve", "--config=/etc/vmcp-config/config.yaml", "--host=0.0.0.0", "--port=4483", "--debug"},
119+
},
120+
}
84121

85-
assert.Contains(t, args, "serve")
86-
assert.Contains(t, args, "--config=/etc/vmcp-config/config.yaml")
122+
for _, tt := range tests {
123+
tt := tt // capture range variable
124+
t.Run(tt.name, func(t *testing.T) {
125+
t.Parallel()
126+
r := &VirtualMCPServerReconciler{}
127+
args := r.buildContainerArgsForVmcp(tt.vmcp)
128+
129+
assert.Equal(t, tt.wantArgs, args)
130+
})
131+
}
87132
}
88133

89134
// TestBuildVolumesForVmcp tests volume and volume mount generation

deploy/charts/operator-crds/Chart.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@ apiVersion: v2
22
name: toolhive-operator-crds
33
description: A Helm chart for installing the ToolHive Operator CRDs into Kubernetes.
44
type: application
5-
version: 0.0.74
5+
version: 0.0.75
66
appVersion: "0.0.1"

deploy/charts/operator-crds/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# ToolHive Operator CRDs Helm Chart
22

3-
![Version: 0.0.74](https://img.shields.io/badge/Version-0.0.74-informational?style=flat-square)
3+
![Version: 0.0.75](https://img.shields.io/badge/Version-0.0.75-informational?style=flat-square)
44
![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square)
55

66
A Helm chart for installing the ToolHive Operator CRDs into Kubernetes.

deploy/charts/operator-crds/crds/toolhive.stacklok.dev_virtualmcpservers.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -624,6 +624,13 @@ spec:
624624
failures before marking unhealthy
625625
type: integer
626626
type: object
627+
logLevel:
628+
description: |-
629+
LogLevel sets the logging level for the Virtual MCP server.
630+
Set to "debug" to enable debug logging. When not set, defaults to info level.
631+
enum:
632+
- debug
633+
type: string
627634
timeouts:
628635
description: Timeouts configures timeout settings
629636
properties:

docs/operator/crd-api.md

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)