|
1 | 1 | package config |
2 | 2 |
|
3 | 3 | import ( |
| 4 | + "context" |
4 | 5 | "os" |
5 | 6 | "path/filepath" |
| 7 | + "sync/atomic" |
6 | 8 | "testing" |
7 | 9 | "time" |
8 | 10 |
|
@@ -220,3 +222,195 @@ firewall: |
220 | 222 | } |
221 | 223 | assert.Equal(t, expected, m) |
222 | 224 | } |
| 225 | + |
| 226 | +func TestConfig_GetCertPaths(t *testing.T) { |
| 227 | + l := test.NewLogger() |
| 228 | + |
| 229 | + // File paths should be returned |
| 230 | + c := NewC(l) |
| 231 | + c.Settings["pki"] = map[string]any{ |
| 232 | + "cert": "/etc/nebula/host.crt", |
| 233 | + "key": "/etc/nebula/host.key", |
| 234 | + "ca": "/etc/nebula/ca.crt", |
| 235 | + } |
| 236 | + paths := c.GetCertPaths() |
| 237 | + assert.Len(t, paths, 3) |
| 238 | + assert.Contains(t, paths, "/etc/nebula/host.crt") |
| 239 | + assert.Contains(t, paths, "/etc/nebula/host.key") |
| 240 | + assert.Contains(t, paths, "/etc/nebula/ca.crt") |
| 241 | + |
| 242 | + // Inline PEM data should be skipped |
| 243 | + c = NewC(l) |
| 244 | + c.Settings["pki"] = map[string]any{ |
| 245 | + "cert": "-----BEGIN NEBULA CERTIFICATE-----\ndata\n-----END NEBULA CERTIFICATE-----", |
| 246 | + "key": "/etc/nebula/host.key", |
| 247 | + "ca": "-----BEGIN NEBULA CERTIFICATE-----\ndata\n-----END NEBULA CERTIFICATE-----", |
| 248 | + } |
| 249 | + paths = c.GetCertPaths() |
| 250 | + assert.Len(t, paths, 1) |
| 251 | + assert.Equal(t, "/etc/nebula/host.key", paths[0]) |
| 252 | + |
| 253 | + // PKCS#11 URIs should be skipped |
| 254 | + c = NewC(l) |
| 255 | + c.Settings["pki"] = map[string]any{ |
| 256 | + "cert": "/etc/nebula/host.crt", |
| 257 | + "key": "pkcs11:token=mytoken", |
| 258 | + "ca": "/etc/nebula/ca.crt", |
| 259 | + } |
| 260 | + paths = c.GetCertPaths() |
| 261 | + assert.Len(t, paths, 2) |
| 262 | + assert.Contains(t, paths, "/etc/nebula/host.crt") |
| 263 | + assert.Contains(t, paths, "/etc/nebula/ca.crt") |
| 264 | + |
| 265 | + // Empty or missing settings should return empty |
| 266 | + c = NewC(l) |
| 267 | + paths = c.GetCertPaths() |
| 268 | + assert.Empty(t, paths) |
| 269 | +} |
| 270 | + |
| 271 | +func TestConfig_CatchCertChange(t *testing.T) { |
| 272 | + l := test.NewLogger() |
| 273 | + |
| 274 | + // Set up a temp directory with config and cert files |
| 275 | + configDir, err := os.MkdirTemp("", "config-certwatch-test") |
| 276 | + require.NoError(t, err) |
| 277 | + defer os.RemoveAll(configDir) |
| 278 | + |
| 279 | + certDir, err := os.MkdirTemp("", "certs-certwatch-test") |
| 280 | + require.NoError(t, err) |
| 281 | + defer os.RemoveAll(certDir) |
| 282 | + |
| 283 | + certPath := filepath.Join(certDir, "host.crt") |
| 284 | + keyPath := filepath.Join(certDir, "host.key") |
| 285 | + caPath := filepath.Join(certDir, "ca.crt") |
| 286 | + |
| 287 | + require.NoError(t, os.WriteFile(certPath, []byte("cert-data"), 0644)) |
| 288 | + require.NoError(t, os.WriteFile(keyPath, []byte("key-data"), 0644)) |
| 289 | + require.NoError(t, os.WriteFile(caPath, []byte("ca-data"), 0644)) |
| 290 | + |
| 291 | + // Create config that references those cert paths |
| 292 | + configYaml := "pki:\n cert: " + certPath + "\n key: " + keyPath + "\n ca: " + caPath + "\n" |
| 293 | + require.NoError(t, os.WriteFile(filepath.Join(configDir, "config.yaml"), []byte(configYaml), 0644)) |
| 294 | + |
| 295 | + c := NewC(l) |
| 296 | + require.NoError(t, c.Load(configDir)) |
| 297 | + |
| 298 | + // Track reload calls via callback |
| 299 | + var reloadCount atomic.Int32 |
| 300 | + c.RegisterReloadCallback(func(c *C) { |
| 301 | + reloadCount.Add(1) |
| 302 | + }) |
| 303 | + |
| 304 | + ctx, cancel := context.WithCancel(context.Background()) |
| 305 | + defer cancel() |
| 306 | + |
| 307 | + // Start watching with a short debounce for testing |
| 308 | + c.CatchCertChange(ctx, 100*time.Millisecond) |
| 309 | + |
| 310 | + // Give the watcher time to start |
| 311 | + time.Sleep(50 * time.Millisecond) |
| 312 | + |
| 313 | + // Modify a cert file |
| 314 | + require.NoError(t, os.WriteFile(certPath, []byte("cert-data-updated"), 0644)) |
| 315 | + |
| 316 | + // Wait for debounce + processing |
| 317 | + time.Sleep(500 * time.Millisecond) |
| 318 | + |
| 319 | + assert.GreaterOrEqual(t, reloadCount.Load(), int32(1), "ReloadConfig should have been called at least once") |
| 320 | + |
| 321 | + // Reset count and test debouncing: rapid writes should coalesce into one reload |
| 322 | + startCount := reloadCount.Load() |
| 323 | + require.NoError(t, os.WriteFile(certPath, []byte("cert-v2"), 0644)) |
| 324 | + time.Sleep(20 * time.Millisecond) |
| 325 | + require.NoError(t, os.WriteFile(keyPath, []byte("key-v2"), 0644)) |
| 326 | + time.Sleep(20 * time.Millisecond) |
| 327 | + require.NoError(t, os.WriteFile(caPath, []byte("ca-v2"), 0644)) |
| 328 | + |
| 329 | + // Wait for debounce to fire |
| 330 | + time.Sleep(500 * time.Millisecond) |
| 331 | + |
| 332 | + endCount := reloadCount.Load() |
| 333 | + // The debounced rapid writes should result in at most 2 reload calls |
| 334 | + // (ideally 1, but filesystem event timing can vary) |
| 335 | + assert.LessOrEqual(t, endCount-startCount, int32(2), |
| 336 | + "Debouncing should coalesce rapid writes into few reloads") |
| 337 | + assert.GreaterOrEqual(t, endCount-startCount, int32(1), |
| 338 | + "At least one reload should occur for the rapid writes") |
| 339 | +} |
| 340 | + |
| 341 | +func TestConfig_CatchCertChange_ContextCancel(t *testing.T) { |
| 342 | + l := test.NewLogger() |
| 343 | + |
| 344 | + certDir, err := os.MkdirTemp("", "certs-cancel-test") |
| 345 | + require.NoError(t, err) |
| 346 | + defer os.RemoveAll(certDir) |
| 347 | + |
| 348 | + certPath := filepath.Join(certDir, "host.crt") |
| 349 | + require.NoError(t, os.WriteFile(certPath, []byte("cert-data"), 0644)) |
| 350 | + |
| 351 | + configDir, err := os.MkdirTemp("", "config-cancel-test") |
| 352 | + require.NoError(t, err) |
| 353 | + defer os.RemoveAll(configDir) |
| 354 | + |
| 355 | + configYaml := "pki:\n cert: " + certPath + "\n" |
| 356 | + require.NoError(t, os.WriteFile(filepath.Join(configDir, "config.yaml"), []byte(configYaml), 0644)) |
| 357 | + |
| 358 | + c := NewC(l) |
| 359 | + require.NoError(t, c.Load(configDir)) |
| 360 | + |
| 361 | + var reloadCount atomic.Int32 |
| 362 | + c.RegisterReloadCallback(func(c *C) { |
| 363 | + reloadCount.Add(1) |
| 364 | + }) |
| 365 | + |
| 366 | + ctx, cancel := context.WithCancel(context.Background()) |
| 367 | + c.CatchCertChange(ctx, 100*time.Millisecond) |
| 368 | + time.Sleep(50 * time.Millisecond) |
| 369 | + |
| 370 | + // Cancel the context to stop the watcher |
| 371 | + cancel() |
| 372 | + time.Sleep(50 * time.Millisecond) |
| 373 | + |
| 374 | + // Write to cert file after cancellation — should NOT trigger reload |
| 375 | + beforeCount := reloadCount.Load() |
| 376 | + require.NoError(t, os.WriteFile(certPath, []byte("cert-after-cancel"), 0644)) |
| 377 | + time.Sleep(300 * time.Millisecond) |
| 378 | + |
| 379 | + assert.Equal(t, beforeCount, reloadCount.Load(), |
| 380 | + "No reload should occur after context cancellation") |
| 381 | +} |
| 382 | + |
| 383 | +func TestConfig_CatchCertChange_NoPath(t *testing.T) { |
| 384 | + l := test.NewLogger() |
| 385 | + c := NewC(l) |
| 386 | + |
| 387 | + // No path set — CatchCertChange should return immediately without panic |
| 388 | + ctx, cancel := context.WithCancel(context.Background()) |
| 389 | + defer cancel() |
| 390 | + c.CatchCertChange(ctx, 100*time.Millisecond) |
| 391 | +} |
| 392 | + |
| 393 | +func TestConfig_CatchCertChange_InlinePEM(t *testing.T) { |
| 394 | + l := test.NewLogger() |
| 395 | + |
| 396 | + configDir, err := os.MkdirTemp("", "config-inline-test") |
| 397 | + require.NoError(t, err) |
| 398 | + defer os.RemoveAll(configDir) |
| 399 | + |
| 400 | + // All inline PEM — no files to watch |
| 401 | + configYaml := `pki: |
| 402 | + cert: "-----BEGIN NEBULA CERTIFICATE-----\ndata\n-----END NEBULA CERTIFICATE-----" |
| 403 | + key: "-----BEGIN NEBULA X25519 PRIVATE KEY-----\ndata\n-----END NEBULA X25519 PRIVATE KEY-----" |
| 404 | + ca: "-----BEGIN NEBULA CERTIFICATE-----\ndata\n-----END NEBULA CERTIFICATE-----" |
| 405 | +` |
| 406 | + require.NoError(t, os.WriteFile(filepath.Join(configDir, "config.yaml"), []byte(configYaml), 0644)) |
| 407 | + |
| 408 | + c := NewC(l) |
| 409 | + require.NoError(t, c.Load(configDir)) |
| 410 | + |
| 411 | + ctx, cancel := context.WithCancel(context.Background()) |
| 412 | + defer cancel() |
| 413 | + |
| 414 | + // Should not panic or error — just logs a warning and returns |
| 415 | + c.CatchCertChange(ctx, 100*time.Millisecond) |
| 416 | +} |
0 commit comments