github.com/cilium/cilium@v1.16.2/pkg/clustermesh/common/config_test.go (about) 1 // SPDX-License-Identifier: Apache-2.0 2 // Copyright Authors of Cilium 3 4 package common 5 6 import ( 7 "context" 8 "crypto/sha256" 9 "os" 10 "path" 11 "testing" 12 "time" 13 14 "github.com/cilium/hive/hivetest" 15 "github.com/stretchr/testify/assert" 16 "github.com/stretchr/testify/require" 17 "golang.org/x/exp/maps" 18 19 "github.com/cilium/cilium/pkg/clustermesh/types" 20 "github.com/cilium/cilium/pkg/kvstore" 21 ) 22 23 const ( 24 content1 = "endpoints:\n- https://cluster1.cilium-etcd.cilium.svc:2379\n" 25 content2 = "endpoints:\n- https://cluster1.cilium-etcd.cilium.svc:2380\n" 26 ) 27 28 // Configure a generous timeout to prevent flakes when running in a noisy CI environment. 29 var ( 30 tick = 10 * time.Millisecond 31 timeout = 5 * time.Second 32 ) 33 34 type fakeRemoteCluster struct{ onRun, onStop, onRemove func(context.Context) } 35 36 func (f *fakeRemoteCluster) Run(ctx context.Context, _ kvstore.BackendOperations, _ types.CiliumClusterConfig, ready chan<- error) { 37 if f.onRun != nil { 38 f.onRun(ctx) 39 } 40 close(ready) 41 } 42 func (f *fakeRemoteCluster) Stop() { 43 if f.onStop != nil { 44 f.onStop(context.Background()) 45 } 46 } 47 func (f *fakeRemoteCluster) Remove(ctx context.Context) { 48 if f.onRemove != nil { 49 f.onRemove(ctx) 50 } 51 } 52 53 func writeFile(t *testing.T, name, content string) { 54 t.Helper() 55 56 err := os.WriteFile(name, []byte(content), 0644) 57 require.NoError(t, err) 58 } 59 60 func expectChange(t *testing.T, cm *clusterMesh, name string) { 61 t.Helper() 62 63 cm.mutex.RLock() 64 require.Contains(t, cm.clusters, name) 65 cluster := cm.clusters[name] 66 cm.mutex.RUnlock() 67 68 select { 69 case <-cluster.changed: 70 case <-time.After(time.Second): 71 t.Fatal("timeout while waiting for changed event") 72 } 73 } 74 75 func expectNoChange(t *testing.T, cm *clusterMesh, name string) { 76 t.Helper() 77 78 cm.mutex.RLock() 79 cluster := cm.clusters[name] 80 cm.mutex.RUnlock() 81 require.NotNil(t, cluster) 82 83 select { 84 case <-cluster.changed: 85 t.Fatal("unexpected changed event detected") 86 case <-time.After(100 * time.Millisecond): 87 } 88 } 89 90 func TestWatchConfigDirectory(t *testing.T) { 91 skipKvstoreConnection = true 92 defer func() { 93 skipKvstoreConnection = false 94 }() 95 96 baseDir := t.TempDir() 97 98 dataDir := path.Join(baseDir, "..data") 99 dataDirTmp := path.Join(baseDir, "..data_tmp") 100 dataDir1 := path.Join(baseDir, "..data-1") 101 dataDir2 := path.Join(baseDir, "..data-2") 102 dataDir3 := path.Join(baseDir, "..data-3") 103 104 require.NoError(t, os.Symlink(dataDir1, dataDir)) 105 require.NoError(t, os.Mkdir(dataDir1, 0755)) 106 require.NoError(t, os.Mkdir(dataDir2, 0755)) 107 require.NoError(t, os.Mkdir(dataDir3, 0755)) 108 109 file1 := path.Join(baseDir, "cluster1") 110 file2 := path.Join(baseDir, "cluster2") 111 file3 := path.Join(baseDir, "cluster3") 112 113 writeFile(t, file1, content1) 114 writeFile(t, path.Join(dataDir1, "cluster2"), content1) 115 writeFile(t, path.Join(dataDir2, "cluster2"), content2) 116 writeFile(t, path.Join(dataDir3, "cluster2"), content1) 117 118 // Create an indirect link, as in case of Kubernetes COnfigMaps/Secret mounted inside pods. 119 require.NoError(t, os.Symlink(path.Join(dataDir, "cluster2"), file2)) 120 121 gcm := NewClusterMesh(Configuration{ 122 Config: Config{ClusterMeshConfig: baseDir}, 123 ClusterInfo: types.ClusterInfo{ID: 255, Name: "test2"}, 124 NewRemoteCluster: func(string, StatusFunc) RemoteCluster { return &fakeRemoteCluster{} }, 125 Metrics: MetricsProvider("clustermesh")(), 126 }) 127 cm := gcm.(*clusterMesh) 128 hivetest.Lifecycle(t).Append(cm) 129 130 // wait for cluster1 and cluster2 to appear 131 require.EventuallyWithT(t, func(c *assert.CollectT) { 132 cm.mutex.RLock() 133 defer cm.mutex.RUnlock() 134 assert.ElementsMatch(c, maps.Keys(cm.clusters), []string{"cluster1", "cluster2"}) 135 }, timeout, tick) 136 137 require.NoError(t, os.RemoveAll(file1)) 138 139 // wait for cluster1 to disappear 140 require.EventuallyWithT(t, func(c *assert.CollectT) { 141 cm.mutex.RLock() 142 defer cm.mutex.RUnlock() 143 assert.ElementsMatch(c, maps.Keys(cm.clusters), []string{"cluster2"}) 144 }, timeout, tick) 145 146 writeFile(t, file3, content1) 147 148 // wait for cluster3 to appear 149 require.EventuallyWithT(t, func(c *assert.CollectT) { 150 cm.mutex.RLock() 151 defer cm.mutex.RUnlock() 152 assert.ElementsMatch(c, maps.Keys(cm.clusters), []string{"cluster2", "cluster3"}) 153 }, timeout, tick) 154 155 // Test renaming of file from cluster3 to cluster1 156 require.NoError(t, os.Rename(file3, file1)) 157 158 // wait for cluster1 to appear 159 require.EventuallyWithT(t, func(c *assert.CollectT) { 160 cm.mutex.RLock() 161 defer cm.mutex.RUnlock() 162 assert.ElementsMatch(c, maps.Keys(cm.clusters), []string{"cluster1", "cluster2"}) 163 }, timeout, tick) 164 165 // touch file 166 require.NoError(t, os.Chtimes(file1, time.Now(), time.Now())) 167 expectNoChange(t, cm, "cluster1") 168 169 // update file content changing the symlink target, adopting 170 // the same approach of the kubelet on ConfigMap/Secret update 171 require.NoError(t, os.Symlink(dataDir2, dataDirTmp)) 172 require.NoError(t, os.Rename(dataDirTmp, dataDir)) 173 require.NoError(t, os.RemoveAll(dataDir1)) 174 expectChange(t, cm, "cluster2") 175 176 // update file content once more 177 require.NoError(t, os.Symlink(dataDir3, dataDirTmp)) 178 require.NoError(t, os.Rename(dataDirTmp, dataDir)) 179 require.NoError(t, os.RemoveAll(dataDir2)) 180 expectChange(t, cm, "cluster2") 181 182 require.NoError(t, os.RemoveAll(file1)) 183 require.NoError(t, os.RemoveAll(file2)) 184 185 // wait for all clusters to disappear 186 require.EventuallyWithT(t, func(c *assert.CollectT) { 187 cm.mutex.RLock() 188 defer cm.mutex.RUnlock() 189 assert.Empty(c, cm.clusters) 190 }, timeout, tick) 191 192 // Ensure that per-config watches are removed properly 193 wl := cm.configWatcher.watcher.WatchList() 194 require.ElementsMatch(t, wl, []string{baseDir}) 195 } 196 197 func TestIsEtcdConfigFile(t *testing.T) { 198 dir := t.TempDir() 199 200 validPath := path.Join(dir, "valid") 201 content := []byte("endpoints:\n- https://cluster1.cilium-etcd.cilium.svc:2379\n") 202 err := os.WriteFile(validPath, content, 0644) 203 require.NoError(t, err) 204 205 isConfig, hash := isEtcdConfigFile(validPath) 206 require.True(t, isConfig) 207 require.Equal(t, fhash(sha256.Sum256(content)), hash) 208 209 invalidPath := path.Join(dir, "valid") 210 err = os.WriteFile(invalidPath, []byte("sf324kj234lkjsdvl\nwl34kj23l4k\nendpoints"), 0644) 211 require.NoError(t, err) 212 213 isConfig, hash = isEtcdConfigFile(validPath) 214 require.False(t, isConfig) 215 require.Equal(t, fhash{}, hash) 216 217 isConfig, hash = isEtcdConfigFile(dir) 218 require.False(t, isConfig) 219 require.Equal(t, fhash{}, hash) 220 }