Skip to content

Commit c9a1d85

Browse files
committed
Add namepsace service labels
- Updated RBAC role to include watch permissions for namespaces. - Enhanced the cluster manager to merge namespace annotations into Ingress and HTTPRoute resources.
1 parent ef615c1 commit c9a1d85

File tree

4 files changed

+721
-25
lines changed

4 files changed

+721
-25
lines changed

config/rbac/role.yaml

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,6 @@ rules:
3030
- ""
3131
resources:
3232
- namespaces
33-
verbs:
34-
- get
35-
- list
36-
- apiGroups:
37-
- ""
38-
resources:
3933
- secrets
4034
verbs:
4135
- get

internal/controller/cluster_manager.go

Lines changed: 77 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@ import (
4141
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
4242
)
4343

44+
const (
45+
// Homer annotation prefixes
46+
serviceAnnotationPrefix = "service.homer.rajsingh.info/"
47+
itemAnnotationPrefix = "item.homer.rajsingh.info/"
48+
)
49+
4450
// ClusterClient represents a client connection to a Kubernetes cluster
4551
type ClusterClient struct {
4652
Name string
@@ -378,6 +384,9 @@ func (m *ClusterManager) discoverClusterIngresses(ctx context.Context, cluster *
378384
}
379385
clusterIngresses.Items[i].Annotations["homer.rajsingh.info/cluster"] = cluster.Name
380386

387+
// Merge namespace annotations from the source cluster
388+
m.mergeNamespaceAnnotationsForIngress(ctx, cluster.Client, &clusterIngresses.Items[i])
389+
381390
filtered = append(filtered, clusterIngresses.Items[i])
382391
}
383392
}
@@ -410,6 +419,9 @@ func (m *ClusterManager) discoverClusterIngresses(ctx context.Context, cluster *
410419
}
411420
ingress.Annotations["homer.rajsingh.info/cluster"] = cluster.Name
412421

422+
// Merge namespace annotations from the source cluster
423+
m.mergeNamespaceAnnotationsForIngress(ctx, cluster.Client, ingress)
424+
413425
filtered = append(filtered, *ingress)
414426
}
415427

@@ -529,6 +541,9 @@ func (m *ClusterManager) discoverClusterHTTPRoutes(ctx context.Context, cluster
529541
clusterHTTPRoutes.Items[i].Annotations["homer.rajsingh.info/domain-filters"] = strings.Join(domainFilters, ",")
530542
}
531543

544+
// Merge namespace annotations from the source cluster
545+
m.mergeNamespaceAnnotationsForHTTPRoute(ctx, cluster.Client, &clusterHTTPRoutes.Items[i])
546+
532547
filtered = append(filtered, clusterHTTPRoutes.Items[i])
533548
}
534549
}
@@ -572,7 +587,6 @@ func (m *ClusterManager) shouldIncludeHTTPRoute(ctx context.Context, cluster *Cl
572587
return false, err
573588
}
574589

575-
matchedGateway := false
576590
for _, parentRef := range httproute.Spec.ParentRefs {
577591
if parentRef.Kind != nil && string(*parentRef.Kind) != gatewayKind {
578592
continue
@@ -593,16 +607,12 @@ func (m *ClusterManager) shouldIncludeHTTPRoute(ctx context.Context, cluster *Cl
593607
}
594608

595609
if selector.Matches(labels.Set(gateway.Labels)) {
596-
matchedGateway = true
597610
m.log.V(1).Info("HTTPRoute matched gateway selector", "cluster", cluster.Name, "httproute", httproute.Name, "gateway", parentRef.Name, "namespace", namespace, "labels", gateway.Labels)
598611
return true, nil
599-
} else {
600-
m.log.V(1).Info("Gateway labels did not match selector", "cluster", cluster.Name, "httproute", httproute.Name, "gateway", parentRef.Name, "namespace", namespace, "gatewayLabels", gateway.Labels, "selector", gatewaySelector)
601612
}
613+
m.log.V(1).Info("Gateway labels did not match selector", "cluster", cluster.Name, "httproute", httproute.Name, "gateway", parentRef.Name, "namespace", namespace, "gatewayLabels", gateway.Labels, "selector", gatewaySelector)
602614
}
603-
if !matchedGateway {
604-
m.log.V(1).Info("HTTPRoute did not match any gateway", "cluster", cluster.Name, "httproute", httproute.Name, "parentRefs", len(httproute.Spec.ParentRefs))
605-
}
615+
m.log.V(1).Info("HTTPRoute did not match any gateway", "cluster", cluster.Name, "httproute", httproute.Name, "parentRefs", len(httproute.Spec.ParentRefs))
606616
return false, nil
607617
}
608618

@@ -733,3 +743,63 @@ func matchesDomain(host, filter string) bool {
733743
}
734744
return false
735745
}
746+
747+
// mergeNamespaceAnnotationsForIngress merges namespace annotations into an Ingress resource
748+
// Namespace annotations serve as defaults, resource annotations override
749+
func (m *ClusterManager) mergeNamespaceAnnotationsForIngress(ctx context.Context, clusterClient client.Client, ingress *networkingv1.Ingress) {
750+
// Fetch the namespace from the appropriate cluster
751+
namespace := &corev1.Namespace{}
752+
if err := clusterClient.Get(ctx, client.ObjectKey{Name: ingress.Namespace}, namespace); err != nil {
753+
// If we can't get the namespace, just continue with existing annotations
754+
m.log.V(2).Info("Could not fetch namespace for ingress", "namespace", ingress.Namespace, "ingress", ingress.Name, "error", err)
755+
return
756+
}
757+
758+
// Merge namespace annotations (namespace defaults, resource overrides)
759+
if ingress.Annotations == nil {
760+
ingress.Annotations = make(map[string]string)
761+
}
762+
763+
// First, apply namespace annotations as defaults (only if not already set)
764+
for key, value := range namespace.Annotations {
765+
if len(key) > len(serviceAnnotationPrefix) && key[:len(serviceAnnotationPrefix)] == serviceAnnotationPrefix {
766+
if _, exists := ingress.Annotations[key]; !exists {
767+
ingress.Annotations[key] = value
768+
}
769+
} else if len(key) > len(itemAnnotationPrefix) && key[:len(itemAnnotationPrefix)] == itemAnnotationPrefix {
770+
if _, exists := ingress.Annotations[key]; !exists {
771+
ingress.Annotations[key] = value
772+
}
773+
}
774+
}
775+
}
776+
777+
// mergeNamespaceAnnotationsForHTTPRoute merges namespace annotations into an HTTPRoute resource
778+
// Namespace annotations serve as defaults, resource annotations override
779+
func (m *ClusterManager) mergeNamespaceAnnotationsForHTTPRoute(ctx context.Context, clusterClient client.Client, httproute *gatewayv1.HTTPRoute) {
780+
// Fetch the namespace from the appropriate cluster
781+
namespace := &corev1.Namespace{}
782+
if err := clusterClient.Get(ctx, client.ObjectKey{Name: httproute.Namespace}, namespace); err != nil {
783+
// If we can't get the namespace, just continue with existing annotations
784+
m.log.V(2).Info("Could not fetch namespace for httproute", "namespace", httproute.Namespace, "httproute", httproute.Name, "error", err)
785+
return
786+
}
787+
788+
// Merge namespace annotations (namespace defaults, resource overrides)
789+
if httproute.Annotations == nil {
790+
httproute.Annotations = make(map[string]string)
791+
}
792+
793+
// First, apply namespace annotations as defaults (only if not already set)
794+
for key, value := range namespace.Annotations {
795+
if len(key) > len(serviceAnnotationPrefix) && key[:len(serviceAnnotationPrefix)] == serviceAnnotationPrefix {
796+
if _, exists := httproute.Annotations[key]; !exists {
797+
httproute.Annotations[key] = value
798+
}
799+
} else if len(key) > len(itemAnnotationPrefix) && key[:len(itemAnnotationPrefix)] == itemAnnotationPrefix {
800+
if _, exists := httproute.Annotations[key]; !exists {
801+
httproute.Annotations[key] = value
802+
}
803+
}
804+
}
805+
}

internal/controller/dashboard_controller.go

Lines changed: 99 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ type DashboardReconciler struct {
6161
//+kubebuilder:rbac:groups="",resources=configmaps,verbs=get;list;watch;create;update;patch;delete
6262
//+kubebuilder:rbac:groups="",resources=configmaps/status,verbs=get;update;patch
6363
//+kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch
64-
//+kubebuilder:rbac:groups="",resources=namespaces,verbs=get;list
64+
//+kubebuilder:rbac:groups="",resources=namespaces,verbs=get;list;watch
6565
//+kubebuilder:rbac:groups=authentication.k8s.io,resources=tokenreviews,verbs=create
6666
//+kubebuilder:rbac:groups=authorization.k8s.io,resources=subjectaccessreviews,verbs=create
6767

@@ -582,6 +582,10 @@ func (r *DashboardReconciler) SetupWithManager(mgr ctrl.Manager) error {
582582
builder = builder.Watches(&corev1.Secret{},
583583
handler.EnqueueRequestsFromMapFunc(r.findDashboardsForSecret))
584584

585+
// Watch namespaces for annotation changes
586+
builder = builder.Watches(&corev1.Namespace{},
587+
handler.EnqueueRequestsFromMapFunc(r.findDashboardsForNamespace))
588+
585589
return builder.Complete(r)
586590
}
587591

@@ -760,6 +764,78 @@ func (r *DashboardReconciler) findDashboardsForSecret(ctx context.Context, obj c
760764
return requests
761765
}
762766

767+
// findDashboardsForNamespace finds all dashboards that should be reconciled when a namespace's annotations change
768+
func (r *DashboardReconciler) findDashboardsForNamespace(ctx context.Context, obj client.Object) []ctrl.Request {
769+
namespace, ok := obj.(*corev1.Namespace)
770+
if !ok {
771+
return nil
772+
}
773+
774+
// Check if namespace has any homer annotations
775+
hasHomerAnnotations := false
776+
for key := range namespace.Annotations {
777+
if len(key) > len(serviceAnnotationPrefix) &&
778+
(key[:len(serviceAnnotationPrefix)] == serviceAnnotationPrefix ||
779+
key[:len(itemAnnotationPrefix)] == itemAnnotationPrefix) {
780+
hasHomerAnnotations = true
781+
break
782+
}
783+
}
784+
785+
if !hasHomerAnnotations {
786+
return nil
787+
}
788+
789+
// Find all dashboards and trigger reconciliation
790+
// The reconciliation will pick up resources from this namespace with the new annotations
791+
dashboards := &homerv1alpha1.DashboardList{}
792+
if err := r.List(ctx, dashboards); err != nil {
793+
return nil
794+
}
795+
796+
requests := make([]ctrl.Request, 0, len(dashboards.Items))
797+
for _, dashboard := range dashboards.Items {
798+
requests = append(requests, ctrl.Request{
799+
NamespacedName: client.ObjectKey{
800+
Namespace: dashboard.Namespace,
801+
Name: dashboard.Name,
802+
},
803+
})
804+
}
805+
806+
return requests
807+
}
808+
809+
// mergeNamespaceAnnotations merges namespace annotations with resource annotations
810+
// Namespace annotations serve as defaults, resource annotations override
811+
func (r *DashboardReconciler) mergeNamespaceAnnotations(ctx context.Context, resourceNamespace string, resourceAnnotations map[string]string) map[string]string {
812+
// Fetch the namespace
813+
namespace := &corev1.Namespace{}
814+
if err := r.Get(ctx, client.ObjectKey{Name: resourceNamespace}, namespace); err != nil {
815+
// If we can't get the namespace, just return the resource annotations
816+
return resourceAnnotations
817+
}
818+
819+
// Start with namespace annotations as the base
820+
merged := make(map[string]string)
821+
822+
// First, copy relevant namespace annotations (service.homer.* and item.homer.*)
823+
for key, value := range namespace.Annotations {
824+
if len(key) > len(serviceAnnotationPrefix) && key[:len(serviceAnnotationPrefix)] == serviceAnnotationPrefix {
825+
merged[key] = value
826+
} else if len(key) > len(itemAnnotationPrefix) && key[:len(itemAnnotationPrefix)] == itemAnnotationPrefix {
827+
merged[key] = value
828+
}
829+
}
830+
831+
// Then overlay resource annotations (these override namespace defaults)
832+
for key, value := range resourceAnnotations {
833+
merged[key] = value
834+
}
835+
836+
return merged
837+
}
838+
763839
func (r *DashboardReconciler) shouldIncludeIngress(ctx context.Context, ingress *networkingv1.Ingress, dashboard *homerv1alpha1.Dashboard) (bool, error) {
764840
log := log.FromContext(ctx)
765841

@@ -860,6 +936,17 @@ func (r *DashboardReconciler) createOrUpdateResources(ctx context.Context, resou
860936
}
861937

862938
func (r *DashboardReconciler) createConfigMap(ctx context.Context, homerConfig *homer.HomerConfig, dashboard *homerv1alpha1.Dashboard, filteredIngressList networkingv1.IngressList) (corev1.ConfigMap, error) {
939+
// Merge namespace annotations into Ingress annotations
940+
mergedIngressList := networkingv1.IngressList{
941+
Items: make([]networkingv1.Ingress, len(filteredIngressList.Items)),
942+
}
943+
for i, ingress := range filteredIngressList.Items {
944+
// Create a copy to avoid mutating the original
945+
ingressCopy := ingress.DeepCopy()
946+
ingressCopy.Annotations = r.mergeNamespaceAnnotations(ctx, ingress.Namespace, ingress.Annotations)
947+
mergedIngressList.Items[i] = *ingressCopy
948+
}
949+
863950
if r.EnableGatewayAPI {
864951
filteredHTTPRoutes := []gatewayv1.HTTPRoute{}
865952

@@ -898,21 +985,21 @@ func (r *DashboardReconciler) createConfigMap(ctx context.Context, homerConfig *
898985
}
899986
}
900987

988+
// Merge namespace annotations into HTTPRoute annotations
989+
mergedHTTPRoutes := make([]gatewayv1.HTTPRoute, len(filteredHTTPRoutes))
990+
for i, httproute := range filteredHTTPRoutes {
991+
// Create a copy to avoid mutating the original
992+
httprouteCopy := httproute.DeepCopy()
993+
httprouteCopy.Annotations = r.mergeNamespaceAnnotations(ctx, httproute.Namespace, httproute.Annotations)
994+
mergedHTTPRoutes[i] = *httprouteCopy
995+
}
996+
901997
// Pass domain filters for single-cluster mode (local cluster uses dashboard-level filters)
902998
// Multi-cluster HTTPRoutes already have domain filters stored in annotations
903-
return homer.CreateConfigMapWithHTTPRoutes(homerConfig, dashboard.Name, dashboard.Namespace, filteredIngressList, filteredHTTPRoutes, dashboard, dashboard.Spec.DomainFilters)
999+
return homer.CreateConfigMapWithHTTPRoutes(homerConfig, dashboard.Name, dashboard.Namespace, mergedIngressList, mergedHTTPRoutes, dashboard, dashboard.Spec.DomainFilters)
9041000
}
9051001

906-
return homer.CreateConfigMap(homerConfig, dashboard.Name, dashboard.Namespace, filteredIngressList, dashboard)
907-
}
908-
909-
// getHTTPRouteHosts extracts hostnames from an HTTPRoute
910-
func (r *DashboardReconciler) getHTTPRouteHosts(httproute *gatewayv1.HTTPRoute) []string {
911-
hosts := []string{}
912-
for _, hostname := range httproute.Spec.Hostnames {
913-
hosts = append(hosts, string(hostname))
914-
}
915-
return hosts
1002+
return homer.CreateConfigMap(homerConfig, dashboard.Name, dashboard.Namespace, mergedIngressList, dashboard)
9161003
}
9171004

9181005
// generatePWAManifest generates PWA manifest if enabled, returns empty string if disabled

0 commit comments

Comments
 (0)