github.com/cilium/cilium@v1.16.2/clustermesh-apiserver/clustermesh/users_mgmt_test.go (about) 1 // SPDX-License-Identifier: Apache-2.0 2 // Copyright Authors of Cilium 3 4 package clustermesh 5 6 import ( 7 "context" 8 "os" 9 "path" 10 "strings" 11 "testing" 12 "time" 13 14 "github.com/cilium/hive/cell" 15 "github.com/cilium/hive/hivetest" 16 "github.com/stretchr/testify/require" 17 "go.uber.org/goleak" 18 19 "github.com/cilium/cilium/operator/watchers" 20 cmtypes "github.com/cilium/cilium/pkg/clustermesh/types" 21 "github.com/cilium/cilium/pkg/hive" 22 "github.com/cilium/cilium/pkg/kvstore" 23 "github.com/cilium/cilium/pkg/promise" 24 ) 25 26 const ( 27 users1 = "users:\n- name: foo\n role: r1\n- name: bar\n role: r2\n- name: qux\n role: r3\n" 28 users2 = "users:\n- name: baz\n role: r3\n- name: foo\n role: r1\n- name: qux\n role: r4\n" 29 ) 30 31 type fakeUserMgmtClient struct { 32 created map[string]string 33 deleted map[string]int 34 } 35 36 func (f *fakeUserMgmtClient) init() { 37 f.created = make(map[string]string) 38 f.deleted = make(map[string]int) 39 } 40 41 func (f *fakeUserMgmtClient) UserEnforcePresence(_ context.Context, name string, roles []string) error { 42 // The existing value (if any) is concatenated, to detect if this is called twice for the same name 43 f.created[name] = f.created[name] + strings.Join(roles, "|") 44 return nil 45 } 46 47 func (f *fakeUserMgmtClient) UserEnforceAbsence(_ context.Context, name string) error { 48 f.deleted[name]++ 49 return nil 50 } 51 52 func TestMain(m *testing.M) { 53 goleak.VerifyTestMain( 54 m, 55 // To ignore goroutine started from sigs.k8s.io/controller-runtime/pkg/log.go 56 // init function 57 goleak.IgnoreTopFunction("time.Sleep"), 58 // Delaying workqueues used by resource.Resource[T].Events leaks this waitingLoop goroutine. 59 // It does stop when shutting down but is not guaranteed to before we actually exit. 60 goleak.IgnoreTopFunction("k8s.io/client-go/util/workqueue.(*delayingType).waitingLoop"), 61 ) 62 } 63 64 func TestUsersManagement(t *testing.T) { 65 defer func() { 66 // force cleanup of goroutines run from initialization of watchers.nodeQueue, 67 // otherwise goleak complains 68 watchers.NodeQueueShutDown() 69 time.Sleep(50 * time.Millisecond) 70 }() 71 72 var client fakeUserMgmtClient 73 client.init() 74 75 tmpdir, err := os.MkdirTemp("", "clustermesh-config") 76 require.NoError(t, err) 77 defer os.RemoveAll(tmpdir) 78 79 cfgPath := path.Join(tmpdir, "users.yaml") 80 require.NoError(t, os.WriteFile(cfgPath, []byte(users1), 0600)) 81 82 hive := hive.New( 83 cell.Provide(func() UsersManagementConfig { 84 return UsersManagementConfig{ 85 ClusterUsersEnabled: true, 86 ClusterUsersConfigPath: cfgPath, 87 } 88 }), 89 90 cell.Provide(func() cmtypes.ClusterInfo { 91 return cmtypes.ClusterInfo{ID: 10, Name: "fred"} 92 }), 93 94 cell.Provide(func(lc cell.Lifecycle) promise.Promise[kvstore.BackendOperationsUserMgmt] { 95 resolver, promise := promise.New[kvstore.BackendOperationsUserMgmt]() 96 resolver.Resolve(&client) 97 return promise 98 }), 99 100 cell.Invoke(registerUsersManager), 101 ) 102 103 ctx, cancel := context.WithCancel(context.Background()) 104 defer cancel() 105 106 tlog := hivetest.Logger(t) 107 if err := hive.Start(tlog, ctx); err != nil { 108 t.Fatalf("failed to start: %s", err) 109 } 110 111 defer func() { 112 if err := hive.Stop(tlog, ctx); err != nil { 113 t.Fatalf("failed to stop: %s", err) 114 } 115 }() 116 117 require.Eventuallyf(t, func() bool { 118 return len(client.created) == 3 && len(client.deleted) == 0 119 }, time.Second, 10*time.Millisecond, 120 "Failed waiting for events to be triggered: created: %v, deleted: %v", 121 client.created, client.deleted) 122 123 require.Equal(t, "r1", client.created["foo"]) 124 require.Equal(t, "r2", client.created["bar"]) 125 require.Equal(t, "r3", client.created["qux"]) 126 127 client.init() 128 129 // Update the users config file, and require that changes are propagated 130 // We first write to a different file and then rename it, to avoid the possible 131 // race condition caused by truncate + write if we detect the event sufficiently 132 // fast (i.e., we first read an empty file, and then the expected one). 133 cfgPath2 := path.Join(tmpdir, "users.yaml.2") 134 require.NoError(t, os.WriteFile(cfgPath2, []byte(users2), 0600)) 135 require.NoError(t, os.Rename(cfgPath2, cfgPath)) 136 137 require.Eventuallyf(t, func() bool { 138 return len(client.created) == 2 && len(client.deleted) == 1 139 }, time.Second, 10*time.Millisecond, 140 "Failed waiting for events to be triggered: created: %v, deleted: %v", 141 client.created, client.deleted) 142 143 require.Equal(t, "r3", client.created["baz"]) 144 require.Equal(t, "r4", client.created["qux"]) 145 require.Equal(t, 1, client.deleted["bar"]) 146 }