Skip to content

Commit a5a0621

Browse files
yroblataskbot
andauthored
add missing coverage for controller: (#2482)
- allow to define service type Co-authored-by: taskbot <[email protected]>
1 parent b159c21 commit a5a0621

File tree

8 files changed

+181
-4
lines changed

8 files changed

+181
-4
lines changed

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,12 @@ type VirtualMCPServerSpec struct {
4242
// +optional
4343
Operational *OperationalConfig `json:"operational,omitempty"`
4444

45+
// ServiceType specifies the Kubernetes service type for the Virtual MCP server
46+
// +kubebuilder:validation:Enum=ClusterIP;NodePort;LoadBalancer
47+
// +kubebuilder:default=ClusterIP
48+
// +optional
49+
ServiceType string `json:"serviceType,omitempty"`
50+
4551
// PodTemplateSpec defines the pod template to use for the Virtual MCP server
4652
// This allows for customizing the pod configuration beyond what is provided by the other fields.
4753
// Note that to modify the specific container the Virtual MCP server runs in, you must specify

cmd/thv-operator/controllers/virtualmcpserver_controller.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -647,6 +647,15 @@ func (*VirtualMCPServerReconciler) serviceNeedsUpdate(
647647
return true
648648
}
649649

650+
// Check if service type has changed
651+
expectedServiceType := corev1.ServiceTypeClusterIP
652+
if vmcp.Spec.ServiceType != "" {
653+
expectedServiceType = corev1.ServiceType(vmcp.Spec.ServiceType)
654+
}
655+
if service.Spec.Type != expectedServiceType {
656+
return true
657+
}
658+
650659
// Check if service metadata has changed
651660
expectedLabels := labelsForVirtualMCPServer(vmcp.Name)
652661
expectedAnnotations := make(map[string]string)

cmd/thv-operator/controllers/virtualmcpserver_controller_test.go

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,156 @@ func TestVirtualMCPServerEnsureService(t *testing.T) {
393393
assert.Equal(t, "http", service.Spec.Ports[0].Name)
394394
}
395395

396+
// TestVirtualMCPServerServiceType tests Service creation with different service types
397+
func TestVirtualMCPServerServiceType(t *testing.T) {
398+
t.Parallel()
399+
400+
tests := []struct {
401+
name string
402+
serviceType string
403+
expectedServiceType corev1.ServiceType
404+
}{
405+
{
406+
name: "default to ClusterIP",
407+
serviceType: "",
408+
expectedServiceType: corev1.ServiceTypeClusterIP,
409+
},
410+
{
411+
name: "explicit ClusterIP",
412+
serviceType: "ClusterIP",
413+
expectedServiceType: corev1.ServiceTypeClusterIP,
414+
},
415+
{
416+
name: "LoadBalancer",
417+
serviceType: "LoadBalancer",
418+
expectedServiceType: corev1.ServiceTypeLoadBalancer,
419+
},
420+
{
421+
name: "NodePort",
422+
serviceType: "NodePort",
423+
expectedServiceType: corev1.ServiceTypeNodePort,
424+
},
425+
}
426+
427+
for _, tt := range tests {
428+
t.Run(tt.name, func(t *testing.T) {
429+
t.Parallel()
430+
431+
vmcp := &mcpv1alpha1.VirtualMCPServer{
432+
ObjectMeta: metav1.ObjectMeta{
433+
Name: "test-vmcp",
434+
Namespace: "default",
435+
},
436+
Spec: mcpv1alpha1.VirtualMCPServerSpec{
437+
GroupRef: mcpv1alpha1.GroupRef{
438+
Name: "test-group",
439+
},
440+
ServiceType: tt.serviceType,
441+
},
442+
}
443+
444+
scheme := runtime.NewScheme()
445+
_ = mcpv1alpha1.AddToScheme(scheme)
446+
_ = corev1.AddToScheme(scheme)
447+
448+
r := &VirtualMCPServerReconciler{
449+
Scheme: scheme,
450+
}
451+
452+
// Test serviceForVirtualMCPServer
453+
service := r.serviceForVirtualMCPServer(context.Background(), vmcp)
454+
require.NotNil(t, service)
455+
assert.Equal(t, tt.expectedServiceType, service.Spec.Type)
456+
})
457+
}
458+
}
459+
460+
// TestVirtualMCPServerServiceNeedsUpdate tests service update detection
461+
func TestVirtualMCPServerServiceNeedsUpdate(t *testing.T) {
462+
t.Parallel()
463+
464+
baseVmcp := &mcpv1alpha1.VirtualMCPServer{
465+
ObjectMeta: metav1.ObjectMeta{
466+
Name: "test-vmcp",
467+
Namespace: "default",
468+
},
469+
Spec: mcpv1alpha1.VirtualMCPServerSpec{
470+
GroupRef: mcpv1alpha1.GroupRef{
471+
Name: "test-group",
472+
},
473+
ServiceType: "ClusterIP",
474+
},
475+
}
476+
477+
baseService := &corev1.Service{
478+
ObjectMeta: metav1.ObjectMeta{
479+
Name: vmcpServiceName(baseVmcp.Name),
480+
Namespace: baseVmcp.Namespace,
481+
Labels: labelsForVirtualMCPServer(baseVmcp.Name),
482+
},
483+
Spec: corev1.ServiceSpec{
484+
Type: corev1.ServiceTypeClusterIP,
485+
Ports: []corev1.ServicePort{{
486+
Port: vmcpDefaultPort,
487+
}},
488+
},
489+
}
490+
491+
tests := []struct {
492+
name string
493+
service *corev1.Service
494+
vmcp *mcpv1alpha1.VirtualMCPServer
495+
needsUpdate bool
496+
}{
497+
{
498+
name: "no update needed",
499+
service: baseService.DeepCopy(),
500+
vmcp: baseVmcp.DeepCopy(),
501+
needsUpdate: false,
502+
},
503+
{
504+
name: "service type changed to LoadBalancer",
505+
service: baseService.DeepCopy(),
506+
vmcp: func() *mcpv1alpha1.VirtualMCPServer {
507+
v := baseVmcp.DeepCopy()
508+
v.Spec.ServiceType = "LoadBalancer"
509+
return v
510+
}(),
511+
needsUpdate: true,
512+
},
513+
{
514+
name: "service type changed to NodePort",
515+
service: baseService.DeepCopy(),
516+
vmcp: func() *mcpv1alpha1.VirtualMCPServer {
517+
v := baseVmcp.DeepCopy()
518+
v.Spec.ServiceType = "NodePort"
519+
return v
520+
}(),
521+
needsUpdate: true,
522+
},
523+
{
524+
name: "port changed",
525+
service: func() *corev1.Service {
526+
s := baseService.DeepCopy()
527+
s.Spec.Ports[0].Port = 9999
528+
return s
529+
}(),
530+
vmcp: baseVmcp.DeepCopy(),
531+
needsUpdate: true,
532+
},
533+
}
534+
535+
for _, tt := range tests {
536+
t.Run(tt.name, func(t *testing.T) {
537+
t.Parallel()
538+
539+
r := &VirtualMCPServerReconciler{}
540+
result := r.serviceNeedsUpdate(tt.service, tt.vmcp)
541+
assert.Equal(t, tt.needsUpdate, result)
542+
})
543+
}
544+
}
545+
396546
// TestVirtualMCPServerUpdateStatus tests status update logic
397547
func TestVirtualMCPServerUpdateStatus(t *testing.T) {
398548
t.Parallel()

cmd/thv-operator/controllers/virtualmcpserver_deployment.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -322,9 +322,11 @@ func (r *VirtualMCPServerReconciler) serviceForVirtualMCPServer(
322322
// Build service metadata
323323
serviceLabels, serviceAnnotations := r.buildServiceMetadataForVmcp(ls, vmcp)
324324

325-
// Determine service type (ClusterIP by default, LoadBalancer if specified)
325+
// Determine service type from spec (defaults to ClusterIP if not specified)
326326
serviceType := corev1.ServiceTypeClusterIP
327-
// TODO: Add configuration option for LoadBalancer service type
327+
if vmcp.Spec.ServiceType != "" {
328+
serviceType = corev1.ServiceType(vmcp.Spec.ServiceType)
329+
}
328330

329331
svc := &corev1.Service{
330332
ObjectMeta: metav1.ObjectMeta{

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.51
5+
version: 0.0.52
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,7 +1,7 @@
11

22
# ToolHive Operator CRDs Helm Chart
33

4-
![Version: 0.0.51](https://img.shields.io/badge/Version-0.0.51-informational?style=flat-square)
4+
![Version: 0.0.52](https://img.shields.io/badge/Version-0.0.52-informational?style=flat-square)
55
![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square)
66

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

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -725,6 +725,15 @@ spec:
725725
This field accepts a PodTemplateSpec object as JSON/YAML.
726726
type: object
727727
x-kubernetes-preserve-unknown-fields: true
728+
serviceType:
729+
default: ClusterIP
730+
description: ServiceType specifies the Kubernetes service type for
731+
the Virtual MCP server
732+
enum:
733+
- ClusterIP
734+
- NodePort
735+
- LoadBalancer
736+
type: string
728737
tokenCache:
729738
description: TokenCache configures token caching behavior
730739
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)