@@ -181,3 +181,134 @@ func TestContextProvider(t *testing.T) {
181181}
182182}
183183}
184+
185+ func TestContextProvider_KubernetesSymlinks (t * testing.T ) {
186+ if runtime .GOOS == "windows" {
187+ t .Skip ("Skipping Kubernetes symlink test on Windows, because atomic replacing a symlink using os.Rename doesn't work" )
188+ }
189+
190+ const testTimeout = 3 * time .Second
191+
192+ // Create directory structure that mimics Kubernetes secrets
193+ tmpDir := t .TempDir ()
194+
195+ // Create initial timestamped directory with secret content
196+ dataDir1 := filepath .Join (tmpDir , "..2024_01_01_12_00" )
197+ require .NoError (t , os .Mkdir (dataDir1 , 0o755 ))
198+
199+ value1 := "secret-token-v1"
200+ tokenFile1 := filepath .Join (dataDir1 , "token" )
201+ require .NoError (t , os .WriteFile (tokenFile1 , []byte (value1 ), 0o644 ))
202+
203+ value2 := "secret-cert-v1"
204+ certFile1 := filepath .Join (dataDir1 , "ca.crt" )
205+ require .NoError (t , os .WriteFile (certFile1 , []byte (value2 ), 0o644 ))
206+
207+ // Create ..data symlink pointing to the timestamped directory
208+ dataSymlink := filepath .Join (tmpDir , "..data" )
209+ require .NoError (t , os .Symlink (dataDir1 , dataSymlink ))
210+
211+ // Create top-level symlinks (what the user actually references)
212+ tokenSymlink := filepath .Join (tmpDir , "token" )
213+ require .NoError (t , os .Symlink (filepath .Join ("..data" , "token" ), tokenSymlink ))
214+
215+ certSymlink := filepath .Join (tmpDir , "ca.crt" )
216+ require .NoError (t , os .Symlink (filepath .Join ("..data" , "ca.crt" ), certSymlink ))
217+
218+ // Setup logger and provider
219+ log , err := logger .New ("filesource_test" , false )
220+ require .NoError (t , err )
221+
222+ osPath := func (path string ) string {
223+ return path
224+ }
225+ if runtime .GOOS == "windows" {
226+ osPath = func (path string ) string {
227+ return strings .ToLower (path )
228+ }
229+ }
230+
231+ c , err := config .NewConfigFrom (map [string ]interface {}{
232+ "sources" : map [string ]interface {}{
233+ "token" : map [string ]interface {}{
234+ "path" : osPath (tokenSymlink ),
235+ },
236+ "cert" : map [string ]interface {}{
237+ "path" : osPath (certSymlink ),
238+ },
239+ },
240+ })
241+ require .NoError (t , err )
242+
243+ builder , _ := composable .Providers .GetContextProvider ("filesource" )
244+ provider , err := builder (log , c , true )
245+ require .NoError (t , err )
246+
247+ ctx , cancel := context .WithCancel (context .Background ())
248+ defer cancel ()
249+ comm := ctesting .NewContextComm (ctx )
250+ setChan := make (chan map [string ]interface {})
251+ comm .CallOnSet (func (value map [string ]interface {}) {
252+ t .Logf ("Set called with: token=%v, cert=%v" , value ["token" ], value ["cert" ])
253+ setChan <- value
254+ })
255+
256+ go func () {
257+ _ = provider .Run (ctx , comm )
258+ }()
259+
260+ // Wait for initial values
261+ var current map [string ]interface {}
262+ select {
263+ case current = <- setChan :
264+ case <- time .After (testTimeout ):
265+ require .FailNow (t , "timeout waiting for provider to call Set" )
266+ }
267+
268+ require .Equal (t , value1 , current ["token" ], "initial token value should match" )
269+ require .Equal (t , value2 , current ["cert" ], "initial cert value should match" )
270+
271+ // Simulate Kubernetes secret update:
272+ // 1. Create new timestamped directory with updated content
273+ dataDir2 := filepath .Join (tmpDir , "..2024_01_01_13_00" )
274+ require .NoError (t , os .Mkdir (dataDir2 , 0o755 ))
275+
276+ value1Updated := "secret-token-v2"
277+ tokenFile2 := filepath .Join (dataDir2 , "token" )
278+ require .NoError (t , os .WriteFile (tokenFile2 , []byte (value1Updated ), 0o644 ))
279+
280+ value2Updated := "secret-cert-v2"
281+ certFile2 := filepath .Join (dataDir2 , "ca.crt" )
282+ require .NoError (t , os .WriteFile (certFile2 , []byte (value2Updated ), 0o644 ))
283+
284+ // 2. Atomically replace ..data symlink (this is what Kubernetes does)
285+ // Create temporary symlink, then rename it to replace the old one atomically
286+ dataTmpSymlink := filepath .Join (tmpDir , "..data_tmp" )
287+ require .NoError (t , os .Symlink (dataDir2 , dataTmpSymlink ))
288+ require .NoError (t , os .Rename (dataTmpSymlink , dataSymlink ))
289+
290+ // Note: The top-level symlinks (token, ca.crt) are NOT modified
291+ // They still point to ..data/token and ..data/ca.crt
292+ // Only the ..data symlink target changed
293+
294+ // Wait for the provider to detect the update
295+ // This should happen because fsnotify should see the ..data symlink change
296+ updateDetected := false
297+ deadline := time .After (testTimeout )
298+ for ! updateDetected {
299+ select {
300+ case updated := <- setChan :
301+ // Check if we got the updated values
302+ if updated ["token" ] == value1Updated && updated ["cert" ] == value2Updated {
303+ updateDetected = true
304+ t .Log ("Successfully detected Kubernetes-style symlink update" )
305+ } else {
306+ t .Logf ("Got update but values don't match yet: token=%v, cert=%v" , updated ["token" ], updated ["cert" ])
307+ }
308+ case <- deadline :
309+ require .FailNow (t , "timeout waiting for provider to detect Kubernetes-style symlink update" )
310+ }
311+ }
312+
313+ require .True (t , updateDetected , "provider should detect Kubernetes-style symlink updates" )
314+ }
0 commit comments