From 9e12d667ff4802a036545fd9f3cf86c05d6e2901 Mon Sep 17 00:00:00 2001 From: Philipp Kolberg Date: Mon, 6 Apr 2026 18:33:23 +0200 Subject: [PATCH 1/2] feat: auto-discover Cilium API versions --- cmd/main.go | 66 +++- internal/controller/bgpsync_controller.go | 53 ++-- .../controller/bgpsync_controller_test.go | 40 +-- internal/controller/ciliumversions.go | 193 ++++++++++++ internal/controller/ciliumversions_test.go | 287 ++++++++++++++++++ internal/controller/poolsync_controller.go | 48 +-- .../controller/poolsync_controller_test.go | 42 +-- .../cilium.io_ciliumbgpadvertisements.yaml | 63 +++- .../crds/cilium.io_ciliumcidrgroups.yaml | 25 +- .../cilium.io_ciliumloadbalancerippools.yaml | 37 ++- 10 files changed, 749 insertions(+), 105 deletions(-) create mode 100644 internal/controller/ciliumversions.go create mode 100644 internal/controller/ciliumversions_test.go diff --git a/cmd/main.go b/cmd/main.go index 1259898..3025ff0 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -19,6 +19,7 @@ package main import ( "crypto/tls" "flag" + "fmt" "os" // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) @@ -27,6 +28,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/client-go/kubernetes" clientgoscheme "k8s.io/client-go/kubernetes/scheme" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/healthz" @@ -182,6 +184,19 @@ func main() { // Create the receiver factory for prefix acquisition receiverFactory := prefix.NewReceiverFactory() + // Discover available Cilium API versions + restConfig := ctrl.GetConfigOrDie() + clientset, err := kubernetes.NewForConfig(restConfig) + if err != nil { + setupLog.Error(err, "unable to create kubernetes clientset") + os.Exit(1) + } + + ciliumVersions, err := controller.DiscoverCiliumVersions(clientset.Discovery()) + if err != nil { + setupLog.Info("Cilium API not available at startup, will poll in background", "reason", err.Error()) + } + // Set up DynamicPrefix controller with receiver factory dynamicPrefixReconciler := controller.NewDynamicPrefixReconciler( mgr.GetClient(), @@ -193,15 +208,6 @@ func main() { os.Exit(1) } - // Set up PoolSync controller for Cilium resource synchronization - if err := (&controller.PoolSyncReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - }).SetupWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create controller", "controller", "PoolSync") - os.Exit(1) - } - // Set up ServiceSync controller for HA mode Service management if err := (&controller.ServiceSyncReconciler{ Client: mgr.GetClient(), @@ -211,13 +217,41 @@ func main() { os.Exit(1) } - // Set up BGPSync controller for BGP advertisement management - if err := (&controller.BGPSyncReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - }).SetupWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create controller", "controller", "BGPSync") - os.Exit(1) + // setupCiliumControllers registers PoolSync and BGPSync with the manager. + setupCiliumControllers := func(versions *controller.CiliumVersions) error { + if err := (&controller.PoolSyncReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + CiliumVersions: versions, + }).SetupWithManager(mgr); err != nil { + return fmt.Errorf("unable to create PoolSync controller: %w", err) + } + + if err := (&controller.BGPSyncReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + CiliumVersions: versions, + }).SetupWithManager(mgr); err != nil { + return fmt.Errorf("unable to create BGPSync controller: %w", err) + } + + return nil + } + + // Set up Cilium-dependent controllers immediately or defer to background poller. + if ciliumVersions != nil { + if err := setupCiliumControllers(ciliumVersions); err != nil { + setupLog.Error(err, "unable to set up Cilium controllers") + os.Exit(1) + } + } else { + if err := mgr.Add(&controller.CiliumControllerStarter{ + Discovery: clientset.Discovery(), + SetupControllers: setupCiliumControllers, + }); err != nil { + setupLog.Error(err, "unable to add Cilium controller starter") + os.Exit(1) + } } // +kubebuilder:scaffold:builder diff --git a/internal/controller/bgpsync_controller.go b/internal/controller/bgpsync_controller.go index fa42e1e..2faa28a 100644 --- a/internal/controller/bgpsync_controller.go +++ b/internal/controller/bgpsync_controller.go @@ -39,14 +39,12 @@ import ( dynamicprefixiov1alpha1 "github.com/jr42/dynamic-prefix-operator/api/v1alpha1" ) -var ( - // CiliumBGPAdvertisementGVK is the GroupVersionKind for CiliumBGPAdvertisement. - CiliumBGPAdvertisementGVK = schema.GroupVersionKind{ - Group: "cilium.io", - Version: "v2alpha1", - Kind: "CiliumBGPAdvertisement", - } -) +// DefaultCiliumBGPAdvertisementGVK is the default GVK used when CiliumVersions is not injected. +var DefaultCiliumBGPAdvertisementGVK = schema.GroupVersionKind{ + Group: "cilium.io", + Version: "v2", + Kind: "CiliumBGPAdvertisement", +} const ( // LabelManagedBy identifies resources managed by this operator. @@ -63,7 +61,22 @@ const ( // resources for subnets with BGP advertisement enabled. type BGPSyncReconciler struct { client.Client - Scheme *runtime.Scheme + Scheme *runtime.Scheme + CiliumVersions *CiliumVersions +} + +func (r *BGPSyncReconciler) bgpAdvGVK() schema.GroupVersionKind { + if r.CiliumVersions != nil { + return r.CiliumVersions.BGPAdvertisement + } + return DefaultCiliumBGPAdvertisementGVK +} + +func (r *BGPSyncReconciler) lbIPPoolGVK() schema.GroupVersionKind { + if r.CiliumVersions != nil { + return r.CiliumVersions.LoadBalancerIPPool + } + return DefaultCiliumLBIPPoolGVK } // +kubebuilder:rbac:groups=cilium.io,resources=ciliumbgpadvertisements,verbs=get;list;watch;create;update;patch;delete @@ -149,7 +162,7 @@ func (r *BGPSyncReconciler) reconcileAdvertisement( // Create or update the advertisement adv := &unstructured.Unstructured{} - adv.SetGroupVersionKind(CiliumBGPAdvertisementGVK) + adv.SetGroupVersionKind(r.bgpAdvGVK()) adv.SetName(advName) // Check if it exists @@ -162,7 +175,7 @@ func (r *BGPSyncReconciler) reconcileAdvertisement( // Create new advertisement adv = &unstructured.Unstructured{ Object: map[string]interface{}{ - "apiVersion": "cilium.io/v2alpha1", + "apiVersion": APIVersion(r.bgpAdvGVK()), "kind": "CiliumBGPAdvertisement", "metadata": map[string]interface{}{ "name": advName, @@ -224,11 +237,7 @@ func (r *BGPSyncReconciler) getPoolServiceSelector( ) (map[string]interface{}, error) { // List all CiliumLoadBalancerIPPools with matching annotations poolList := &unstructured.UnstructuredList{} - poolList.SetGroupVersionKind(schema.GroupVersionKind{ - Group: "cilium.io", - Version: "v2alpha1", - Kind: "CiliumLoadBalancerIPPoolList", - }) + poolList.SetGroupVersionKind(ListGVK(r.lbIPPoolGVK())) if err := r.List(ctx, poolList); err != nil { return nil, fmt.Errorf("failed to list CiliumLoadBalancerIPPools: %w", err) @@ -299,11 +308,7 @@ func (r *BGPSyncReconciler) deleteOrphanedAdvertisements( // List all advertisements managed by this operator for this DynamicPrefix advList := &unstructured.UnstructuredList{} - advList.SetGroupVersionKind(schema.GroupVersionKind{ - Group: "cilium.io", - Version: "v2alpha1", - Kind: "CiliumBGPAdvertisementList", - }) + advList.SetGroupVersionKind(ListGVK(r.bgpAdvGVK())) if err := r.List(ctx, advList, client.MatchingLabels{ LabelManagedBy: LabelManagedByValue, @@ -391,7 +396,7 @@ func (r *BGPSyncReconciler) buildBGPCondition( for _, subnet := range subnetsWithBGP { advName := r.advertisementName(dp.Name, subnet.Name) adv := &unstructured.Unstructured{} - adv.SetGroupVersionKind(CiliumBGPAdvertisementGVK) + adv.SetGroupVersionKind(r.bgpAdvGVK()) if err := r.Get(ctx, types.NamespacedName{Name: advName}, adv); err != nil { allReady = false break @@ -450,7 +455,7 @@ func (r *BGPSyncReconciler) setCondition(conditions *[]metav1.Condition, conditi func (r *BGPSyncReconciler) SetupWithManager(mgr ctrl.Manager) error { // Watch CiliumBGPAdvertisement for owned resources bgpAdv := &unstructured.Unstructured{} - bgpAdv.SetGroupVersionKind(CiliumBGPAdvertisementGVK) + bgpAdv.SetGroupVersionKind(r.bgpAdvGVK()) return ctrl.NewControllerManagedBy(mgr). Named("bgpsync"). @@ -458,7 +463,7 @@ func (r *BGPSyncReconciler) SetupWithManager(mgr ctrl.Manager) error { Owns(bgpAdv). Watches(&unstructured.Unstructured{ Object: map[string]interface{}{ - "apiVersion": "cilium.io/v2alpha1", + "apiVersion": APIVersion(r.lbIPPoolGVK()), "kind": "CiliumLoadBalancerIPPool", }, }, handler.EnqueueRequestsFromMapFunc(r.findDynamicPrefixForPool), diff --git a/internal/controller/bgpsync_controller_test.go b/internal/controller/bgpsync_controller_test.go index ace4edf..f57be62 100644 --- a/internal/controller/bgpsync_controller_test.go +++ b/internal/controller/bgpsync_controller_test.go @@ -97,7 +97,7 @@ var _ = Describe("BGPSync Controller", func() { // Cleanup CiliumBGPAdvertisement adv := &unstructured.Unstructured{} - adv.SetGroupVersionKind(CiliumBGPAdvertisementGVK) + adv.SetGroupVersionKind(DefaultCiliumBGPAdvertisementGVK) adv.SetName("dp-" + dpName + "-" + subnetName) _ = k8sClient.Delete(ctx, adv) }) @@ -115,7 +115,7 @@ var _ = Describe("BGPSync Controller", func() { // Verify CiliumBGPAdvertisement was created adv := &unstructured.Unstructured{} - adv.SetGroupVersionKind(CiliumBGPAdvertisementGVK) + adv.SetGroupVersionKind(DefaultCiliumBGPAdvertisementGVK) advName := "dp-" + dpName + "-" + subnetName Expect(k8sClient.Get(ctx, types.NamespacedName{Name: advName}, adv)).To(Succeed()) @@ -319,7 +319,7 @@ var _ = Describe("BGPSync Controller", func() { // Verify advertisement exists adv := &unstructured.Unstructured{} - adv.SetGroupVersionKind(CiliumBGPAdvertisementGVK) + adv.SetGroupVersionKind(DefaultCiliumBGPAdvertisementGVK) Expect(k8sClient.Get(ctx, types.NamespacedName{Name: "dp-" + dpName + "-" + subnetName}, adv)).To(Succeed()) }) @@ -329,7 +329,7 @@ var _ = Describe("BGPSync Controller", func() { _ = k8sClient.Delete(ctx, dp) adv := &unstructured.Unstructured{} - adv.SetGroupVersionKind(CiliumBGPAdvertisementGVK) + adv.SetGroupVersionKind(DefaultCiliumBGPAdvertisementGVK) adv.SetName("dp-" + dpName + "-" + subnetName) _ = k8sClient.Delete(ctx, adv) }) @@ -354,7 +354,7 @@ var _ = Describe("BGPSync Controller", func() { // Verify advertisement was deleted adv := &unstructured.Unstructured{} - adv.SetGroupVersionKind(CiliumBGPAdvertisementGVK) + adv.SetGroupVersionKind(DefaultCiliumBGPAdvertisementGVK) err = k8sClient.Get(ctx, types.NamespacedName{Name: "dp-" + dpName + "-" + subnetName}, adv) Expect(err).To(HaveOccurred()) }) @@ -370,8 +370,8 @@ func newTestScheme() *runtime.Scheme { _ = clientgoscheme.AddToScheme(scheme) _ = dynamicprefixiov1alpha1.AddToScheme(scheme) // Register unstructured types for Cilium resources - scheme.AddKnownTypeWithName(CiliumBGPAdvertisementGVK, &unstructured.Unstructured{}) - scheme.AddKnownTypeWithName(CiliumLBIPPoolGVK, &unstructured.Unstructured{}) + scheme.AddKnownTypeWithName(DefaultCiliumBGPAdvertisementGVK, &unstructured.Unstructured{}) + scheme.AddKnownTypeWithName(DefaultCiliumLBIPPoolGVK, &unstructured.Unstructured{}) return scheme } @@ -437,7 +437,7 @@ func TestBGPSyncReconciler_Reconcile_CreateAdvertisement(t *testing.T) { // Verify CiliumBGPAdvertisement was created advName := "dp-test-dp-loadbalancers" adv := &unstructured.Unstructured{} - adv.SetGroupVersionKind(CiliumBGPAdvertisementGVK) + adv.SetGroupVersionKind(DefaultCiliumBGPAdvertisementGVK) err = fakeClient.Get(ctx, types.NamespacedName{Name: advName}, adv) if err != nil { t.Fatalf("Failed to get CiliumBGPAdvertisement: %v", err) @@ -697,7 +697,7 @@ func TestBGPSyncReconciler_Reconcile_DeleteOrphaned(t *testing.T) { // Create an orphaned advertisement (from when BGP was enabled) orphanedAdv := &unstructured.Unstructured{ Object: map[string]interface{}{ - "apiVersion": "cilium.io/v2alpha1", + "apiVersion": "cilium.io/v2", "kind": "CiliumBGPAdvertisement", "metadata": map[string]interface{}{ "name": "dp-test-dp-orphan-was-bgp", @@ -738,7 +738,7 @@ func TestBGPSyncReconciler_Reconcile_DeleteOrphaned(t *testing.T) { // Verify orphaned advertisement was deleted adv := &unstructured.Unstructured{} - adv.SetGroupVersionKind(CiliumBGPAdvertisementGVK) + adv.SetGroupVersionKind(DefaultCiliumBGPAdvertisementGVK) err = fakeClient.Get(ctx, types.NamespacedName{Name: "dp-test-dp-orphan-was-bgp"}, adv) if err == nil { t.Error("Expected orphaned advertisement to be deleted, but it still exists") @@ -789,7 +789,7 @@ func TestBGPSyncReconciler_Reconcile_WithPoolSelector(t *testing.T) { // Create a CiliumLoadBalancerIPPool with serviceSelector pool := &unstructured.Unstructured{ Object: map[string]interface{}{ - "apiVersion": "cilium.io/v2alpha1", + "apiVersion": "cilium.io/v2", "kind": "CiliumLoadBalancerIPPool", "metadata": map[string]interface{}{ "name": "test-pool", @@ -831,7 +831,7 @@ func TestBGPSyncReconciler_Reconcile_WithPoolSelector(t *testing.T) { // Verify CiliumBGPAdvertisement was created with selector advName := "dp-test-dp-selector-with-selector" adv := &unstructured.Unstructured{} - adv.SetGroupVersionKind(CiliumBGPAdvertisementGVK) + adv.SetGroupVersionKind(DefaultCiliumBGPAdvertisementGVK) err = fakeClient.Get(ctx, types.NamespacedName{Name: advName}, adv) if err != nil { t.Fatalf("Failed to get CiliumBGPAdvertisement: %v", err) @@ -1081,22 +1081,22 @@ func TestBuildAdvertisementSpec(t *testing.T) { } } -func TestCiliumBGPAdvertisementGVK(t *testing.T) { +func TestDefaultCiliumBGPAdvertisementGVK(t *testing.T) { // Expected values for GVK verification const ( expectedGroup = "cilium.io" - expectedVersion = "v2alpha1" + expectedVersion = "v2" expectedKind = "CiliumBGPAdvertisement" ) - if CiliumBGPAdvertisementGVK.Group != expectedGroup { - t.Errorf("CiliumBGPAdvertisementGVK.Group = %q, want %q", CiliumBGPAdvertisementGVK.Group, expectedGroup) + if DefaultCiliumBGPAdvertisementGVK.Group != expectedGroup { + t.Errorf("DefaultCiliumBGPAdvertisementGVK.Group = %q, want %q", DefaultCiliumBGPAdvertisementGVK.Group, expectedGroup) } - if CiliumBGPAdvertisementGVK.Version != expectedVersion { - t.Errorf("CiliumBGPAdvertisementGVK.Version = %q, want %q", CiliumBGPAdvertisementGVK.Version, expectedVersion) + if DefaultCiliumBGPAdvertisementGVK.Version != expectedVersion { + t.Errorf("DefaultCiliumBGPAdvertisementGVK.Version = %q, want %q", DefaultCiliumBGPAdvertisementGVK.Version, expectedVersion) } - if CiliumBGPAdvertisementGVK.Kind != expectedKind { - t.Errorf("CiliumBGPAdvertisementGVK.Kind = %q, want %q", CiliumBGPAdvertisementGVK.Kind, expectedKind) + if DefaultCiliumBGPAdvertisementGVK.Kind != expectedKind { + t.Errorf("DefaultCiliumBGPAdvertisementGVK.Kind = %q, want %q", DefaultCiliumBGPAdvertisementGVK.Kind, expectedKind) } } diff --git a/internal/controller/ciliumversions.go b/internal/controller/ciliumversions.go new file mode 100644 index 0000000..8c1b684 --- /dev/null +++ b/internal/controller/ciliumversions.go @@ -0,0 +1,193 @@ +/* +Copyright 2026 jr42. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controller + +import ( + "context" + "fmt" + "time" + + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/discovery" + ctrl "sigs.k8s.io/controller-runtime" +) + +const ciliumAPIGroup = "cilium.io" + +var preferredVersions = []string{"v2", "v2alpha1"} + +// CiliumVersions holds the resolved GroupVersionKind for Cilium resources used by the operator. +type CiliumVersions struct { + LoadBalancerIPPool schema.GroupVersionKind + CIDRGroup schema.GroupVersionKind + BGPAdvertisement schema.GroupVersionKind +} + +// DiscoverCiliumVersions probes the API server and resolves the preferred served version +// for each Cilium resource used by the operator. +func DiscoverCiliumVersions(dc discovery.DiscoveryInterface) (*CiliumVersions, error) { + log := ctrl.Log.WithName("setup") + + available, err := discoverCiliumGroupVersions(dc) + if err != nil { + return nil, fmt.Errorf("failed to discover cilium.io API versions: %w", err) + } + + resourceVersions := discoverCiliumResources(dc, available) + + resolve := func(kind, plural string) (schema.GroupVersionKind, error) { + for _, v := range preferredVersions { + if hasResource(resourceVersions, plural, v) { + gvk := schema.GroupVersionKind{Group: ciliumAPIGroup, Version: v, Kind: kind} + log.Info("Resolved Cilium API version", "kind", kind, "version", v) + return gvk, nil + } + } + + return schema.GroupVersionKind{}, fmt.Errorf("no supported API version found for %s/%s (checked %v)", ciliumAPIGroup, kind, preferredVersions) + } + + lbPool, err := resolve("CiliumLoadBalancerIPPool", "ciliumloadbalancerippools") + if err != nil { + return nil, err + } + cidrGroup, err := resolve("CiliumCIDRGroup", "ciliumcidrgroups") + if err != nil { + return nil, err + } + bgpAdv, err := resolve("CiliumBGPAdvertisement", "ciliumbgpadvertisements") + if err != nil { + return nil, err + } + + return &CiliumVersions{ + LoadBalancerIPPool: lbPool, + CIDRGroup: cidrGroup, + BGPAdvertisement: bgpAdv, + }, nil +} + +// ListGVK returns the list variant of a given GVK. +func ListGVK(gvk schema.GroupVersionKind) schema.GroupVersionKind { + return schema.GroupVersionKind{ + Group: gvk.Group, + Version: gvk.Version, + Kind: gvk.Kind + "List", + } +} + +// APIVersion returns the apiVersion string for a GVK. +func APIVersion(gvk schema.GroupVersionKind) string { + return gvk.Group + "/" + gvk.Version +} + +func discoverCiliumGroupVersions(dc discovery.DiscoveryInterface) ([]string, error) { + groups, err := dc.ServerGroups() + if err != nil { + return nil, fmt.Errorf("failed to fetch API groups: %w", err) + } + + for _, g := range groups.Groups { + if g.Name == ciliumAPIGroup { + versions := make([]string, 0, len(g.Versions)) + for _, v := range g.Versions { + versions = append(versions, v.Version) + } + return versions, nil + } + } + + return nil, fmt.Errorf("%s API group not found on the cluster — is Cilium installed?", ciliumAPIGroup) +} + +type resourceSet map[string]map[string]bool + +func discoverCiliumResources(dc discovery.DiscoveryInterface, versions []string) resourceSet { + rs := make(resourceSet) + + for _, v := range versions { + resources, err := dc.ServerResourcesForGroupVersion(ciliumAPIGroup + "/" + v) + if err != nil { + continue + } + + for _, r := range resources.APIResources { + if _, ok := rs[r.Name]; !ok { + rs[r.Name] = make(map[string]bool) + } + rs[r.Name][v] = true + } + } + + return rs +} + +func hasResource(rs resourceSet, plural, version string) bool { + if versions, ok := rs[plural]; ok { + return versions[version] + } + return false +} + +// CiliumControllerStarter polls for Cilium API availability and registers Cilium-dependent controllers once available. +type CiliumControllerStarter struct { + Discovery discovery.DiscoveryInterface + PollInterval time.Duration + SetupControllers func(versions *CiliumVersions) error +} + +// Start implements manager.Runnable. +func (s *CiliumControllerStarter) Start(ctx context.Context) error { + log := ctrl.Log.WithName("setup") + + interval := s.PollInterval + if interval == 0 { + interval = 30 * time.Second + } + + ticker := time.NewTicker(interval) + defer ticker.Stop() + + log.Info("Waiting for Cilium APIs to become available", "pollInterval", interval) + + for { + select { + case <-ctx.Done(): + log.Info("Stopping Cilium API discovery (context cancelled)") + return nil + case <-ticker.C: + versions, err := DiscoverCiliumVersions(s.Discovery) + if err != nil { + log.V(1).Info("Cilium API not yet available, will retry", "reason", err.Error()) + continue + } + + log.Info("Cilium APIs detected, registering Cilium-dependent controllers") + if err := s.SetupControllers(versions); err != nil { + return fmt.Errorf("failed to register Cilium controllers: %w", err) + } + + log.Info("Cilium-dependent controllers registered successfully") + return nil + } + } +} + +// NeedLeaderElection implements manager.LeaderElectionRunnable. +func (s *CiliumControllerStarter) NeedLeaderElection() bool { + return true +} diff --git a/internal/controller/ciliumversions_test.go b/internal/controller/ciliumversions_test.go new file mode 100644 index 0000000..b210461 --- /dev/null +++ b/internal/controller/ciliumversions_test.go @@ -0,0 +1,287 @@ +/* +Copyright 2026 jr42. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controller + +import ( + "context" + "fmt" + "sync/atomic" + "testing" + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/discovery/fake" + coretesting "k8s.io/client-go/testing" +) + +func TestDiscoverCiliumVersions_PrefersV2(t *testing.T) { + dc := &fake.FakeDiscovery{Fake: &coretesting.Fake{}} + dc.Resources = []*metav1.APIResourceList{ + {GroupVersion: "cilium.io/v2", APIResources: []metav1.APIResource{{Name: "ciliumloadbalancerippools", Kind: "CiliumLoadBalancerIPPool"}, {Name: "ciliumcidrgroups", Kind: "CiliumCIDRGroup"}, {Name: "ciliumbgpadvertisements", Kind: "CiliumBGPAdvertisement"}}}, + {GroupVersion: "cilium.io/v2alpha1", APIResources: []metav1.APIResource{{Name: "ciliumloadbalancerippools", Kind: "CiliumLoadBalancerIPPool"}, {Name: "ciliumcidrgroups", Kind: "CiliumCIDRGroup"}, {Name: "ciliumbgpadvertisements", Kind: "CiliumBGPAdvertisement"}}}, + } + + versions, err := DiscoverCiliumVersions(dc) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + assertGVK(t, versions.LoadBalancerIPPool, "v2", "CiliumLoadBalancerIPPool") + assertGVK(t, versions.CIDRGroup, "v2", "CiliumCIDRGroup") + assertGVK(t, versions.BGPAdvertisement, "v2", "CiliumBGPAdvertisement") +} + +func TestDiscoverCiliumVersions_FallsBackToV2Alpha1(t *testing.T) { + dc := &fake.FakeDiscovery{Fake: &coretesting.Fake{}} + dc.Resources = []*metav1.APIResourceList{ + {GroupVersion: "cilium.io/v2alpha1", APIResources: []metav1.APIResource{{Name: "ciliumloadbalancerippools", Kind: "CiliumLoadBalancerIPPool"}, {Name: "ciliumcidrgroups", Kind: "CiliumCIDRGroup"}, {Name: "ciliumbgpadvertisements", Kind: "CiliumBGPAdvertisement"}}}, + } + + versions, err := DiscoverCiliumVersions(dc) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + assertGVK(t, versions.LoadBalancerIPPool, "v2alpha1", "CiliumLoadBalancerIPPool") + assertGVK(t, versions.CIDRGroup, "v2alpha1", "CiliumCIDRGroup") + assertGVK(t, versions.BGPAdvertisement, "v2alpha1", "CiliumBGPAdvertisement") +} + +func TestDiscoverCiliumVersions_MixedVersions(t *testing.T) { + dc := &fake.FakeDiscovery{Fake: &coretesting.Fake{}} + dc.Resources = []*metav1.APIResourceList{ + {GroupVersion: "cilium.io/v2", APIResources: []metav1.APIResource{{Name: "ciliumloadbalancerippools", Kind: "CiliumLoadBalancerIPPool"}}}, + {GroupVersion: "cilium.io/v2alpha1", APIResources: []metav1.APIResource{{Name: "ciliumloadbalancerippools", Kind: "CiliumLoadBalancerIPPool"}, {Name: "ciliumcidrgroups", Kind: "CiliumCIDRGroup"}, {Name: "ciliumbgpadvertisements", Kind: "CiliumBGPAdvertisement"}}}, + } + + versions, err := DiscoverCiliumVersions(dc) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + assertGVK(t, versions.LoadBalancerIPPool, "v2", "CiliumLoadBalancerIPPool") + assertGVK(t, versions.CIDRGroup, "v2alpha1", "CiliumCIDRGroup") + assertGVK(t, versions.BGPAdvertisement, "v2alpha1", "CiliumBGPAdvertisement") +} + +func TestDiscoverCiliumVersions_NoCilium(t *testing.T) { + dc := &fake.FakeDiscovery{Fake: &coretesting.Fake{}} + dc.Resources = []*metav1.APIResourceList{{GroupVersion: "apps/v1", APIResources: []metav1.APIResource{{Name: "deployments", Kind: "Deployment"}}}} + + _, err := DiscoverCiliumVersions(dc) + if err == nil { + t.Fatal("expected error when cilium.io not found, got nil") + } +} + +func TestDiscoverCiliumVersions_MissingResource(t *testing.T) { + dc := &fake.FakeDiscovery{Fake: &coretesting.Fake{}} + dc.Resources = []*metav1.APIResourceList{{GroupVersion: "cilium.io/v2", APIResources: []metav1.APIResource{{Name: "ciliumloadbalancerippools", Kind: "CiliumLoadBalancerIPPool"}}}} + + _, err := DiscoverCiliumVersions(dc) + if err == nil { + t.Fatal("expected error when required resource is missing, got nil") + } +} + +func TestListGVK(t *testing.T) { + gvk := schema.GroupVersionKind{Group: ciliumAPIGroup, Version: "v2", Kind: "CiliumLoadBalancerIPPool"} + listGVK := ListGVK(gvk) + + if listGVK.Kind != "CiliumLoadBalancerIPPoolList" { + t.Errorf("ListGVK().Kind = %q, want %q", listGVK.Kind, "CiliumLoadBalancerIPPoolList") + } + if listGVK.Group != ciliumAPIGroup { + t.Errorf("ListGVK().Group = %q, want %q", listGVK.Group, ciliumAPIGroup) + } + if listGVK.Version != "v2" { + t.Errorf("ListGVK().Version = %q, want %q", listGVK.Version, "v2") + } +} + +func TestAPIVersion(t *testing.T) { + tests := []struct { + name string + gvk schema.GroupVersionKind + expected string + }{ + {name: "v2", gvk: schema.GroupVersionKind{Group: ciliumAPIGroup, Version: "v2", Kind: "CiliumLoadBalancerIPPool"}, expected: ciliumAPIGroup + "/v2"}, + {name: "v2alpha1", gvk: schema.GroupVersionKind{Group: ciliumAPIGroup, Version: "v2alpha1", Kind: "CiliumBGPAdvertisement"}, expected: ciliumAPIGroup + "/v2alpha1"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := APIVersion(tt.gvk) + if result != tt.expected { + t.Errorf("APIVersion() = %q, want %q", result, tt.expected) + } + }) + } +} + +func assertGVK(t *testing.T, gvk schema.GroupVersionKind, version, kind string) { + t.Helper() + if gvk.Group != ciliumAPIGroup { + t.Errorf("GVK.Group = %q, want %q", gvk.Group, ciliumAPIGroup) + } + if gvk.Version != version { + t.Errorf("GVK.Version = %q, want %q (for %s)", gvk.Version, version, kind) + } + if gvk.Kind != kind { + t.Errorf("GVK.Kind = %q, want %q", gvk.Kind, kind) + } +} + +func ciliumResources() []*metav1.APIResourceList { + return []*metav1.APIResourceList{{ + GroupVersion: "cilium.io/v2", + APIResources: []metav1.APIResource{{Name: "ciliumloadbalancerippools", Kind: "CiliumLoadBalancerIPPool"}, {Name: "ciliumcidrgroups", Kind: "CiliumCIDRGroup"}, {Name: "ciliumbgpadvertisements", Kind: "CiliumBGPAdvertisement"}}, + }} +} + +func TestCiliumControllerStarter_DetectsCiliumImmediately(t *testing.T) { + dc := &fake.FakeDiscovery{Fake: &coretesting.Fake{}} + dc.Resources = ciliumResources() + + var called atomic.Bool + starter := &CiliumControllerStarter{ + Discovery: dc, + PollInterval: 10 * time.Millisecond, + SetupControllers: func(versions *CiliumVersions) error { + called.Store(true) + if versions.LoadBalancerIPPool.Version != "v2" { + t.Errorf("expected v2, got %s", versions.LoadBalancerIPPool.Version) + } + return nil + }, + } + + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + + if err := starter.Start(ctx); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !called.Load() { + t.Error("SetupControllers was not called") + } +} + +func TestCiliumControllerStarter_WaitsForCilium(t *testing.T) { + dc := &fake.FakeDiscovery{Fake: &coretesting.Fake{}} + dc.Resources = []*metav1.APIResourceList{{GroupVersion: "apps/v1", APIResources: []metav1.APIResource{{Name: "deployments", Kind: "Deployment"}}}} + + var called atomic.Bool + var pollCount atomic.Int32 + starter := &CiliumControllerStarter{ + Discovery: dc, + PollInterval: 10 * time.Millisecond, + SetupControllers: func(versions *CiliumVersions) error { + called.Store(true) + return nil + }, + } + + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + + go func() { + for pollCount.Load() < 3 { + time.Sleep(5 * time.Millisecond) + pollCount.Add(1) + } + dc.Resources = ciliumResources() + }() + + if err := starter.Start(ctx); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !called.Load() { + t.Error("SetupControllers was not called after Cilium became available") + } +} + +func TestCiliumControllerStarter_StopsOnContextCancel(t *testing.T) { + dc := &fake.FakeDiscovery{Fake: &coretesting.Fake{}} + dc.Resources = []*metav1.APIResourceList{} + + var called atomic.Bool + starter := &CiliumControllerStarter{ + Discovery: dc, + PollInterval: 10 * time.Millisecond, + SetupControllers: func(versions *CiliumVersions) error { + called.Store(true) + return nil + }, + } + + ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) + defer cancel() + + if err := starter.Start(ctx); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if called.Load() { + t.Error("SetupControllers should not have been called") + } +} + +func TestCiliumControllerStarter_PropagatesSetupError(t *testing.T) { + dc := &fake.FakeDiscovery{Fake: &coretesting.Fake{}} + dc.Resources = ciliumResources() + + setupErr := fmt.Errorf("controller setup failed") + starter := &CiliumControllerStarter{ + Discovery: dc, + PollInterval: 10 * time.Millisecond, + SetupControllers: func(versions *CiliumVersions) error { + return setupErr + }, + } + + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + + err := starter.Start(ctx) + if err == nil { + t.Fatal("expected error, got nil") + } + if !contains(err.Error(), "controller setup failed") { + t.Errorf("error %q should contain 'controller setup failed'", err.Error()) + } +} + +func TestCiliumControllerStarter_DefaultPollInterval(t *testing.T) { + starter := &CiliumControllerStarter{} + if !starter.NeedLeaderElection() { + t.Error("NeedLeaderElection() should return true") + } +} + +func contains(s, substr string) bool { + return len(s) >= len(substr) && searchString(s, substr) +} + +func searchString(s, substr string) bool { + for i := 0; i <= len(s)-len(substr); i++ { + if s[i:i+len(substr)] == substr { + return true + } + } + return false +} diff --git a/internal/controller/poolsync_controller.go b/internal/controller/poolsync_controller.go index 733ebdb..a0be922 100644 --- a/internal/controller/poolsync_controller.go +++ b/internal/controller/poolsync_controller.go @@ -52,17 +52,16 @@ const ( ) var ( - // CiliumLBIPPoolGVK is the GroupVersionKind for CiliumLoadBalancerIPPool. - CiliumLBIPPoolGVK = schema.GroupVersionKind{ + // Default GVKs used when CiliumVersions is not injected (for example in tests). + DefaultCiliumLBIPPoolGVK = schema.GroupVersionKind{ Group: "cilium.io", - Version: "v2alpha1", + Version: "v2", Kind: "CiliumLoadBalancerIPPool", } - // CiliumCIDRGroupGVK is the GroupVersionKind for CiliumCIDRGroup. - CiliumCIDRGroupGVK = schema.GroupVersionKind{ + DefaultCiliumCIDRGroupGVK = schema.GroupVersionKind{ Group: "cilium.io", - Version: "v2alpha1", + Version: "v2", Kind: "CiliumCIDRGroup", } ) @@ -82,7 +81,22 @@ type poolConfiguration struct { // PoolSyncReconciler reconciles Cilium pool resources annotated with dynamic-prefix.io annotations. type PoolSyncReconciler struct { client.Client - Scheme *runtime.Scheme + Scheme *runtime.Scheme + CiliumVersions *CiliumVersions +} + +func (r *PoolSyncReconciler) lbIPPoolGVK() schema.GroupVersionKind { + if r.CiliumVersions != nil { + return r.CiliumVersions.LoadBalancerIPPool + } + return DefaultCiliumLBIPPoolGVK +} + +func (r *PoolSyncReconciler) cidrGroupGVK() schema.GroupVersionKind { + if r.CiliumVersions != nil { + return r.CiliumVersions.CIDRGroup + } + return DefaultCiliumCIDRGroupGVK } // +kubebuilder:rbac:groups=cilium.io,resources=ciliumloadbalancerippools,verbs=get;list;watch;update;patch @@ -95,12 +109,12 @@ func (r *PoolSyncReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c // Determine resource type from request // Try to fetch as CiliumLoadBalancerIPPool first pool := &unstructured.Unstructured{} - pool.SetGroupVersionKind(CiliumLBIPPoolGVK) + pool.SetGroupVersionKind(r.lbIPPoolGVK()) if err := r.Get(ctx, req.NamespacedName, pool); err != nil { // Try CiliumCIDRGroup pool = &unstructured.Unstructured{} - pool.SetGroupVersionKind(CiliumCIDRGroupGVK) + pool.SetGroupVersionKind(r.cidrGroupGVK()) if err := r.Get(ctx, req.NamespacedName, pool); err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) } @@ -633,11 +647,11 @@ func (r *PoolSyncReconciler) SetupWithManager(mgr ctrl.Manager) error { // Watch CiliumLoadBalancerIPPool lbIPPool := &unstructured.Unstructured{} - lbIPPool.SetGroupVersionKind(CiliumLBIPPoolGVK) + lbIPPool.SetGroupVersionKind(r.lbIPPoolGVK()) // Watch CiliumCIDRGroup cidrGroup := &unstructured.Unstructured{} - cidrGroup.SetGroupVersionKind(CiliumCIDRGroupGVK) + cidrGroup.SetGroupVersionKind(r.cidrGroupGVK()) // Build controller controllerBuilder := ctrl.NewControllerManagedBy(mgr). @@ -673,11 +687,7 @@ func (r *PoolSyncReconciler) findReferencingPools(ctx context.Context, obj clien // List CiliumLoadBalancerIPPools lbPoolList := &unstructured.UnstructuredList{} - lbPoolList.SetGroupVersionKind(schema.GroupVersionKind{ - Group: "cilium.io", - Version: "v2alpha1", - Kind: "CiliumLoadBalancerIPPoolList", - }) + lbPoolList.SetGroupVersionKind(ListGVK(r.lbIPPoolGVK())) if err := r.List(ctx, lbPoolList); err == nil { for _, pool := range lbPoolList.Items { @@ -698,11 +708,7 @@ func (r *PoolSyncReconciler) findReferencingPools(ctx context.Context, obj clien // List CiliumCIDRGroups cidrGroupList := &unstructured.UnstructuredList{} - cidrGroupList.SetGroupVersionKind(schema.GroupVersionKind{ - Group: "cilium.io", - Version: "v2alpha1", - Kind: "CiliumCIDRGroupList", - }) + cidrGroupList.SetGroupVersionKind(ListGVK(r.cidrGroupGVK())) if err := r.List(ctx, cidrGroupList); err == nil { for _, group := range cidrGroupList.Items { diff --git a/internal/controller/poolsync_controller_test.go b/internal/controller/poolsync_controller_test.go index 48fd5c5..199ad05 100644 --- a/internal/controller/poolsync_controller_test.go +++ b/internal/controller/poolsync_controller_test.go @@ -81,7 +81,7 @@ var _ = Describe("PoolSync Controller", func() { // Create CiliumLoadBalancerIPPool pool := &unstructured.Unstructured{} - pool.SetGroupVersionKind(CiliumLBIPPoolGVK) + pool.SetGroupVersionKind(DefaultCiliumLBIPPoolGVK) pool.SetName(poolName) pool.SetAnnotations(map[string]string{ AnnotationName: dpName, @@ -96,7 +96,7 @@ var _ = Describe("PoolSync Controller", func() { AfterEach(func() { // Cleanup pool := &unstructured.Unstructured{} - pool.SetGroupVersionKind(CiliumLBIPPoolGVK) + pool.SetGroupVersionKind(DefaultCiliumLBIPPoolGVK) pool.SetName(poolName) _ = k8sClient.Delete(ctx, pool) @@ -118,7 +118,7 @@ var _ = Describe("PoolSync Controller", func() { // Fetch updated pool pool := &unstructured.Unstructured{} - pool.SetGroupVersionKind(CiliumLBIPPoolGVK) + pool.SetGroupVersionKind(DefaultCiliumLBIPPoolGVK) Expect(k8sClient.Get(ctx, types.NamespacedName{Name: poolName}, pool)).To(Succeed()) // Check spec.blocks @@ -185,7 +185,7 @@ var _ = Describe("PoolSync Controller", func() { // Create CiliumCIDRGroup group := &unstructured.Unstructured{} - group.SetGroupVersionKind(CiliumCIDRGroupGVK) + group.SetGroupVersionKind(DefaultCiliumCIDRGroupGVK) group.SetName(groupName) group.SetAnnotations(map[string]string{ AnnotationName: dpName, @@ -200,7 +200,7 @@ var _ = Describe("PoolSync Controller", func() { AfterEach(func() { // Cleanup group := &unstructured.Unstructured{} - group.SetGroupVersionKind(CiliumCIDRGroupGVK) + group.SetGroupVersionKind(DefaultCiliumCIDRGroupGVK) group.SetName(groupName) _ = k8sClient.Delete(ctx, group) @@ -222,7 +222,7 @@ var _ = Describe("PoolSync Controller", func() { // Fetch updated group group := &unstructured.Unstructured{} - group.SetGroupVersionKind(CiliumCIDRGroupGVK) + group.SetGroupVersionKind(DefaultCiliumCIDRGroupGVK) Expect(k8sClient.Get(ctx, types.NamespacedName{Name: groupName}, group)).To(Succeed()) // Check spec.externalCIDRs @@ -272,7 +272,7 @@ var _ = Describe("PoolSync Controller", func() { // Create CiliumLoadBalancerIPPool without subnet annotation pool := &unstructured.Unstructured{} - pool.SetGroupVersionKind(CiliumLBIPPoolGVK) + pool.SetGroupVersionKind(DefaultCiliumLBIPPoolGVK) pool.SetName(poolName) pool.SetAnnotations(map[string]string{ AnnotationName: dpName, @@ -285,7 +285,7 @@ var _ = Describe("PoolSync Controller", func() { AfterEach(func() { pool := &unstructured.Unstructured{} - pool.SetGroupVersionKind(CiliumLBIPPoolGVK) + pool.SetGroupVersionKind(DefaultCiliumLBIPPoolGVK) pool.SetName(poolName) _ = k8sClient.Delete(ctx, pool) @@ -307,7 +307,7 @@ var _ = Describe("PoolSync Controller", func() { // Fetch updated pool pool := &unstructured.Unstructured{} - pool.SetGroupVersionKind(CiliumLBIPPoolGVK) + pool.SetGroupVersionKind(DefaultCiliumLBIPPoolGVK) Expect(k8sClient.Get(ctx, types.NamespacedName{Name: poolName}, pool)).To(Succeed()) // Check spec.blocks uses main prefix @@ -355,24 +355,24 @@ func TestAnnotationConstants(t *testing.T) { } func TestGVKConstants(t *testing.T) { - if CiliumLBIPPoolGVK.Group != "cilium.io" { - t.Errorf("CiliumLBIPPoolGVK.Group = %q, want %q", CiliumLBIPPoolGVK.Group, "cilium.io") + if DefaultCiliumLBIPPoolGVK.Group != "cilium.io" { + t.Errorf("DefaultCiliumLBIPPoolGVK.Group = %q, want %q", DefaultCiliumLBIPPoolGVK.Group, "cilium.io") } - if CiliumLBIPPoolGVK.Version != "v2alpha1" { - t.Errorf("CiliumLBIPPoolGVK.Version = %q, want %q", CiliumLBIPPoolGVK.Version, "v2alpha1") + if DefaultCiliumLBIPPoolGVK.Version != "v2" { + t.Errorf("DefaultCiliumLBIPPoolGVK.Version = %q, want %q", DefaultCiliumLBIPPoolGVK.Version, "v2") } - if CiliumLBIPPoolGVK.Kind != "CiliumLoadBalancerIPPool" { - t.Errorf("CiliumLBIPPoolGVK.Kind = %q, want %q", CiliumLBIPPoolGVK.Kind, "CiliumLoadBalancerIPPool") + if DefaultCiliumLBIPPoolGVK.Kind != "CiliumLoadBalancerIPPool" { + t.Errorf("DefaultCiliumLBIPPoolGVK.Kind = %q, want %q", DefaultCiliumLBIPPoolGVK.Kind, "CiliumLoadBalancerIPPool") } - if CiliumCIDRGroupGVK.Group != "cilium.io" { - t.Errorf("CiliumCIDRGroupGVK.Group = %q, want %q", CiliumCIDRGroupGVK.Group, "cilium.io") + if DefaultCiliumCIDRGroupGVK.Group != "cilium.io" { + t.Errorf("DefaultCiliumCIDRGroupGVK.Group = %q, want %q", DefaultCiliumCIDRGroupGVK.Group, "cilium.io") } - if CiliumCIDRGroupGVK.Version != "v2alpha1" { - t.Errorf("CiliumCIDRGroupGVK.Version = %q, want %q", CiliumCIDRGroupGVK.Version, "v2alpha1") + if DefaultCiliumCIDRGroupGVK.Version != "v2" { + t.Errorf("DefaultCiliumCIDRGroupGVK.Version = %q, want %q", DefaultCiliumCIDRGroupGVK.Version, "v2") } - if CiliumCIDRGroupGVK.Kind != "CiliumCIDRGroup" { - t.Errorf("CiliumCIDRGroupGVK.Kind = %q, want %q", CiliumCIDRGroupGVK.Kind, "CiliumCIDRGroup") + if DefaultCiliumCIDRGroupGVK.Kind != "CiliumCIDRGroup" { + t.Errorf("DefaultCiliumCIDRGroupGVK.Kind = %q, want %q", DefaultCiliumCIDRGroupGVK.Kind, "CiliumCIDRGroup") } } diff --git a/internal/controller/testdata/crds/cilium.io_ciliumbgpadvertisements.yaml b/internal/controller/testdata/crds/cilium.io_ciliumbgpadvertisements.yaml index 4357f9a..26c2cb6 100644 --- a/internal/controller/testdata/crds/cilium.io_ciliumbgpadvertisements.yaml +++ b/internal/controller/testdata/crds/cilium.io_ciliumbgpadvertisements.yaml @@ -13,7 +13,7 @@ spec: - cbgpadv scope: Cluster versions: - - name: v2alpha1 + - name: v2 served: true storage: true schema: @@ -74,3 +74,64 @@ spec: type: string localPreference: type: integer + - name: v2alpha1 + served: true + storage: false + schema: + openAPIV3Schema: + type: object + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + type: object + properties: + advertisements: + type: array + items: + type: object + properties: + advertisementType: + type: string + enum: + - PodCIDR + - Service + - CiliumPodIPPool + service: + type: object + properties: + addresses: + type: array + items: + type: string + aggregationLengthIPv4: + type: integer + aggregationLengthIPv6: + type: integer + selector: + type: object + x-kubernetes-preserve-unknown-fields: true + attributes: + type: object + properties: + communities: + type: object + properties: + standard: + type: array + items: + type: string + wellKnown: + type: array + items: + type: string + large: + type: array + items: + type: string + localPreference: + type: integer diff --git a/internal/controller/testdata/crds/cilium.io_ciliumcidrgroups.yaml b/internal/controller/testdata/crds/cilium.io_ciliumcidrgroups.yaml index fe8039d..74969bb 100644 --- a/internal/controller/testdata/crds/cilium.io_ciliumcidrgroups.yaml +++ b/internal/controller/testdata/crds/cilium.io_ciliumcidrgroups.yaml @@ -14,7 +14,7 @@ spec: - ccg scope: Cluster versions: - - name: v2alpha1 + - name: v2 served: true storage: true schema: @@ -37,3 +37,26 @@ spec: status: type: object x-kubernetes-preserve-unknown-fields: true + - name: v2alpha1 + served: true + storage: false + schema: + openAPIV3Schema: + type: object + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + type: object + properties: + externalCIDRs: + type: array + items: + type: string + status: + type: object + x-kubernetes-preserve-unknown-fields: true diff --git a/internal/controller/testdata/crds/cilium.io_ciliumloadbalancerippools.yaml b/internal/controller/testdata/crds/cilium.io_ciliumloadbalancerippools.yaml index 8347b4c..18ba3e9 100644 --- a/internal/controller/testdata/crds/cilium.io_ciliumloadbalancerippools.yaml +++ b/internal/controller/testdata/crds/cilium.io_ciliumloadbalancerippools.yaml @@ -14,7 +14,7 @@ spec: - lbippool scope: Cluster versions: - - name: v2alpha1 + - name: v2 served: true storage: true schema: @@ -49,3 +49,38 @@ spec: status: type: object x-kubernetes-preserve-unknown-fields: true + - name: v2alpha1 + served: true + storage: false + schema: + openAPIV3Schema: + type: object + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + type: object + properties: + blocks: + type: array + items: + type: object + properties: + cidr: + type: string + start: + type: string + stop: + type: string + serviceSelector: + type: object + x-kubernetes-preserve-unknown-fields: true + disabled: + type: boolean + status: + type: object + x-kubernetes-preserve-unknown-fields: true From 620a92837b010a3d5691435b5544baf6c774008f Mon Sep 17 00:00:00 2001 From: Philipp Kolberg Date: Mon, 6 Apr 2026 19:27:11 +0200 Subject: [PATCH 2/2] refactor: simplify cilium controller reconciles --- internal/controller/bgpsync_controller.go | 39 ++++++++++------------- 1 file changed, 16 insertions(+), 23 deletions(-) diff --git a/internal/controller/bgpsync_controller.go b/internal/controller/bgpsync_controller.go index 2faa28a..c2522d1 100644 --- a/internal/controller/bgpsync_controller.go +++ b/internal/controller/bgpsync_controller.go @@ -384,10 +384,11 @@ func (r *BGPSyncReconciler) buildBGPCondition( ) metav1.Condition { if len(subnetsWithBGP) == 0 { return metav1.Condition{ - Type: dynamicprefixiov1alpha1.ConditionTypeBGPAdvertisementReady, - Status: metav1.ConditionFalse, - Reason: "NoBGPSubnets", - Message: "No subnets have BGP advertisement enabled", + Type: dynamicprefixiov1alpha1.ConditionTypeBGPAdvertisementReady, + Status: metav1.ConditionFalse, + Reason: "NoBGPSubnets", + Message: "No subnets have BGP advertisement enabled", + LastTransitionTime: metav1.Now(), } } @@ -405,18 +406,20 @@ func (r *BGPSyncReconciler) buildBGPCondition( if allReady { return metav1.Condition{ - Type: dynamicprefixiov1alpha1.ConditionTypeBGPAdvertisementReady, - Status: metav1.ConditionTrue, - Reason: "AdvertisementsReady", - Message: fmt.Sprintf("%d BGP advertisement(s) configured", len(subnetsWithBGP)), + Type: dynamicprefixiov1alpha1.ConditionTypeBGPAdvertisementReady, + Status: metav1.ConditionTrue, + Reason: "AdvertisementsReady", + Message: fmt.Sprintf("%d BGP advertisement(s) configured", len(subnetsWithBGP)), + LastTransitionTime: metav1.Now(), } } return metav1.Condition{ - Type: dynamicprefixiov1alpha1.ConditionTypeBGPAdvertisementReady, - Status: metav1.ConditionFalse, - Reason: "AdvertisementsPending", - Message: "Some BGP advertisements are not yet ready", + Type: dynamicprefixiov1alpha1.ConditionTypeBGPAdvertisementReady, + Status: metav1.ConditionFalse, + Reason: "AdvertisementsPending", + Message: "Some BGP advertisements are not yet ready", + LastTransitionTime: metav1.Now(), } } @@ -430,24 +433,14 @@ func (r *BGPSyncReconciler) findCondition(conditions []metav1.Condition, conditi return nil } -// setCondition updates or adds a condition, preserving LastTransitionTime -// when the status has not changed (per Kubernetes convention). +// setCondition updates or adds a condition. func (r *BGPSyncReconciler) setCondition(conditions *[]metav1.Condition, condition metav1.Condition) { - now := metav1.Now() for i := range *conditions { if (*conditions)[i].Type == condition.Type { - if (*conditions)[i].Status == condition.Status { - // Status unchanged — preserve the existing transition time - condition.LastTransitionTime = (*conditions)[i].LastTransitionTime - } else { - condition.LastTransitionTime = now - } (*conditions)[i] = condition return } } - // New condition - condition.LastTransitionTime = now *conditions = append(*conditions, condition) }