@@ -21,7 +21,9 @@ import (
2121 "context"
2222 "errors"
2323 "fmt"
24+ "slices"
2425 "strconv"
26+ "sync"
2527 "time"
2628
2729 . "github.com/onsi/ginkgo/v2"
@@ -1492,45 +1494,6 @@ var _ = Describe("Reconciler", func() {
14921494 })
14931495 })
14941496 })
1495- When ("label selector set" , func () {
1496- It ("reconcile only matching CR" , func () {
1497- By ("adding selector to the reconciler" , func () {
1498- selectorFoo := metav1.LabelSelector {MatchLabels : map [string ]string {"app" : "foo" }}
1499- Expect (WithSelector (selectorFoo )(r )).To (Succeed ())
1500- })
1501-
1502- By ("adding not matching label to the CR" , func () {
1503- Expect (mgr .GetClient ().Get (ctx , objKey , obj )).To (Succeed ())
1504- obj .SetLabels (map [string ]string {"app" : "bar" })
1505- Expect (mgr .GetClient ().Update (ctx , obj )).To (Succeed ())
1506- })
1507-
1508- By ("reconciling skipped and no actions for the release" , func () {
1509- res , err := r .Reconcile (ctx , req )
1510- Expect (res ).To (Equal (reconcile.Result {}))
1511- Expect (err ).ToNot (HaveOccurred ())
1512- })
1513-
1514- By ("verifying the release has not changed" , func () {
1515- rel , err := ac .Get (obj .GetName ())
1516- Expect (err ).ToNot (HaveOccurred ())
1517- Expect (rel ).NotTo (BeNil ())
1518- Expect (* rel ).To (Equal (* currentRelease ))
1519- })
1520-
1521- By ("adding matching label to the CR" , func () {
1522- Expect (mgr .GetClient ().Get (ctx , objKey , obj )).To (Succeed ())
1523- obj .SetLabels (map [string ]string {"app" : "foo" })
1524- Expect (mgr .GetClient ().Update (ctx , obj )).To (Succeed ())
1525- })
1526-
1527- By ("successfully reconciling with correct labels" , func () {
1528- res , err := r .Reconcile (ctx , req )
1529- Expect (res ).To (Equal (reconcile.Result {}))
1530- Expect (err ).ToNot (HaveOccurred ())
1531- })
1532- })
1533- })
15341497 })
15351498 })
15361499 })
@@ -1545,6 +1508,184 @@ var _ = Describe("Reconciler", func() {
15451508 })
15461509 })
15471510
1511+ _ = Describe ("WithSelector" , func () {
1512+ var (
1513+ ctx context.Context
1514+ cancel context.CancelFunc
1515+ matchingLabels map [string ]string
1516+ anotherMatchingLabels map [string ]string
1517+ mu sync.Mutex
1518+ doneChans []chan struct {}
1519+ )
1520+
1521+ BeforeEach (func () {
1522+ matchingLabels = map [string ]string {"app" : "foo" }
1523+ anotherMatchingLabels = map [string ]string {"app" : "bar" }
1524+ doneChans = nil
1525+ ctx , cancel = context .WithCancel (context .Background ())
1526+
1527+ cleanupMgr := getManagerOrFail ()
1528+ cleanupClient := cleanupMgr .GetClient ()
1529+ crList := & unstructured.UnstructuredList {}
1530+ crList .SetGroupVersionKind (gvk )
1531+ Expect (cleanupClient .List (ctx , crList )).To (Succeed ())
1532+
1533+ for i := range crList .Items {
1534+ cr := & crList .Items [i ]
1535+ // Remove all finalizers to allow CR deletion
1536+ cr .SetFinalizers ([]string {})
1537+ Expect (cleanupClient .Update (ctx , cr )).To (Succeed ())
1538+ Expect (cleanupClient .Delete (ctx , cr )).To (Succeed ())
1539+ }
1540+
1541+ // Wait for all CRs to be deleted
1542+ Eventually (func () bool {
1543+ crList := & unstructured.UnstructuredList {}
1544+ crList .SetGroupVersionKind (gvk )
1545+ if err := cleanupClient .List (ctx , crList ); err != nil {
1546+ return false
1547+ }
1548+ return len (crList .Items ) == 0
1549+ }, "10s" , "100ms" ).Should (BeTrue ())
1550+ })
1551+
1552+ AfterEach (func () {
1553+ // Cancel the context to stop all managers
1554+ if cancel != nil {
1555+ cancel ()
1556+ }
1557+ // Wait for all managers to shut down completely
1558+ for _ , done := range doneChans {
1559+ select {
1560+ case <- done :
1561+ // Manager has shut down
1562+ case <- time .After (5 * time .Second ):
1563+ // Timeout waiting for manager shutdown
1564+ }
1565+ }
1566+ })
1567+
1568+ It ("should only reconcile CRs matching the label selector" , func () {
1569+ labeledObj := testutil .BuildTestCR (gvk )
1570+ labeledObj .SetName ("labeled-cr-test1" )
1571+ labeledObj .SetLabels (matchingLabels )
1572+ labeledObjKey := types.NamespacedName {Namespace : labeledObj .GetNamespace (), Name : labeledObj .GetName ()}
1573+
1574+ anotherObj := testutil .BuildTestCR (gvk )
1575+ anotherObj .SetName ("another-cr-test1" )
1576+ anotherObjKey := types.NamespacedName {Namespace : anotherObj .GetNamespace (), Name : anotherObj .GetName ()}
1577+
1578+ var reconciledCRs []string
1579+ postHook := makePostHook (& mu , & reconciledCRs )
1580+ mgr , done := setupManagerWithSelectorAndPostHook (ctx , postHook , matchingLabels )
1581+ doneChans = append (doneChans , done )
1582+
1583+ By ("creating a CR without matching labels" , func () {
1584+ Expect (mgr .GetClient ().Create (ctx , anotherObj )).To (Succeed ())
1585+ })
1586+
1587+ // There is no robust way to check whether controller-runtime will never do something.
1588+ // So there is a low chance that the unlabeled CR could be reconciled.
1589+ // See: https://kubernetes.slack.com/archives/C02MRBMN00Z/p1762251589740719
1590+ By ("verifying that the labeled reconciler does not reconcile CR without labels" , func () {
1591+ Consistently (func () bool {
1592+ mu .Lock ()
1593+ defer mu .Unlock ()
1594+ return len (reconciledCRs ) == 0
1595+ }, "2s" , "100ms" ).Should (BeTrue ())
1596+ })
1597+
1598+ By ("creating a CR with matching labels" , func () {
1599+ Expect (mgr .GetClient ().Create (ctx , labeledObj )).To (Succeed ())
1600+ })
1601+
1602+ By ("verifying only the labeled CR was reconciled" , func () {
1603+ Eventually (func () bool {
1604+ mu .Lock ()
1605+ defer mu .Unlock ()
1606+ return len (reconciledCRs ) == 1 && reconciledCRs [0 ] == labeledObjKey .Name
1607+ }).Should (BeTrue ())
1608+ })
1609+
1610+ By ("updating the unlabeled CR to have matching labels" , func () {
1611+ Expect (mgr .GetClient ().Get (ctx , anotherObjKey , anotherObj )).To (Succeed ())
1612+ anotherObj .SetLabels (matchingLabels )
1613+ Expect (mgr .GetClient ().Update (ctx , anotherObj )).To (Succeed ())
1614+ })
1615+
1616+ By ("verifying that both CRs were reconciled after setting label to the unlabeled CR" , func () {
1617+ Eventually (func () bool {
1618+ mu .Lock ()
1619+ defer mu .Unlock ()
1620+ return len (reconciledCRs ) == 2 &&
1621+ slices .Contains (reconciledCRs , labeledObjKey .Name ) &&
1622+ slices .Contains (reconciledCRs , anotherObjKey .Name )
1623+ }, "10s" , "100ms" ).Should (BeTrue ())
1624+ })
1625+ })
1626+
1627+ It ("should reconcile CRs independently when using two managers with different label selectors" , func () {
1628+ labeledObj := testutil .BuildTestCR (gvk )
1629+ labeledObj .SetName ("labeled-cr-test2" )
1630+ labeledObj .SetLabels (matchingLabels )
1631+ labeledObjKey := types.NamespacedName {Namespace : labeledObj .GetNamespace (), Name : labeledObj .GetName ()}
1632+
1633+ anotherObj := testutil .BuildTestCR (gvk )
1634+ anotherObj .SetName ("another-cr-test2" )
1635+ anotherObjKey := types.NamespacedName {Namespace : anotherObj .GetNamespace (), Name : anotherObj .GetName ()}
1636+
1637+ var reconciledCRs []string
1638+ var anotherReconciledCRs []string
1639+
1640+ postHook := makePostHook (& mu , & reconciledCRs )
1641+ mgr , done := setupManagerWithSelectorAndPostHook (ctx , postHook , matchingLabels )
1642+ doneChans = append (doneChans , done )
1643+
1644+ postHook2 := makePostHook (& mu , & anotherReconciledCRs )
1645+ _ , done2 := setupManagerWithSelectorAndPostHook (ctx , postHook2 , anotherMatchingLabels )
1646+ doneChans = append (doneChans , done2 )
1647+
1648+ By ("creating a CR with matching labels for the first manager" , func () {
1649+ Expect (mgr .GetClient ().Create (ctx , labeledObj )).To (Succeed ())
1650+ })
1651+
1652+ By ("verifying that only the first manager reconciled the CR" , func () {
1653+ Eventually (func () bool {
1654+ mu .Lock ()
1655+ defer mu .Unlock ()
1656+ return len (reconciledCRs ) == 1 && reconciledCRs [0 ] == labeledObjKey .Name
1657+ }, "10s" , "100ms" ).Should (BeTrue ())
1658+
1659+ Consistently (func () bool {
1660+ mu .Lock ()
1661+ defer mu .Unlock ()
1662+ return len (anotherReconciledCRs ) == 0
1663+ }, "2s" , "100ms" ).Should (BeTrue ())
1664+ })
1665+
1666+ By ("creating a CR with matching labels for the second manager" , func () {
1667+ Expect (mgr .GetClient ().Create (ctx , anotherObj )).To (Succeed ())
1668+ Expect (mgr .GetClient ().Get (ctx , anotherObjKey , anotherObj )).To (Succeed ())
1669+ anotherObj .SetLabels (anotherMatchingLabels )
1670+ Expect (mgr .GetClient ().Update (ctx , anotherObj )).To (Succeed ())
1671+ })
1672+
1673+ By ("verifying that both managers reconcile only matching labels CRs" , func () {
1674+ Eventually (func () bool {
1675+ mu .Lock ()
1676+ defer mu .Unlock ()
1677+ return len (reconciledCRs ) == 1 && reconciledCRs [0 ] == labeledObjKey .Name
1678+ }, "10s" , "100ms" ).Should (BeTrue ())
1679+
1680+ Eventually (func () bool {
1681+ mu .Lock ()
1682+ defer mu .Unlock ()
1683+ return len (anotherReconciledCRs ) == 1 && anotherReconciledCRs [0 ] == anotherObjKey .Name
1684+ }, "10s" , "100ms" ).Should (BeTrue ())
1685+ })
1686+ })
1687+ })
1688+
15481689 _ = Describe ("Test custom controller setup" , func () {
15491690 var (
15501691 mgr manager.Manager
@@ -1743,3 +1884,34 @@ func verifyEvent(ctx context.Context, cl client.Reader, obj metav1.Object, event
17431884 Reason: %q
17441885 Message: %q` , eventType , reason , message ))
17451886}
1887+
1888+ func makePostHook (mu * sync.Mutex , reconciledCRs * []string ) hook.PostHook {
1889+ return hook .PostHookFunc (func (obj * unstructured.Unstructured , _ release.Release , _ logr.Logger ) error {
1890+ mu .Lock ()
1891+ defer mu .Unlock ()
1892+ if ! slices .Contains (* reconciledCRs , obj .GetName ()) {
1893+ * reconciledCRs = append (* reconciledCRs , obj .GetName ())
1894+ }
1895+ return nil
1896+ })
1897+ }
1898+
1899+ func setupManagerWithSelectorAndPostHook (ctx context.Context , postHook hook.PostHook , matchingLabels map [string ]string ) (manager.Manager , chan struct {}) {
1900+ mgr := getManagerOrFail ()
1901+ r , err := New (
1902+ WithGroupVersionKind (gvk ),
1903+ WithChart (chrt ),
1904+ WithSelector (metav1.LabelSelector {MatchLabels : matchingLabels }),
1905+ WithPostHook (postHook ),
1906+ )
1907+ Expect (err ).ToNot (HaveOccurred ())
1908+ Expect (r .SetupWithManager (mgr )).To (Succeed ())
1909+
1910+ done := make (chan struct {})
1911+ go func () {
1912+ defer close (done )
1913+ Expect (mgr .Start (ctx )).To (Succeed ())
1914+ }()
1915+ Expect (mgr .GetCache ().WaitForCacheSync (ctx )).To (BeTrue ())
1916+ return mgr , done
1917+ }
0 commit comments