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
19 changes: 17 additions & 2 deletions cmd/thv-operator/api/v1alpha1/mcpoidcconfig_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,20 @@ type InlineOIDCSharedConfig struct {
InsecureAllowHTTP bool `json:"insecureAllowHTTP"`
}

// WorkloadReference identifies a workload that references a shared configuration resource.
// Namespace is implicit — cross-namespace references are not supported.
type WorkloadReference struct {
// Kind is the type of workload resource
// +kubebuilder:validation:Enum=MCPServer;VirtualMCPServer;MCPRemoteProxy
// +kubebuilder:validation:Required
Kind string `json:"kind"`

// Name is the name of the workload resource
// +kubebuilder:validation:Required
// +kubebuilder:validation:MinLength=1
Name string `json:"name"`
}

// MCPOIDCConfigStatus defines the observed state of MCPOIDCConfig
type MCPOIDCConfigStatus struct {
// Conditions represent the latest available observations of the MCPOIDCConfig's state
Expand All @@ -158,9 +172,10 @@ type MCPOIDCConfigStatus struct {
// +optional
ConfigHash string `json:"configHash,omitempty"`

// ReferencingServers is a list of MCPServer resources that reference this MCPOIDCConfig
// ReferencingWorkloads is a list of workload resources that reference this MCPOIDCConfig.
// Each entry identifies the workload by kind and name.
// +optional
ReferencingServers []string `json:"referencingServers,omitempty"`
ReferencingWorkloads []WorkloadReference `json:"referencingWorkloads,omitempty"`
}

// +kubebuilder:object:root=true
Expand Down
21 changes: 18 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.

52 changes: 26 additions & 26 deletions cmd/thv-operator/controllers/mcpoidcconfig_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,12 +124,12 @@ func (r *MCPOIDCConfigReconciler) Reconcile(ctx context.Context, req ctrl.Reques
return ctrl.Result{}, nil
}

// Refresh ReferencingServers list
referencingServers, err := r.findReferencingServers(ctx, oidcConfig)
// Refresh ReferencingWorkloads list
referencingWorkloads, err := r.findReferencingWorkloads(ctx, oidcConfig)
if err != nil {
logger.Error(err, "Failed to find referencing servers")
} else if !slices.Equal(oidcConfig.Status.ReferencingServers, referencingServers) {
oidcConfig.Status.ReferencingServers = referencingServers
logger.Error(err, "Failed to find referencing workloads")
} else if !slices.Equal(oidcConfig.Status.ReferencingWorkloads, referencingWorkloads) {
oidcConfig.Status.ReferencingWorkloads = referencingWorkloads
conditionChanged = true
}

Expand Down Expand Up @@ -160,26 +160,26 @@ func (r *MCPOIDCConfigReconciler) handleDeletion(
logger := log.FromContext(ctx)

if controllerutil.ContainsFinalizer(oidcConfig, OIDCConfigFinalizerName) {
// Check if any MCPServers still reference this config
referencingServers, err := r.findReferencingServers(ctx, oidcConfig)
// Check if any workloads still reference this config
referencingWorkloads, err := r.findReferencingWorkloads(ctx, oidcConfig)
if err != nil {
logger.Error(err, "Failed to check referencing servers during deletion")
logger.Error(err, "Failed to check referencing workloads during deletion")
return ctrl.Result{}, err
}

if len(referencingServers) > 0 {
logger.Info("MCPOIDCConfig is still referenced by MCPServers, blocking deletion",
if len(referencingWorkloads) > 0 {
logger.Info("MCPOIDCConfig is still referenced by workloads, blocking deletion",
"oidcConfig", oidcConfig.Name,
"referencingServers", referencingServers)
"referencingWorkloads", referencingWorkloads)

meta.SetStatusCondition(&oidcConfig.Status.Conditions, metav1.Condition{
Type: "DeletionBlocked",
Status: metav1.ConditionTrue,
Reason: "ReferencedByServers",
Message: fmt.Sprintf("Cannot delete: referenced by MCPServers: %v", referencingServers),
Reason: "ReferencedByWorkloads",
Message: fmt.Sprintf("Cannot delete: referenced by workloads: %v", referencingWorkloads),
ObservedGeneration: oidcConfig.Generation,
})
oidcConfig.Status.ReferencingServers = referencingServers
oidcConfig.Status.ReferencingWorkloads = referencingWorkloads
if updateErr := r.Status().Update(ctx, oidcConfig); updateErr != nil {
logger.Error(updateErr, "Failed to update status during deletion block")
}
Expand All @@ -199,32 +199,32 @@ func (r *MCPOIDCConfigReconciler) handleDeletion(
return ctrl.Result{}, nil
}

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

var refs []string
var refs []mcpv1alpha1.WorkloadReference
for _, server := range mcpServerList.Items {
if server.Spec.OIDCConfigRef != nil && server.Spec.OIDCConfigRef.Name == oidcConfig.Name {
refs = append(refs, server.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 ReferencingServers status.
// Watches MCPServer changes to maintain accurate ReferencingWorkloads status.
func (r *MCPOIDCConfigReconciler) SetupWithManager(mgr ctrl.Manager) error {
// Watch MCPServer changes to update ReferencingServers on referenced MCPOIDCConfigs.
// Watch MCPServer changes to update ReferencingWorkloads on referenced MCPOIDCConfigs.
// This handler enqueues both the currently-referenced MCPOIDCConfig AND any
// MCPOIDCConfig that still lists this server in ReferencingServers (covers the
// MCPOIDCConfig that still lists this server in ReferencingWorkloads (covers the
// case where a server removes its oidcConfigRef — the previously-referenced
// config needs to reconcile and clean up the stale entry).
mcpServerHandler := handler.EnqueueRequestsFromMapFunc(
Expand All @@ -248,7 +248,7 @@ func (r *MCPOIDCConfigReconciler) SetupWithManager(mgr ctrl.Manager) error {
}

// Also enqueue any MCPOIDCConfig that still lists this server in
// ReferencingServers — handles ref-removal and server-deletion cases.
// ReferencingWorkloads — handles ref-removal and server-deletion cases.
oidcConfigList := &mcpv1alpha1.MCPOIDCConfigList{}
if err := r.List(ctx, oidcConfigList, client.InNamespace(server.Namespace)); err != nil {
log.FromContext(ctx).Error(err, "Failed to list MCPOIDCConfigs for MCPServer watch")
Expand All @@ -259,8 +259,8 @@ func (r *MCPOIDCConfigReconciler) SetupWithManager(mgr ctrl.Manager) error {
if _, already := seen[nn]; already {
continue
}
for _, ref := range cfg.Status.ReferencingServers {
if ref == server.Name {
for _, ref := range cfg.Status.ReferencingWorkloads {
if ref.Kind == "MCPServer" && ref.Name == server.Name {
requests = append(requests, reconcile.Request{NamespacedName: nn})
break
}
Expand Down
27 changes: 16 additions & 11 deletions cmd/thv-operator/controllers/mcpserver_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -2085,9 +2085,9 @@ func (r *MCPServerReconciler) handleOIDCConfig(ctx context.Context, m *mcpv1alph
return fmt.Errorf("%s", msg)
}

// Update ReferencingServers on the MCPOIDCConfig status
if err := r.updateOIDCConfigReferencingServers(ctx, oidcConfig, m.Name); err != nil {
ctxLogger.Error(err, "Failed to update MCPOIDCConfig ReferencingServers")
// Update ReferencingWorkloads on the MCPOIDCConfig status
if err := r.updateOIDCConfigReferencingWorkloads(ctx, oidcConfig, m.Name); err != nil {
ctxLogger.Error(err, "Failed to update MCPOIDCConfig ReferencingWorkloads")
// Non-fatal: continue with reconciliation
}

Expand Down Expand Up @@ -2124,24 +2124,29 @@ func setOIDCConfigRefCondition(m *mcpv1alpha1.MCPServer, status metav1.Condition
})
}

// updateOIDCConfigReferencingServers ensures the MCPServer name is listed in
// the MCPOIDCConfig's ReferencingServers status field.
func (r *MCPServerReconciler) updateOIDCConfigReferencingServers(
// updateOIDCConfigReferencingWorkloads ensures the MCPServer is listed in
// the MCPOIDCConfig's ReferencingWorkloads status field.
func (r *MCPServerReconciler) updateOIDCConfigReferencingWorkloads(
ctx context.Context,
oidcConfig *mcpv1alpha1.MCPOIDCConfig,
serverName string,
) error {
ref := mcpv1alpha1.WorkloadReference{
Kind: "MCPServer",
Name: serverName,
}

// Check if already listed
for _, name := range oidcConfig.Status.ReferencingServers {
if name == serverName {
for _, entry := range oidcConfig.Status.ReferencingWorkloads {
if entry.Kind == ref.Kind && entry.Name == ref.Name {
return nil
}
}

// Add the server name
oidcConfig.Status.ReferencingServers = append(oidcConfig.Status.ReferencingServers, serverName)
// Add the workload reference
oidcConfig.Status.ReferencingWorkloads = append(oidcConfig.Status.ReferencingWorkloads, ref)
if err := r.Status().Update(ctx, oidcConfig); err != nil {
return fmt.Errorf("failed to update MCPOIDCConfig ReferencingServers: %w", err)
return fmt.Errorf("failed to update MCPOIDCConfig ReferencingWorkloads: %w", err)
}

return nil
Expand Down
24 changes: 14 additions & 10 deletions cmd/thv-operator/controllers/mcpserver_oidcconfig_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -203,49 +203,53 @@ func TestMCPServerReconciler_handleOIDCConfig(t *testing.T) {
if tt.expectReferencingServer && tt.oidcConfig != nil {
var updated mcpv1alpha1.MCPOIDCConfig
require.NoError(t, fakeClient.Get(ctx, client.ObjectKeyFromObject(tt.oidcConfig), &updated))
assert.Contains(t, updated.Status.ReferencingServers, tt.mcpServer.Name)
expectedRef := mcpv1alpha1.WorkloadReference{Kind: "MCPServer", Name: tt.mcpServer.Name}
assert.Contains(t, updated.Status.ReferencingWorkloads, expectedRef)
}
})
}
}

func TestMCPServerReconciler_updateOIDCConfigReferencingServers(t *testing.T) {
func TestMCPServerReconciler_updateOIDCConfigReferencingWorkloads(t *testing.T) {
t.Parallel()

t.Run("adds new server name", func(t *testing.T) {
existingRef := mcpv1alpha1.WorkloadReference{Kind: "MCPServer", Name: "existing"}

t.Run("adds new server reference", func(t *testing.T) {
t.Parallel()
ctx := t.Context()
scheme := runtime.NewScheme()
require.NoError(t, mcpv1alpha1.AddToScheme(scheme))

cfg := &mcpv1alpha1.MCPOIDCConfig{
ObjectMeta: metav1.ObjectMeta{Name: "cfg", Namespace: "default"},
Status: mcpv1alpha1.MCPOIDCConfigStatus{ReferencingServers: []string{"existing"}},
Status: mcpv1alpha1.MCPOIDCConfigStatus{ReferencingWorkloads: []mcpv1alpha1.WorkloadReference{existingRef}},
}
fc := fake.NewClientBuilder().WithScheme(scheme).WithObjects(cfg).
WithStatusSubresource(&mcpv1alpha1.MCPOIDCConfig{}).Build()
r := newTestMCPServerReconciler(fc, scheme, kubernetes.PlatformKubernetes)

require.NoError(t, r.updateOIDCConfigReferencingServers(ctx, cfg, "new"))
assert.ElementsMatch(t, []string{"existing", "new"}, cfg.Status.ReferencingServers)
require.NoError(t, r.updateOIDCConfigReferencingWorkloads(ctx, cfg, "new"))
newRef := mcpv1alpha1.WorkloadReference{Kind: "MCPServer", Name: "new"}
assert.ElementsMatch(t, []mcpv1alpha1.WorkloadReference{existingRef, newRef}, cfg.Status.ReferencingWorkloads)
})

t.Run("does not duplicate existing name", func(t *testing.T) {
t.Run("does not duplicate existing reference", func(t *testing.T) {
t.Parallel()
ctx := t.Context()
scheme := runtime.NewScheme()
require.NoError(t, mcpv1alpha1.AddToScheme(scheme))

cfg := &mcpv1alpha1.MCPOIDCConfig{
ObjectMeta: metav1.ObjectMeta{Name: "cfg", Namespace: "default"},
Status: mcpv1alpha1.MCPOIDCConfigStatus{ReferencingServers: []string{"existing"}},
Status: mcpv1alpha1.MCPOIDCConfigStatus{ReferencingWorkloads: []mcpv1alpha1.WorkloadReference{existingRef}},
}
fc := fake.NewClientBuilder().WithScheme(scheme).WithObjects(cfg).
WithStatusSubresource(&mcpv1alpha1.MCPOIDCConfig{}).Build()
r := newTestMCPServerReconciler(fc, scheme, kubernetes.PlatformKubernetes)

require.NoError(t, r.updateOIDCConfigReferencingServers(ctx, cfg, "existing"))
assert.Len(t, cfg.Status.ReferencingServers, 1)
require.NoError(t, r.updateOIDCConfigReferencingWorkloads(ctx, cfg, "existing"))
assert.Len(t, cfg.Status.ReferencingWorkloads, 1)
})
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ var _ = Describe("MCPOIDCConfig and MCPServer Cross-Resource Integration Tests",
}, timeout, interval).Should(BeTrue())
})

It("should track MCPServer in MCPOIDCConfig ReferencingServers", func() {
It("should track MCPServer in MCPOIDCConfig ReferencingWorkloads", func() {
Eventually(func() bool {
updated := &mcpv1alpha1.MCPOIDCConfig{}
err := k8sClient.Get(ctx, types.NamespacedName{
Expand All @@ -149,8 +149,9 @@ var _ = Describe("MCPOIDCConfig and MCPServer Cross-Resource Integration Tests",
if err != nil {
return false
}
for _, server := range updated.Status.ReferencingServers {
if server == serverName {
expectedRef := mcpv1alpha1.WorkloadReference{Kind: "MCPServer", Name: serverName}
for _, ref := range updated.Status.ReferencingWorkloads {
if ref == expectedRef {
return true
}
}
Expand All @@ -159,7 +160,7 @@ var _ = Describe("MCPOIDCConfig and MCPServer Cross-Resource Integration Tests",
})
})

Context("When MCPServer is deleted, should clean up ReferencingServers", Ordered, func() {
Context("When MCPServer is deleted, should clean up ReferencingWorkloads", Ordered, func() {
var (
namespace string
configName string
Expand Down Expand Up @@ -228,7 +229,7 @@ var _ = Describe("MCPOIDCConfig and MCPServer Cross-Resource Integration Tests",
}
Expect(k8sClient.Create(ctx, mcpServer)).Should(Succeed())

// Wait for ReferencingServers to contain the server
// Wait for ReferencingWorkloads to contain the server
Eventually(func() bool {
updated := &mcpv1alpha1.MCPOIDCConfig{}
err := k8sClient.Get(ctx, types.NamespacedName{
Expand All @@ -238,8 +239,9 @@ var _ = Describe("MCPOIDCConfig and MCPServer Cross-Resource Integration Tests",
if err != nil {
return false
}
for _, server := range updated.Status.ReferencingServers {
if server == serverName {
expectedRef := mcpv1alpha1.WorkloadReference{Kind: "MCPServer", Name: serverName}
for _, ref := range updated.Status.ReferencingWorkloads {
if ref == expectedRef {
return true
}
}
Expand All @@ -252,11 +254,11 @@ var _ = Describe("MCPOIDCConfig and MCPServer Cross-Resource Integration Tests",
Expect(k8sClient.Delete(ctx, ns)).Should(Succeed())
})

It("should remove server from ReferencingServers after MCPServer deletion", func() {
It("should remove server from ReferencingWorkloads after MCPServer deletion", func() {
// Delete the MCPServer
Expect(k8sClient.Delete(ctx, mcpServer)).Should(Succeed())

// Eventually the referencing servers list should be empty
// Eventually the referencing workloads list should be empty
Eventually(func() bool {
updated := &mcpv1alpha1.MCPOIDCConfig{}
err := k8sClient.Get(ctx, types.NamespacedName{
Expand All @@ -266,7 +268,7 @@ var _ = Describe("MCPOIDCConfig and MCPServer Cross-Resource Integration Tests",
if err != nil {
return false
}
return len(updated.Status.ReferencingServers) == 0
return len(updated.Status.ReferencingWorkloads) == 0
}, timeout, interval).Should(BeTrue())
})
})
Expand Down Expand Up @@ -340,7 +342,7 @@ var _ = Describe("MCPOIDCConfig and MCPServer Cross-Resource Integration Tests",
}
Expect(k8sClient.Create(ctx, mcpServer)).Should(Succeed())

// Wait for ReferencingServers to be populated
// Wait for ReferencingWorkloads to be populated
Eventually(func() bool {
updated := &mcpv1alpha1.MCPOIDCConfig{}
err := k8sClient.Get(ctx, types.NamespacedName{
Expand All @@ -350,8 +352,9 @@ var _ = Describe("MCPOIDCConfig and MCPServer Cross-Resource Integration Tests",
if err != nil {
return false
}
for _, server := range updated.Status.ReferencingServers {
if server == serverName {
expectedRef := mcpv1alpha1.WorkloadReference{Kind: "MCPServer", Name: serverName}
for _, ref := range updated.Status.ReferencingWorkloads {
if ref == expectedRef {
return true
}
}
Expand Down
Loading
Loading