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
Original file line number Diff line number Diff line change
Expand Up @@ -712,18 +712,18 @@ type MCPExternalAuthConfigStatus struct {
// +optional
ConfigHash string `json:"configHash,omitempty"`

// ReferencingServers is a list of MCPServer resources that reference this MCPExternalAuthConfig
// This helps track which servers need to be reconciled when this config changes
// ReferencingWorkloads is a list of workload resources that reference this MCPExternalAuthConfig.
// Each entry identifies the workload by kind and name.
// +optional
ReferencingServers []string `json:"referencingServers,omitempty"`
ReferencingWorkloads []WorkloadReference `json:"referencingWorkloads,omitempty"`
}

// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:resource:shortName=extauth;mcpextauth,categories=toolhive
// +kubebuilder:printcolumn:name="Type",type=string,JSONPath=`.spec.type`
// +kubebuilder:printcolumn:name="Ready",type=string,JSONPath=`.status.conditions[?(@.type=='Valid')].status`
// +kubebuilder:printcolumn:name="References",type=string,JSONPath=`.status.referencingServers`
// +kubebuilder:printcolumn:name="References",type=string,JSONPath=`.status.referencingWorkloads`
// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`

// MCPExternalAuthConfig is the Schema for the mcpexternalauthconfigs API.
Expand Down
6 changes: 3 additions & 3 deletions cmd/thv-operator/api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

162 changes: 109 additions & 53 deletions cmd/thv-operator/controllers/mcpexternalauthconfig_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,9 +123,9 @@ func (r *MCPExternalAuthConfigReconciler) Reconcile(ctx context.Context, req ctr
}
}

// Even when hash hasn't changed, update referencing servers list.
// This ensures ReferencingServers is updated when MCPServers are created or deleted.
return r.updateReferencingServers(ctx, externalAuthConfig)
// Even when hash hasn't changed, update referencing workloads list.
// This ensures ReferencingWorkloads is updated when MCPServers are created or deleted.
return r.updateReferencingWorkloads(ctx, externalAuthConfig)
}

// calculateConfigHash calculates a hash of the MCPExternalAuthConfig spec using Kubernetes utilities
Expand Down Expand Up @@ -155,13 +155,27 @@ func (r *MCPExternalAuthConfigReconciler) handleConfigHashChange(
return ctrl.Result{}, fmt.Errorf("failed to find referencing MCPServers: %w", err)
}

// Update the status with the list of referencing servers
serverNames := make([]string, 0, len(referencingServers))
// Update the status with the list of referencing workloads
refs := make([]mcpv1alpha1.WorkloadReference, 0, len(referencingServers))
for _, server := range referencingServers {
serverNames = append(serverNames, server.Name)
refs = append(refs, mcpv1alpha1.WorkloadReference{Kind: "MCPServer", Name: server.Name})
}
slices.Sort(serverNames)
externalAuthConfig.Status.ReferencingServers = serverNames
slices.SortFunc(refs, func(a, b mcpv1alpha1.WorkloadReference) int {
if a.Kind != b.Kind {
if a.Kind < b.Kind {
return -1
}
return 1
}
if a.Name < b.Name {
return -1
}
if a.Name > b.Name {
return 1
}
return 0
})
externalAuthConfig.Status.ReferencingWorkloads = refs

// Update the MCPExternalAuthConfig status
if err := r.Status().Update(ctx, externalAuthConfig); err != nil {
Expand Down Expand Up @@ -197,31 +211,32 @@ func (r *MCPExternalAuthConfigReconciler) handleDeletion(
logger := log.FromContext(ctx)

if controllerutil.ContainsFinalizer(externalAuthConfig, ExternalAuthConfigFinalizerName) {
// Check if any MCPServers are still referencing this MCPExternalAuthConfig
referencingServers, err := r.findReferencingMCPServers(ctx, externalAuthConfig)
// Check if any workloads still reference this MCPExternalAuthConfig
referencingWorkloads, err := r.findReferencingWorkloads(ctx, externalAuthConfig)
if err != nil {
logger.Error(err, "Failed to find referencing MCPServers during deletion")
logger.Error(err, "Failed to check referencing workloads during deletion")
return ctrl.Result{}, err
}

if len(referencingServers) > 0 {
// Cannot delete - still referenced
serverNames := make([]string, 0, len(referencingServers))
for _, server := range referencingServers {
serverNames = append(serverNames, server.Name)
}
logger.Info("Cannot delete MCPExternalAuthConfig - still referenced by MCPServers",
"externalAuthConfig", externalAuthConfig.Name, "referencingServers", serverNames)

// Update status to show it's still referenced
externalAuthConfig.Status.ReferencingServers = serverNames
if err := r.Status().Update(ctx, externalAuthConfig); err != nil {
logger.Error(err, "Failed to update MCPExternalAuthConfig status during deletion")
if len(referencingWorkloads) > 0 {
logger.Info("MCPExternalAuthConfig is still referenced by workloads, blocking deletion",
"externalAuthConfig", externalAuthConfig.Name,
"referencingWorkloads", referencingWorkloads)

meta.SetStatusCondition(&externalAuthConfig.Status.Conditions, metav1.Condition{
Type: "DeletionBlocked",
Status: metav1.ConditionTrue,
Reason: "ReferencedByWorkloads",
Message: fmt.Sprintf("Cannot delete: referenced by workloads: %v", referencingWorkloads),
ObservedGeneration: externalAuthConfig.Generation,
})
externalAuthConfig.Status.ReferencingWorkloads = referencingWorkloads
if updateErr := r.Status().Update(ctx, externalAuthConfig); updateErr != nil {
logger.Error(updateErr, "Failed to update status during deletion block")
}

// Return an error to prevent deletion
return ctrl.Result{}, fmt.Errorf("MCPExternalAuthConfig %s is still referenced by MCPServers: %v",
externalAuthConfig.Name, serverNames)
// Requeue to check again later
return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
}

// No references, safe to remove finalizer and allow deletion
Expand Down Expand Up @@ -250,28 +265,75 @@ func (r *MCPExternalAuthConfigReconciler) findReferencingMCPServers(
})
}

// findReferencingWorkloads returns the workload resources (MCPServer)
// that reference this MCPExternalAuthConfig via their ExternalAuthConfigRef field.
func (r *MCPExternalAuthConfigReconciler) findReferencingWorkloads(
ctx context.Context,
externalAuthConfig *mcpv1alpha1.MCPExternalAuthConfig,
) ([]mcpv1alpha1.WorkloadReference, error) {
mcpServerList := &mcpv1alpha1.MCPServerList{}
if err := r.List(ctx, mcpServerList, client.InNamespace(externalAuthConfig.Namespace)); err != nil {
return nil, fmt.Errorf("failed to list MCPServers: %w", err)
}

var refs []mcpv1alpha1.WorkloadReference
for _, server := range mcpServerList.Items {
if server.Spec.ExternalAuthConfigRef != nil && server.Spec.ExternalAuthConfigRef.Name == externalAuthConfig.Name {
refs = append(refs, mcpv1alpha1.WorkloadReference{Kind: "MCPServer", Name: server.Name})
}
}
return refs, nil
}

// SetupWithManager sets up the controller with the Manager.
// Watches MCPServer changes to maintain accurate ReferencingWorkloads status.
func (r *MCPExternalAuthConfigReconciler) SetupWithManager(mgr ctrl.Manager) error {
Comment thread
ChrisJBurns marked this conversation as resolved.
// Create a handler that maps MCPServer changes to MCPExternalAuthConfig reconciliation requests.
// When an MCPServer is created, updated, or deleted, we reconcile the MCPExternalAuthConfig
// it references so that the ReferencingServers status field stays up to date.
// Watch MCPServer changes to update ReferencingWorkloads on referenced MCPExternalAuthConfigs.
// This handler enqueues both the currently-referenced MCPExternalAuthConfig AND any
// MCPExternalAuthConfig that still lists this server in ReferencingWorkloads (covers the
// case where a server removes its externalAuthConfigRef — the previously-referenced
// config needs to reconcile and clean up the stale entry).
mcpServerHandler := handler.EnqueueRequestsFromMapFunc(
func(_ context.Context, obj client.Object) []reconcile.Request {
mcpServer, ok := obj.(*mcpv1alpha1.MCPServer)
func(ctx context.Context, obj client.Object) []reconcile.Request {
server, ok := obj.(*mcpv1alpha1.MCPServer)
if !ok {
return nil
}

if mcpServer.Spec.ExternalAuthConfigRef == nil {
return nil
seen := make(map[types.NamespacedName]struct{})
var requests []reconcile.Request

// Enqueue the currently-referenced MCPExternalAuthConfig (if any)
if server.Spec.ExternalAuthConfigRef != nil {
nn := types.NamespacedName{
Name: server.Spec.ExternalAuthConfigRef.Name,
Namespace: server.Namespace,
}
seen[nn] = struct{}{}
requests = append(requests, reconcile.Request{NamespacedName: nn})
}

// Also enqueue any MCPExternalAuthConfig that still lists this server in
// ReferencingWorkloads — handles ref-removal and server-deletion cases.
extAuthConfigList := &mcpv1alpha1.MCPExternalAuthConfigList{}
if err := r.List(ctx, extAuthConfigList, client.InNamespace(server.Namespace)); err != nil {
log.FromContext(ctx).Error(err, "Failed to list MCPExternalAuthConfigs for MCPServer watch")
return requests
}
for _, cfg := range extAuthConfigList.Items {
nn := types.NamespacedName{Name: cfg.Name, Namespace: cfg.Namespace}
if _, already := seen[nn]; already {
continue
}
for _, ref := range cfg.Status.ReferencingWorkloads {
if ref.Kind == "MCPServer" && ref.Name == server.Name {
requests = append(requests, reconcile.Request{NamespacedName: nn})
break
}
}
}

return []reconcile.Request{{
NamespacedName: types.NamespacedName{
Name: mcpServer.Spec.ExternalAuthConfigRef.Name,
Namespace: mcpServer.Namespace,
},
}}
return requests
},
)

Expand All @@ -282,26 +344,20 @@ func (r *MCPExternalAuthConfigReconciler) SetupWithManager(mgr ctrl.Manager) err
Complete(r)
}

// updateReferencingServers finds referencing MCPServers and updates the status if the list changed
func (r *MCPExternalAuthConfigReconciler) updateReferencingServers(
// updateReferencingWorkloads finds referencing workloads and updates the status if the list changed
func (r *MCPExternalAuthConfigReconciler) updateReferencingWorkloads(
ctx context.Context,
externalAuthConfig *mcpv1alpha1.MCPExternalAuthConfig,
) (ctrl.Result, error) {
referencingServers, err := r.findReferencingMCPServers(ctx, externalAuthConfig)
refs, err := r.findReferencingWorkloads(ctx, externalAuthConfig)
if err != nil {
logger := log.FromContext(ctx)
logger.Error(err, "Failed to find referencing MCPServers")
return ctrl.Result{}, fmt.Errorf("failed to find referencing MCPServers: %w", err)
}

serverNames := make([]string, 0, len(referencingServers))
for _, server := range referencingServers {
serverNames = append(serverNames, server.Name)
logger.Error(err, "Failed to find referencing workloads")
return ctrl.Result{}, fmt.Errorf("failed to find referencing workloads: %w", err)
}
slices.Sort(serverNames)

if !slices.Equal(externalAuthConfig.Status.ReferencingServers, serverNames) {
externalAuthConfig.Status.ReferencingServers = serverNames
if !slices.Equal(externalAuthConfig.Status.ReferencingWorkloads, refs) {
externalAuthConfig.Status.ReferencingWorkloads = refs
if err := r.Status().Update(ctx, externalAuthConfig); err != nil {
logger := log.FromContext(ctx)
logger.Error(err, "Failed to update MCPExternalAuthConfig status")
Expand Down
Loading
Loading