Skip to content

Commit 31d531b

Browse files
borisurbanikclaude
andcommitted
Fix route watch cannot map events to APIManager when Zync is disabled
Replace the ownerRef-chain mapper (Route → Zync → APIManager) with a domain-name mapper that lists APIManagers in the namespace and matches by expected tenant route hosts. The ownerRef chain is broken when Zync is disabled, causing noisy not-found errors and stale availability status. The new mapper is indifferent to who created the route — Zync-managed routes and manually-created routes are handled identically, avoiding the need for callers to fake a Zync label just to get status updates. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent fd973da commit 31d531b

7 files changed

Lines changed: 258 additions & 293 deletions

controllers/apps/apimanager_controller.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ import (
4040
subController "github.com/3scale/3scale-operator/controllers/subscription"
4141
"github.com/3scale/3scale-operator/pkg/3scale/amp/component"
4242
"github.com/3scale/3scale-operator/pkg/3scale/amp/operator"
43-
"github.com/3scale/3scale-operator/pkg/handlers"
4443
"github.com/3scale/3scale-operator/pkg/helper"
4544
"github.com/3scale/3scale-operator/pkg/reconcilers"
4645
"github.com/3scale/3scale-operator/version"
@@ -315,10 +314,10 @@ func (r *APIManagerReconciler) SetupWithManager(mgr ctrl.Manager) error {
315314
Namespace: r.WatchedNamespace,
316315
}
317316

318-
handlers := &handlers.APIManagerRoutesEventMapper{
317+
zyncRouteMapper := &ZyncRouteToAPIManagerMapper{
319318
Context: r.Context(),
320319
K8sClient: r.Client(),
321-
Logger: r.Logger().WithName("APIManagerRoutesHandler"),
320+
Logger: r.Logger().WithName("zyncRouteToAPIManagerMapper"),
322321
}
323322

324323
operatorNamespace, err := helper.GetOperatorNamespace()
@@ -341,7 +340,10 @@ func (r *APIManagerReconciler) SetupWithManager(mgr ctrl.Manager) error {
341340
builder.WithPredicates(labelSelectorPredicate),
342341
).
343342
Owns(&k8sappsv1.Deployment{}).
344-
Watches(&routev1.Route{}, handler.EnqueueRequestsFromMapFunc(handlers.Map)).
343+
Watches(
344+
&routev1.Route{},
345+
handler.EnqueueRequestsFromMapFunc(zyncRouteMapper.Map),
346+
).
345347
Watches(
346348
&v1.ConfigMap{
347349
ObjectMeta: apimachinerymetav1.ObjectMeta{

controllers/apps/apimanager_status_reconciler_test.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -174,11 +174,15 @@ func getRequiredSecrets(namespace string) []runtime.Object {
174174
}
175175
}
176176

177-
// makeAdmittedRoute returns a Route with the Admitted condition set to True.
177+
// makeAdmittedRoute returns a Route with the Admitted condition set to True and the Zync created-by label.
178178
func makeAdmittedRoute(name, namespace, host string) *routev1.Route {
179179
return &routev1.Route{
180-
ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace},
181-
Spec: routev1.RouteSpec{Host: host},
180+
ObjectMeta: metav1.ObjectMeta{
181+
Name: name,
182+
Namespace: namespace,
183+
Labels: map[string]string{zyncCreatedByLabel: zyncCreatedByValue},
184+
},
185+
Spec: routev1.RouteSpec{Host: host},
182186
Status: routev1.RouteStatus{
183187
Ingress: []routev1.RouteIngress{
184188
{Conditions: []routev1.RouteIngressCondition{
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package controllers
2+
3+
import (
4+
"context"
5+
6+
"github.com/go-logr/logr"
7+
routev1 "github.com/openshift/api/route/v1"
8+
"k8s.io/apimachinery/pkg/types"
9+
"sigs.k8s.io/controller-runtime/pkg/client"
10+
"sigs.k8s.io/controller-runtime/pkg/reconcile"
11+
12+
appsv1alpha1 "github.com/3scale/3scale-operator/apis/apps/v1alpha1"
13+
)
14+
15+
const zyncCreatedByLabel = "3scale.net/created-by"
16+
const zyncCreatedByValue = "zync"
17+
18+
type ZyncRouteToAPIManagerMapper struct {
19+
Context context.Context
20+
K8sClient client.Client
21+
Logger logr.Logger
22+
}
23+
24+
func (h *ZyncRouteToAPIManagerMapper) Map(ctx context.Context, o client.Object) []reconcile.Request {
25+
route, ok := o.(*routev1.Route)
26+
if !ok {
27+
return nil
28+
}
29+
30+
apimanagerList := &appsv1alpha1.APIManagerList{}
31+
if err := h.K8sClient.List(ctx, apimanagerList, client.InNamespace(o.GetNamespace())); err != nil {
32+
h.Logger.Error(err, "failed to list APIManagers")
33+
return nil
34+
}
35+
36+
var requests []reconcile.Request
37+
for i := range apimanagerList.Items {
38+
if routeHostMatchesAPIManager(route.Spec.Host, &apimanagerList.Items[i]) {
39+
requests = append(requests, reconcile.Request{
40+
NamespacedName: types.NamespacedName{
41+
Name: apimanagerList.Items[i].Name,
42+
Namespace: apimanagerList.Items[i].Namespace,
43+
},
44+
})
45+
}
46+
}
47+
return requests
48+
}
49+
50+
func routeHostMatchesAPIManager(routeHost string, apimanager *appsv1alpha1.APIManager) bool {
51+
for _, expectedHost := range apimanagerExpectedRouteHosts(apimanager) {
52+
if expectedHost == routeHost {
53+
return true
54+
}
55+
}
56+
return false
57+
}
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
package controllers
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/go-logr/logr"
8+
routev1 "github.com/openshift/api/route/v1"
9+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
10+
"k8s.io/apimachinery/pkg/runtime"
11+
"k8s.io/apimachinery/pkg/types"
12+
"k8s.io/client-go/kubernetes/scheme"
13+
"sigs.k8s.io/controller-runtime/pkg/client/fake"
14+
"sigs.k8s.io/controller-runtime/pkg/reconcile"
15+
16+
appsv1alpha1 "github.com/3scale/3scale-operator/apis/apps/v1alpha1"
17+
)
18+
19+
func TestZyncRouteToAPIManagerMapperMap(t *testing.T) {
20+
const namespace = "test-ns"
21+
const otherNamespace = "other-ns"
22+
23+
am := getTestAPIManager(namespace)
24+
am.Name = "main-apimanager"
25+
26+
amOtherNs := getTestAPIManager(otherNamespace)
27+
amOtherNs.Name = "other-apimanager"
28+
29+
s := scheme.Scheme
30+
s.AddKnownTypes(appsv1alpha1.GroupVersion, am, &appsv1alpha1.APIManagerList{})
31+
if err := routev1.Install(s); err != nil {
32+
t.Fatal(err)
33+
}
34+
35+
newRoute := func(host, ns string) *routev1.Route {
36+
return &routev1.Route{
37+
TypeMeta: metav1.TypeMeta{Kind: "Route", APIVersion: "route.openshift.io/v1"},
38+
ObjectMeta: metav1.ObjectMeta{Name: "a-route", Namespace: ns},
39+
Spec: routev1.RouteSpec{Host: host},
40+
}
41+
}
42+
43+
enqueuesMainAM := []reconcile.Request{
44+
{NamespacedName: types.NamespacedName{Name: "main-apimanager", Namespace: namespace}},
45+
}
46+
47+
tests := []struct {
48+
name string
49+
objects []runtime.Object
50+
route *routev1.Route
51+
want []reconcile.Request
52+
}{
53+
{
54+
name: "backend listener route host enqueues APIManager",
55+
objects: []runtime.Object{am},
56+
route: newRoute("backend-3scale.test.example.com", namespace),
57+
want: enqueuesMainAM,
58+
},
59+
{
60+
name: "apicast production route host enqueues APIManager",
61+
objects: []runtime.Object{am},
62+
route: newRoute("api-3scale-apicast-production.test.example.com", namespace),
63+
want: enqueuesMainAM,
64+
},
65+
{
66+
name: "apicast staging route host enqueues APIManager",
67+
objects: []runtime.Object{am},
68+
route: newRoute("api-3scale-apicast-staging.test.example.com", namespace),
69+
want: enqueuesMainAM,
70+
},
71+
{
72+
name: "master portal route host enqueues APIManager",
73+
objects: []runtime.Object{am},
74+
route: newRoute("master.test.example.com", namespace),
75+
want: enqueuesMainAM,
76+
},
77+
{
78+
name: "developer portal route host enqueues APIManager",
79+
objects: []runtime.Object{am},
80+
route: newRoute("3scale.test.example.com", namespace),
81+
want: enqueuesMainAM,
82+
},
83+
{
84+
name: "admin portal route host enqueues APIManager",
85+
objects: []runtime.Object{am},
86+
route: newRoute("3scale-admin.test.example.com", namespace),
87+
want: enqueuesMainAM,
88+
},
89+
{
90+
name: "route host does not match any APIManager — no requests",
91+
objects: []runtime.Object{am},
92+
route: newRoute("unrelated.example.com", namespace),
93+
want: nil,
94+
},
95+
{
96+
name: "no APIManagers in namespace — no requests",
97+
objects: []runtime.Object{},
98+
route: newRoute("backend-3scale.test.example.com", namespace),
99+
want: nil,
100+
},
101+
{
102+
name: "route in different namespace matches only that namespace's APIManager",
103+
objects: []runtime.Object{am, amOtherNs},
104+
route: newRoute("backend-3scale.test.example.com", otherNamespace),
105+
want: []reconcile.Request{
106+
{NamespacedName: types.NamespacedName{Name: "other-apimanager", Namespace: otherNamespace}},
107+
},
108+
},
109+
}
110+
111+
for _, tt := range tests {
112+
t.Run(tt.name, func(t *testing.T) {
113+
cl := fake.NewClientBuilder().WithScheme(s).WithRuntimeObjects(tt.objects...).Build()
114+
mapper := &ZyncRouteToAPIManagerMapper{
115+
Context: context.TODO(),
116+
K8sClient: cl,
117+
Logger: logr.Discard(),
118+
}
119+
120+
got := mapper.Map(context.TODO(), tt.route)
121+
122+
if len(got) != len(tt.want) {
123+
t.Fatalf("Map() returned %d requests, want %d: got %v", len(got), len(tt.want), got)
124+
}
125+
for i := range tt.want {
126+
if got[i] != tt.want[i] {
127+
t.Errorf("Map()[%d] = %v, want %v", i, got[i], tt.want[i])
128+
}
129+
}
130+
})
131+
}
132+
}

pkg/handlers/apimanager_managed_routes_event_mapper.go

Lines changed: 0 additions & 101 deletions
This file was deleted.

0 commit comments

Comments
 (0)