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
81 changes: 5 additions & 76 deletions cmd/thv-operator/api/v1alpha1/virtualmcpserver_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ type GroupRef struct {

// IncomingAuthConfig configures authentication for clients connecting to the Virtual MCP server
type IncomingAuthConfig struct {
// Type defines the authentication type: anonymous, local, or oidc
// +kubebuilder:validation:Enum=anonymous;local;oidc
// +optional
Type string `json:"type,omitempty"`

// OIDCConfig defines OIDC authentication configuration
// Reuses MCPServer OIDC patterns
// +optional
Expand Down Expand Up @@ -426,14 +431,6 @@ type VirtualMCPServerStatus struct {
// +optional
Conditions []metav1.Condition `json:"conditions,omitempty"`

// DiscoveredBackends lists discovered backend configurations when source=discovered
// +optional
DiscoveredBackends []DiscoveredBackend `json:"discoveredBackends,omitempty"`

// Capabilities summarizes aggregated capabilities from all backends
// +optional
Capabilities *CapabilitiesSummary `json:"capabilities,omitempty"`

// ObservedGeneration is the most recent generation observed for this VirtualMCPServer
// +optional
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
Expand All @@ -452,54 +449,6 @@ type VirtualMCPServerStatus struct {
URL string `json:"url,omitempty"`
}

// DiscoveredBackend represents a discovered backend MCPServer
type DiscoveredBackend struct {
// Name is the name of the backend MCPServer
// +kubebuilder:validation:Required
Name string `json:"name"`

// AuthConfigRef is the name of the discovered MCPExternalAuthConfig
// Empty if backend has no external auth config
// +optional
AuthConfigRef string `json:"authConfigRef,omitempty"`

// AuthType is the type of authentication configured
// +optional
AuthType string `json:"authType,omitempty"`

// Status is the current status of the backend
// +kubebuilder:validation:Enum=ready;degraded;unavailable
// +optional
Status string `json:"status,omitempty"`

// LastHealthCheck is the timestamp of the last health check
// +optional
LastHealthCheck *metav1.Time `json:"lastHealthCheck,omitempty"`

// URL is the URL of the backend MCPServer
// +optional
URL string `json:"url,omitempty"`
}

// CapabilitiesSummary summarizes aggregated capabilities
type CapabilitiesSummary struct {
// ToolCount is the total number of tools exposed
// +optional
ToolCount int `json:"toolCount,omitempty"`

// ResourceCount is the total number of resources exposed
// +optional
ResourceCount int `json:"resourceCount,omitempty"`

// PromptCount is the total number of prompts exposed
// +optional
PromptCount int `json:"promptCount,omitempty"`

// CompositeToolCount is the number of composite tools defined
// +optional
CompositeToolCount int `json:"compositeToolCount,omitempty"`
}

// VirtualMCPServerPhase represents the lifecycle phase of a VirtualMCPServer
// +kubebuilder:validation:Enum=Pending;Ready;Degraded;Failed
type VirtualMCPServerPhase string
Expand All @@ -524,36 +473,18 @@ const (
// ConditionTypeVirtualMCPServerReady indicates whether the VirtualMCPServer is ready
ConditionTypeVirtualMCPServerReady = "Ready"

// ConditionTypeBackendsDiscovered indicates whether backends have been discovered
ConditionTypeBackendsDiscovered = "BackendsDiscovered"

// ConditionTypeVirtualMCPServerGroupRefValidated indicates whether the GroupRef is valid
ConditionTypeVirtualMCPServerGroupRefValidated = "GroupRefValidated"
)

// Condition reasons for VirtualMCPServer
const (
// ConditionReasonAllBackendsReady indicates all backends are ready
ConditionReasonAllBackendsReady = "AllBackendsReady"

// ConditionReasonSomeBackendsUnavailable indicates some backends are unavailable
ConditionReasonSomeBackendsUnavailable = "SomeBackendsUnavailable"

// ConditionReasonNoBackends indicates no backends were discovered
ConditionReasonNoBackends = "NoBackends"

// ConditionReasonIncomingAuthValid indicates incoming auth is valid
ConditionReasonIncomingAuthValid = "IncomingAuthValid"

// ConditionReasonIncomingAuthInvalid indicates incoming auth is invalid
ConditionReasonIncomingAuthInvalid = "IncomingAuthInvalid"

// ConditionReasonDiscoveryComplete indicates backend discovery is complete
ConditionReasonDiscoveryComplete = "DiscoveryComplete"

// ConditionReasonDiscoveryFailed indicates backend discovery failed
ConditionReasonDiscoveryFailed = "DiscoveryFailed"

// ConditionReasonGroupRefValid indicates the GroupRef is valid
ConditionReasonVirtualMCPServerGroupRefValid = "GroupRefValid"

Expand Down Expand Up @@ -604,8 +535,6 @@ const (
//+kubebuilder:subresource:status
//+kubebuilder:resource:shortName=vmcp;virtualmcp
//+kubebuilder:printcolumn:name="Phase",type="string",JSONPath=".status.phase",description="The phase of the VirtualMCPServer"
//+kubebuilder:printcolumn:name="Tools",type="integer",JSONPath=".status.capabilities.toolCount",description="Total tools"
//+kubebuilder:printcolumn:name="Backends",type="integer",JSONPath=".status.discoveredBackends[*]",description="Backends"
//+kubebuilder:printcolumn:name="URL",type="string",JSONPath=".status.url",description="Virtual MCP server URL"
//+kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="Age"
//+kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type=='Ready')].status"
Expand Down
210 changes: 2 additions & 208 deletions cmd/thv-operator/api/v1alpha1/virtualmcpserver_types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,62 +92,22 @@ func TestVirtualMCPServerConditions(t *testing.T) {
{
Type: ConditionTypeVirtualMCPServerReady,
Status: metav1.ConditionTrue,
Reason: ConditionReasonAllBackendsReady,
Reason: "DeploymentReady",
},
{
Type: ConditionTypeAuthConfigured,
Status: metav1.ConditionTrue,
Reason: ConditionReasonIncomingAuthValid,
},
{
Type: ConditionTypeBackendsDiscovered,
Status: metav1.ConditionTrue,
Reason: ConditionReasonDiscoveryComplete,
},
},
validate: func(t *testing.T, vmcp *VirtualMCPServer) {
t.Helper()
assert.Len(t, vmcp.Status.Conditions, 3)
assert.Len(t, vmcp.Status.Conditions, 2)
for _, cond := range vmcp.Status.Conditions {
assert.Equal(t, metav1.ConditionTrue, cond.Status)
}
},
},
{
name: "ready_false_with_backend_issues",
conditions: []metav1.Condition{
{
Type: ConditionTypeVirtualMCPServerReady,
Status: metav1.ConditionFalse,
Reason: ConditionReasonSomeBackendsUnavailable,
Message: "2 out of 5 backends unavailable",
},
},
validate: func(t *testing.T, vmcp *VirtualMCPServer) {
t.Helper()
assert.Len(t, vmcp.Status.Conditions, 1)
cond := vmcp.Status.Conditions[0]
assert.Equal(t, metav1.ConditionFalse, cond.Status)
assert.Contains(t, cond.Message, "backends unavailable")
},
},
{
name: "discovery_failed",
conditions: []metav1.Condition{
{
Type: ConditionTypeBackendsDiscovered,
Status: metav1.ConditionFalse,
Reason: ConditionReasonDiscoveryFailed,
Message: "Failed to discover backends from group",
},
},
validate: func(t *testing.T, vmcp *VirtualMCPServer) {
t.Helper()
cond := vmcp.Status.Conditions[0]
assert.Equal(t, ConditionTypeBackendsDiscovered, cond.Type)
assert.Equal(t, metav1.ConditionFalse, cond.Status)
},
},
}

for _, tt := range tests {
Expand All @@ -169,172 +129,6 @@ func TestVirtualMCPServerConditions(t *testing.T) {
}
}

func TestDiscoveredBackendsStatus(t *testing.T) {
t.Parallel()

tests := []struct {
name string
discoveredBackends []DiscoveredBackend
expectedCount int
validate func(*testing.T, []DiscoveredBackend)
}{
{
name: "multiple_backends_all_ready",
discoveredBackends: []DiscoveredBackend{
{
Name: "github",
AuthConfigRef: "github-token-exchange",
AuthType: "token_exchange",
Status: "ready",
URL: "http://github-mcp.default.svc:8080",
},
{
Name: "jira",
AuthConfigRef: "jira-token-exchange",
AuthType: "token_exchange",
Status: "ready",
URL: "http://jira-mcp.default.svc:8080",
},
{
Name: "slack",
AuthType: "service_account",
Status: "ready",
URL: "http://slack-mcp.default.svc:8080",
},
},
expectedCount: 3,
validate: func(t *testing.T, backends []DiscoveredBackend) {
t.Helper()
readyCount := 0
for _, b := range backends {
if b.Status == "ready" {
readyCount++
}
}
assert.Equal(t, 3, readyCount, "All backends should be ready")
},
},
{
name: "mixed_backend_status",
discoveredBackends: []DiscoveredBackend{
{
Name: "github",
AuthConfigRef: "github-token-exchange",
Status: "ready",
},
{
Name: "jira",
AuthConfigRef: "jira-token-exchange",
Status: "degraded",
},
{
Name: "slack",
Status: "unavailable",
},
},
expectedCount: 3,
validate: func(t *testing.T, backends []DiscoveredBackend) {
t.Helper()
statusCounts := make(map[string]int)
for _, b := range backends {
statusCounts[b.Status]++
}
assert.Equal(t, 1, statusCounts["ready"])
assert.Equal(t, 1, statusCounts["degraded"])
assert.Equal(t, 1, statusCounts["unavailable"])
},
},
{
name: "backend_with_no_auth",
discoveredBackends: []DiscoveredBackend{
{
Name: "internal-api",
AuthConfigRef: "", // No auth config
AuthType: "pass_through",
Status: "ready",
},
},
expectedCount: 1,
validate: func(t *testing.T, backends []DiscoveredBackend) {
t.Helper()
assert.Empty(t, backends[0].AuthConfigRef)
assert.Equal(t, "pass_through", backends[0].AuthType)
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

vmcp := &VirtualMCPServer{
Status: VirtualMCPServerStatus{
DiscoveredBackends: tt.discoveredBackends,
},
}

assert.Len(t, vmcp.Status.DiscoveredBackends, tt.expectedCount)
tt.validate(t, vmcp.Status.DiscoveredBackends)
})
}
}

func TestCapabilitiesSummary(t *testing.T) {
t.Parallel()

tests := []struct {
name string
capabilities *CapabilitiesSummary
validate func(*testing.T, *CapabilitiesSummary)
}{
{
name: "full_capabilities",
capabilities: &CapabilitiesSummary{
ToolCount: 25,
ResourceCount: 10,
PromptCount: 5,
CompositeToolCount: 3,
},
validate: func(t *testing.T, caps *CapabilitiesSummary) {
t.Helper()
assert.Equal(t, 25, caps.ToolCount)
assert.Equal(t, 10, caps.ResourceCount)
assert.Equal(t, 5, caps.PromptCount)
assert.Equal(t, 3, caps.CompositeToolCount)
},
},
{
name: "only_tools_no_resources",
capabilities: &CapabilitiesSummary{
ToolCount: 15,
ResourceCount: 0,
PromptCount: 0,
CompositeToolCount: 1,
},
validate: func(t *testing.T, caps *CapabilitiesSummary) {
t.Helper()
assert.Greater(t, caps.ToolCount, 0)
assert.Equal(t, 0, caps.ResourceCount)
assert.Equal(t, 0, caps.PromptCount)
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

vmcp := &VirtualMCPServer{
Status: VirtualMCPServerStatus{
Capabilities: tt.capabilities,
},
}

tt.validate(t, vmcp.Status.Capabilities)
})
}
}

func TestVirtualMCPServerDefaultValues(t *testing.T) {
t.Parallel()

Expand Down
Loading
Loading