A Kubernetes operator that bridges Hardware Security Module (HSM) data storage with Kubernetes Secrets, providing true secret portability th
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

major refactor to try and use the hsmpool and DiscoveredDevices instead of the hsmdevice name

+310 -284
+12 -14
internal/agent/grpc_client.go
··· 34 34 35 35 // GRPCClient implements the HSM client interface using gRPC 36 36 type GRPCClient struct { 37 - client hsmv1.HSMAgentClient 38 - conn *grpc.ClientConn 39 - logger logr.Logger 40 - deviceName string 41 - endpoint string 42 - timeout time.Duration 37 + client hsmv1.HSMAgentClient 38 + conn *grpc.ClientConn 39 + logger logr.Logger 40 + endpoint string 41 + timeout time.Duration 43 42 } 44 43 45 44 // NewGRPCClient creates a new gRPC-based HSM client 46 - func NewGRPCClient(endpoint, deviceName string, logger logr.Logger) (*GRPCClient, error) { 45 + func NewGRPCClient(endpoint string, logger logr.Logger) (*GRPCClient, error) { 47 46 if endpoint == "" { 48 47 return nil, fmt.Errorf("endpoint cannot be empty") 49 48 } ··· 64 63 client := hsmv1.NewHSMAgentClient(conn) 65 64 66 65 return &GRPCClient{ 67 - client: client, 68 - conn: conn, 69 - logger: logger.WithName("grpc-client"), 70 - deviceName: deviceName, 71 - endpoint: endpoint, 72 - timeout: 30 * time.Second, 66 + client: client, 67 + conn: conn, 68 + logger: logger.WithName("grpc-client"), 69 + endpoint: endpoint, 70 + timeout: 30 * time.Second, 73 71 }, nil 74 72 } 75 73 ··· 84 82 return fmt.Errorf("failed to initialize gRPC client: %w", err) 85 83 } 86 84 87 - c.logger.Info("gRPC client initialized", "device", c.deviceName, "hsm_label", info.Label, "endpoint", c.endpoint) 85 + c.logger.Info("gRPC client initialized", "hsm_label", info.Label, "endpoint", c.endpoint) 88 86 return nil 89 87 } 90 88
+19 -23
internal/agent/grpc_client_test.go
··· 232 232 logger := logr.Discard() 233 233 234 234 t.Run("successful creation", func(t *testing.T) { 235 - client, err := NewGRPCClient("localhost:9090", "test-device", logger) 235 + client, err := NewGRPCClient("localhost:9090", logger) 236 236 require.NoError(t, err) 237 237 assert.NotNil(t, client) 238 - assert.Equal(t, "test-device", client.deviceName) 239 238 assert.Equal(t, "localhost:9090", client.endpoint) 240 239 assert.Equal(t, 30*time.Second, client.timeout) 241 240 ··· 244 243 }) 245 244 246 245 t.Run("empty endpoint", func(t *testing.T) { 247 - client, err := NewGRPCClient("", "test-device", logger) 246 + client, err := NewGRPCClient("", logger) 248 247 require.Error(t, err) 249 248 assert.Nil(t, client) 250 249 assert.Contains(t, err.Error(), "endpoint cannot be empty") ··· 253 252 t.Run("invalid host format", func(t *testing.T) { 254 253 // gRPC connections are lazy, so invalid format won't fail at creation 255 254 // but will fail when actually trying to connect during Initialize 256 - client, err := NewGRPCClient("invalid://bad-host", "test-device", logger) 255 + client, err := NewGRPCClient("invalid://bad-host", logger) 257 256 require.NoError(t, err) 258 257 require.NotNil(t, client) 259 258 ··· 287 286 }() 288 287 289 288 client := &GRPCClient{ 290 - client: hsmv1.NewHSMAgentClient(conn), 291 - conn: conn, 292 - logger: logger, 293 - deviceName: "test-device", 294 - endpoint: "passthrough:///bufnet", 295 - timeout: 5 * time.Second, 289 + client: hsmv1.NewHSMAgentClient(conn), 290 + conn: conn, 291 + logger: logger, 292 + endpoint: "passthrough:///bufnet", 293 + timeout: 5 * time.Second, 296 294 } 297 295 298 296 ctx := context.Background() ··· 471 469 }() 472 470 473 471 client := &GRPCClient{ 474 - client: hsmv1.NewHSMAgentClient(conn), 475 - conn: conn, 476 - logger: logger, 477 - deviceName: "test-device", 478 - endpoint: "passthrough:///bufnet", 479 - timeout: 5 * time.Second, 472 + client: hsmv1.NewHSMAgentClient(conn), 473 + conn: conn, 474 + logger: logger, 475 + endpoint: "passthrough:///bufnet", 476 + timeout: 5 * time.Second, 480 477 } 481 478 482 479 ctx := context.Background() ··· 532 529 533 530 t.Run("connection unavailable", func(t *testing.T) { 534 531 // Try to connect to non-existent server 535 - client, err := NewGRPCClient("localhost:99999", "test-device", logger) 532 + client, err := NewGRPCClient("localhost:99999", logger) 536 533 require.NoError(t, err) // Connection is lazy 537 534 538 535 // Initialize should fail ··· 572 569 }() 573 570 574 571 client := &GRPCClient{ 575 - client: hsmv1.NewHSMAgentClient(conn), 576 - conn: conn, 577 - logger: logger, 578 - deviceName: "test-device", 579 - endpoint: "passthrough:///bufnet", 580 - timeout: 5 * time.Second, 572 + client: hsmv1.NewHSMAgentClient(conn), 573 + conn: conn, 574 + logger: logger, 575 + endpoint: "passthrough:///bufnet", 576 + timeout: 5 * time.Second, 581 577 } 582 578 583 579 ctx := context.Background()
+3 -3
internal/agent/grpc_integration_test.go
··· 60 60 61 61 // Start a real gRPC server 62 62 logger := logr.Discard() 63 - grpcServer := NewGRPCServer(mockHSMClient, "test-device", 0, 0, logger) 63 + grpcServer := NewGRPCServer(mockHSMClient, 0, 0, logger) 64 64 65 65 // Find available port 66 66 listener, err := net.Listen("tcp", ":0") ··· 91 91 92 92 // Create gRPC client 93 93 endpoint := "localhost:" + strconv.Itoa(port) 94 - client, err := NewGRPCClient(endpoint, "test-device", logger) 94 + client, err := NewGRPCClient(endpoint, logger) 95 95 require.NoError(t, err) 96 96 defer func() { 97 97 assert.NoError(t, client.Close()) ··· 232 232 logger := logr.Discard() 233 233 234 234 // Test connection to non-existent server 235 - client, err := NewGRPCClient("localhost:99999", "test-device", logger) 235 + client, err := NewGRPCClient("localhost:99999", logger) 236 236 require.NoError(t, err) 237 237 defer func() { 238 238 assert.NoError(t, client.Close())
+4 -7
internal/agent/grpc_server.go
··· 42 42 hsmv1.UnimplementedHSMAgentServer 43 43 hsmClient hsm.Client 44 44 logger logr.Logger 45 - deviceName string 46 45 port int 47 46 healthPort int 48 47 startTime time.Time 49 48 } 50 49 51 50 // NewGRPCServer creates a new HSM agent gRPC server 52 - func NewGRPCServer(hsmClient hsm.Client, deviceName string, port, healthPort int, logger logr.Logger) *GRPCServer { 51 + func NewGRPCServer(hsmClient hsm.Client, port, healthPort int, logger logr.Logger) *GRPCServer { 53 52 return &GRPCServer{ 54 53 hsmClient: hsmClient, 55 54 logger: logger.WithName("grpc-server"), 56 - deviceName: deviceName, 57 55 port: port, 58 56 healthPort: healthPort, 59 57 startTime: time.Now(), ··· 85 83 grpcServer.GracefulStop() 86 84 }() 87 85 88 - s.logger.Info("Starting HSM agent gRPC server", "port", s.port, "device", s.deviceName) 86 + s.logger.Info("Starting HSM agent gRPC server", "port", s.port) 89 87 return grpcServer.Serve(lis) 90 88 } 91 89 ··· 367 365 368 366 w.Header().Set("Content-Type", "application/json") 369 367 uptime := time.Since(s.startTime).String() 370 - if _, err := fmt.Fprintf(w, `{"status":"%s","deviceName":"%s","hsmConnected":%t,"uptime":"%s","timestamp":"%s"}`, 371 - healthStatus, s.deviceName, hsmConnected, uptime, time.Now().Format(time.RFC3339)); err != nil { 368 + if _, err := fmt.Fprintf(w, `{"status":"%s","hsmConnected":%t,"uptime":"%s","timestamp":"%s"}`, 369 + healthStatus, hsmConnected, uptime, time.Now().Format(time.RFC3339)); err != nil { 372 370 s.logger.Error(err, "Failed to write health response") 373 371 } 374 372 } ··· 398 396 // Extract request-specific details 399 397 logFields := []interface{}{ 400 398 "method", info.FullMethod, 401 - "device", s.deviceName, 402 399 } 403 400 404 401 // Add request-specific fields based on the method
+16 -17
internal/agent/grpc_server_test.go
··· 32 32 mockClient := hsm.NewMockClient() 33 33 logger := logr.Discard() 34 34 35 - server := NewGRPCServer(mockClient, "test-device", 9090, 8080, logger) 35 + server := NewGRPCServer(mockClient, 9090, 8080, logger) 36 36 37 37 assert.NotNil(t, server) 38 38 assert.Equal(t, mockClient, server.hsmClient) 39 - assert.Equal(t, "test-device", server.deviceName) 40 39 assert.Equal(t, 9090, server.port) 41 40 assert.Equal(t, 8080, server.healthPort) 42 41 } ··· 44 43 func TestGRPCServerGetInfo(t *testing.T) { 45 44 mockClient := hsm.NewMockClient() 46 45 logger := logr.Discard() 47 - server := NewGRPCServer(mockClient, "test-device", 9090, 8080, logger) 46 + server := NewGRPCServer(mockClient, 9090, 8080, logger) 48 47 49 48 ctx := context.Background() 50 49 ··· 65 64 66 65 t.Run("client not connected", func(t *testing.T) { 67 66 // Create server with nil client 68 - server := NewGRPCServer(nil, "test-device", 9090, 8080, logger) 67 + server := NewGRPCServer(nil, 9090, 8080, logger) 69 68 70 69 resp, err := server.GetInfo(ctx, &hsmv1.GetInfoRequest{}) 71 70 assert.Error(t, err) ··· 77 76 func TestGRPCServerReadSecret(t *testing.T) { 78 77 mockClient := hsm.NewMockClient() 79 78 logger := logr.Discard() 80 - server := NewGRPCServer(mockClient, "test-device", 9090, 8080, logger) 79 + server := NewGRPCServer(mockClient, 9090, 8080, logger) 81 80 82 81 ctx := context.Background() 83 82 err := mockClient.Initialize(ctx, hsm.Config{}) ··· 109 108 }) 110 109 111 110 t.Run("client not connected", func(t *testing.T) { 112 - server := NewGRPCServer(nil, "test-device", 9090, 8080, logger) 111 + server := NewGRPCServer(nil, 9090, 8080, logger) 113 112 req := &hsmv1.ReadSecretRequest{Path: "test-secret"} 114 113 resp, err := server.ReadSecret(ctx, req) 115 114 assert.Error(t, err) ··· 121 120 func TestGRPCServerWriteSecret(t *testing.T) { 122 121 mockClient := hsm.NewMockClient() 123 122 logger := logr.Discard() 124 - server := NewGRPCServer(mockClient, "test-device", 9090, 8080, logger) 123 + server := NewGRPCServer(mockClient, 9090, 8080, logger) 125 124 126 125 ctx := context.Background() 127 126 err := mockClient.Initialize(ctx, hsm.Config{}) ··· 176 175 }) 177 176 178 177 t.Run("client not connected", func(t *testing.T) { 179 - server := NewGRPCServer(nil, "test-device", 9090, 8080, logger) 178 + server := NewGRPCServer(nil, 9090, 8080, logger) 180 179 req := &hsmv1.WriteSecretRequest{ 181 180 Path: "test-secret", 182 181 SecretData: &hsmv1.SecretData{ ··· 194 193 func TestGRPCServerWriteSecretWithMetadata(t *testing.T) { 195 194 mockClient := hsm.NewMockClient() 196 195 logger := logr.Discard() 197 - server := NewGRPCServer(mockClient, "test-device", 9090, 8080, logger) 196 + server := NewGRPCServer(mockClient, 9090, 8080, logger) 198 197 199 198 ctx := context.Background() 200 199 err := mockClient.Initialize(ctx, hsm.Config{}) ··· 280 279 func TestGRPCServerReadMetadata(t *testing.T) { 281 280 mockClient := hsm.NewMockClient() 282 281 logger := logr.Discard() 283 - server := NewGRPCServer(mockClient, "test-device", 9090, 8080, logger) 282 + server := NewGRPCServer(mockClient, 9090, 8080, logger) 284 283 285 284 ctx := context.Background() 286 285 err := mockClient.Initialize(ctx, hsm.Config{}) ··· 335 334 func TestGRPCServerDeleteSecret(t *testing.T) { 336 335 mockClient := hsm.NewMockClient() 337 336 logger := logr.Discard() 338 - server := NewGRPCServer(mockClient, "test-device", 9090, 8080, logger) 337 + server := NewGRPCServer(mockClient, 9090, 8080, logger) 339 338 340 339 ctx := context.Background() 341 340 err := mockClient.Initialize(ctx, hsm.Config{}) ··· 375 374 func TestGRPCServerListSecrets(t *testing.T) { 376 375 mockClient := hsm.NewMockClient() 377 376 logger := logr.Discard() 378 - server := NewGRPCServer(mockClient, "test-device", 9090, 8080, logger) 377 + server := NewGRPCServer(mockClient, 9090, 8080, logger) 379 378 380 379 ctx := context.Background() 381 380 err := mockClient.Initialize(ctx, hsm.Config{}) ··· 418 417 func TestGRPCServerGetChecksum(t *testing.T) { 419 418 mockClient := hsm.NewMockClient() 420 419 logger := logr.Discard() 421 - server := NewGRPCServer(mockClient, "test-device", 9090, 8080, logger) 420 + server := NewGRPCServer(mockClient, 9090, 8080, logger) 422 421 423 422 ctx := context.Background() 424 423 err := mockClient.Initialize(ctx, hsm.Config{}) ··· 452 451 func TestGRPCServerIsConnected(t *testing.T) { 453 452 mockClient := hsm.NewMockClient() 454 453 logger := logr.Discard() 455 - server := NewGRPCServer(mockClient, "test-device", 9090, 8080, logger) 454 + server := NewGRPCServer(mockClient, 9090, 8080, logger) 456 455 457 456 ctx := context.Background() 458 457 ··· 477 476 }) 478 477 479 478 t.Run("nil client", func(t *testing.T) { 480 - server := NewGRPCServer(nil, "test-device", 9090, 8080, logger) 479 + server := NewGRPCServer(nil, 9090, 8080, logger) 481 480 482 481 req := &hsmv1.IsConnectedRequest{} 483 482 resp, err := server.IsConnected(ctx, req) ··· 489 488 func TestGRPCServerHealth(t *testing.T) { 490 489 mockClient := hsm.NewMockClient() 491 490 logger := logr.Discard() 492 - server := NewGRPCServer(mockClient, "test-device", 9090, 8080, logger) 491 + server := NewGRPCServer(mockClient, 9090, 8080, logger) 493 492 494 493 ctx := context.Background() 495 494 ··· 516 515 }) 517 516 518 517 t.Run("nil client", func(t *testing.T) { 519 - server := NewGRPCServer(nil, "test-device", 9090, 8080, logger) 518 + server := NewGRPCServer(nil, 9090, 8080, logger) 520 519 521 520 req := &hsmv1.HealthRequest{} 522 521 resp, err := server.Health(ctx, req)
+51 -37
internal/agent/manager.go
··· 871 871 } 872 872 873 873 // GetAgentPodIPs returns all agent pod IPs for a device type from HSMPool 874 - func (m *Manager) GetAgentPodIPs(ctx context.Context, deviceName, namespace string) ([]string, error) { 875 - // Get HSMPool for this device 876 - poolName := deviceName + "-pool" 877 - var hsmPool hsmv1alpha1.HSMPool 878 - if err := m.Get(ctx, types.NamespacedName{ 879 - Name: poolName, 880 - Namespace: namespace, 881 - }, &hsmPool); err != nil { 882 - return nil, fmt.Errorf("failed to get HSMPool %s: %w", poolName, err) 883 - } 874 + func (m *Manager) GetAgentPodIPs(hsmPool *hsmv1alpha1.HSMPool) ([]string, error) { 875 + // Extract device name from pool name (remove "-pool" suffix) 876 + deviceName := strings.TrimSuffix(hsmPool.Name, "-pool") 884 877 885 878 m.mu.RLock() 886 879 defer m.mu.RUnlock() ··· 896 889 } 897 890 898 891 if len(allPodIPs) == 0 { 899 - return nil, fmt.Errorf("no active agents found for device %s in pool %s", deviceName, poolName) 892 + return nil, fmt.Errorf("no active agents found for device %s in pool %s", deviceName, hsmPool.Name) 900 893 } 901 894 902 895 return allPodIPs, nil ··· 935 928 } 936 929 937 930 // GetGRPCEndpoints returns gRPC endpoints for all agent pods of a device 938 - func (m *Manager) GetGRPCEndpoints(ctx context.Context, deviceName, namespace string) ([]string, error) { 939 - podIPs, err := m.GetAgentPodIPs(ctx, deviceName, namespace) 931 + func (m *Manager) GetGRPCEndpoints(hsmPool *hsmv1alpha1.HSMPool) ([]string, error) { 932 + podIPs, err := m.GetAgentPodIPs(hsmPool) 940 933 if err != nil { 941 934 return nil, err 942 935 } ··· 949 942 return endpoints, nil 950 943 } 951 944 952 - // CreateGRPCClient creates a gRPC client for the first available agent pod of a device 953 - func (m *Manager) CreateGRPCClient(ctx context.Context, deviceName, namespace string, logger logr.Logger) (hsm.Client, error) { 954 - endpoints, err := m.GetGRPCEndpoints(ctx, deviceName, namespace) 955 - if err != nil { 956 - return nil, err 945 + // CreateGRPCClient creates a gRPC client to the specific agent pod for the given DiscoveredDevice 946 + func (m *Manager) CreateGRPCClient(ctx context.Context, device hsmv1alpha1.DiscoveredDevice, logger logr.Logger) (hsm.Client, error) { 947 + // Find the specific agent pod using labels based on the device's serial number 948 + var podList corev1.PodList 949 + listOpts := []client.ListOption{ 950 + client.MatchingLabels{ 951 + "app.kubernetes.io/name": "hsm-agent", 952 + "app.kubernetes.io/component": "hsm-agent", 953 + "hsm.j5t.io/serial-number": device.SerialNumber, 954 + }, 957 955 } 958 956 959 - if len(endpoints) == 0 { 960 - return nil, fmt.Errorf("no agent endpoints available for device %s", deviceName) 957 + if err := m.List(ctx, &podList, listOpts...); err != nil { 958 + return nil, fmt.Errorf("failed to list agent pods for device %s: %w", device.SerialNumber, err) 959 + } 960 + 961 + if len(podList.Items) == 0 { 962 + return nil, fmt.Errorf("no agent pod found for device with serial number %s", device.SerialNumber) 961 963 } 962 964 963 - // Use the first endpoint for single client 964 - grpcClient, err := NewGRPCClient(endpoints[0], deviceName, logger) 965 + // Find the first running pod 966 + var targetPod *corev1.Pod 967 + for i := range podList.Items { 968 + pod := &podList.Items[i] 969 + if pod.Status.Phase == corev1.PodRunning && len(pod.Status.PodIPs) > 0 { 970 + targetPod = pod 971 + break 972 + } 973 + } 974 + 975 + if targetPod == nil { 976 + return nil, fmt.Errorf("no running agent pod found for device with serial number %s", device.SerialNumber) 977 + } 978 + 979 + // Get the pod IP and create gRPC endpoint 980 + podIP := targetPod.Status.PodIPs[0].IP 981 + endpoint := fmt.Sprintf("%s:%d", podIP, AgentPort) 982 + 983 + // Create gRPC client 984 + grpcClient, err := NewGRPCClient(endpoint, logger) 965 985 if err != nil { 966 - return nil, fmt.Errorf("failed to create gRPC client for %s: %w", endpoints[0], err) 986 + return nil, fmt.Errorf("failed to create gRPC client for %s: %w", endpoint, err) 967 987 } 968 988 969 989 // Test the connection ··· 971 991 if err := grpcClient.Close(); err != nil { 972 992 logger.Error(err, "Failed to close gRPC client after failed initialization") 973 993 } 974 - return nil, fmt.Errorf("failed to initialize gRPC client for %s: %w", endpoints[0], err) 994 + return nil, fmt.Errorf("failed to initialize gRPC client for %s: %w", endpoint, err) 975 995 } 976 996 977 997 return grpcClient, nil 978 998 } 979 999 980 - // GetAvailableDevices finds all devices with ready HSMPools and active agents 981 - func (m *Manager) GetAvailableDevices(ctx context.Context, namespace string) ([]string, error) { 982 - // List all HSMPools cluster-wide to find all with active agents 1000 + // GetAvailableDevices finds all devices with ready HSMPools 1001 + func (m *Manager) GetAvailableDevices(ctx context.Context, namespace string) ([]hsmv1alpha1.DiscoveredDevice, error) { 1002 + // List all HSMPools cluster-wide to find all ready pools 983 1003 var hsmPoolList hsmv1alpha1.HSMPoolList 984 1004 if err := m.List(ctx, &hsmPoolList); err != nil { 985 1005 return nil, fmt.Errorf("failed to list HSM pools: %w", err) 986 1006 } 987 1007 988 - var availableDevices []string 989 - // Check all pools that have active agents 1008 + var availableDevices []hsmv1alpha1.DiscoveredDevice 1009 + // Check all pools that are in Ready phase 990 1010 for _, pool := range hsmPoolList.Items { 991 1011 if pool.Status.Phase != hsmv1alpha1.HSMPoolPhaseReady { 992 1012 continue 993 1013 } 994 1014 995 - // Extract device name from pool name (remove "-pool" suffix) 996 - deviceName := strings.TrimSuffix(pool.Name, "-pool") 997 - 998 - // Use the HSMPool's namespace for agent lookup 999 - if podIPs, err := m.GetAgentPodIPs(ctx, deviceName, pool.Namespace); err == nil && len(podIPs) > 0 { 1000 - availableDevices = append(availableDevices, deviceName) 1001 - } 1015 + availableDevices = append(availableDevices, pool.Status.AggregatedDevices...) 1002 1016 } 1003 1017 1004 1018 if len(availableDevices) == 0 { 1005 - return nil, fmt.Errorf("no available HSM agents found") 1019 + return nil, fmt.Errorf("no available HSM devices found") 1006 1020 } 1007 1021 1008 1022 return availableDevices, nil
+42 -11
internal/agent/manager_test.go
··· 296 296 name string 297 297 hsmPools []*hsmv1alpha1.HSMPool 298 298 agentPods []*corev1.Pod 299 - expectedDevices []string 299 + expectedDevices []hsmv1alpha1.DiscoveredDevice 300 300 expectError bool 301 301 }{ 302 302 { 303 - name: "ready pool but no active agents", 303 + name: "ready pool returns device name regardless of agents", 304 304 hsmPools: []*hsmv1alpha1.HSMPool{ 305 305 { 306 306 ObjectMeta: metav1.ObjectMeta{ ··· 319 319 }, 320 320 }, 321 321 }, 322 - agentPods: []*corev1.Pod{}, 323 - expectedDevices: []string{}, 324 - expectError: true, 322 + agentPods: []*corev1.Pod{}, 323 + expectedDevices: []hsmv1alpha1.DiscoveredDevice{ 324 + { 325 + DevicePath: "/dev/bus/usb/001/015", 326 + Available: true, 327 + SerialNumber: "ABC123", 328 + }, 329 + }, 330 + expectError: false, 325 331 }, 326 332 { 327 - name: "multiple ready pools but no active agents", 333 + name: "multiple ready pools return multiple device names", 328 334 hsmPools: []*hsmv1alpha1.HSMPool{ 329 335 { 330 336 ObjectMeta: metav1.ObjectMeta{ ··· 333 339 }, 334 340 Status: hsmv1alpha1.HSMPoolStatus{ 335 341 Phase: hsmv1alpha1.HSMPoolPhaseReady, 342 + AggregatedDevices: []hsmv1alpha1.DiscoveredDevice{ 343 + { 344 + DevicePath: "/dev/bus/usb/001/016", 345 + Available: true, 346 + SerialNumber: "DEF456", 347 + }, 348 + }, 336 349 }, 337 350 }, 338 351 { ··· 342 355 }, 343 356 Status: hsmv1alpha1.HSMPoolStatus{ 344 357 Phase: hsmv1alpha1.HSMPoolPhaseReady, 358 + AggregatedDevices: []hsmv1alpha1.DiscoveredDevice{ 359 + { 360 + DevicePath: "/dev/bus/usb/001/017", 361 + Available: true, 362 + SerialNumber: "GHI789", 363 + }, 364 + }, 345 365 }, 346 366 }, 347 367 }, 348 - agentPods: []*corev1.Pod{}, 349 - expectedDevices: []string{}, 350 - expectError: true, 368 + agentPods: []*corev1.Pod{}, 369 + expectedDevices: []hsmv1alpha1.DiscoveredDevice{ 370 + { 371 + DevicePath: "/dev/bus/usb/001/016", 372 + Available: true, 373 + SerialNumber: "DEF456", 374 + }, 375 + { 376 + DevicePath: "/dev/bus/usb/001/017", 377 + Available: true, 378 + SerialNumber: "GHI789", 379 + }, 380 + }, 381 + expectError: false, 351 382 }, 352 383 { 353 384 name: "pool not ready - should be excluded", ··· 363 394 }, 364 395 }, 365 396 agentPods: []*corev1.Pod{}, 366 - expectedDevices: []string{}, 397 + expectedDevices: []hsmv1alpha1.DiscoveredDevice{}, 367 398 expectError: true, 368 399 }, 369 400 { 370 401 name: "no pools", 371 402 hsmPools: []*hsmv1alpha1.HSMPool{}, 372 403 agentPods: []*corev1.Pod{}, 373 - expectedDevices: []string{}, 404 + expectedDevices: []hsmv1alpha1.DiscoveredDevice{}, 374 405 expectError: true, 375 406 }, 376 407 }
+4 -4
internal/api/mock_test.go
··· 19 19 import ( 20 20 "context" 21 21 22 + "github.com/evanjarrett/hsm-secrets-operator/api/v1alpha1" 23 + "github.com/evanjarrett/hsm-secrets-operator/internal/hsm" 22 24 "github.com/go-logr/logr" 23 25 "github.com/stretchr/testify/mock" 24 - 25 - "github.com/evanjarrett/hsm-secrets-operator/internal/hsm" 26 26 ) 27 27 28 28 // MockAgentManager provides a mock implementation of the agent manager interface ··· 35 35 return args.Get(0).([]string), args.Error(1) 36 36 } 37 37 38 - func (m *MockAgentManager) CreateGRPCClient(ctx context.Context, deviceName, namespace string, logger logr.Logger) (hsm.Client, error) { 39 - args := m.Called(ctx, deviceName, namespace, logger) 38 + func (m *MockAgentManager) CreateGRPCClient(ctx context.Context, device v1alpha1.DiscoveredDevice, logger logr.Logger) (hsm.Client, error) { 39 + args := m.Called(ctx, device, logger) 40 40 if args.Get(0) == nil { 41 41 return nil, args.Error(1) 42 42 }
+12 -12
internal/api/proxy_client.go
··· 62 62 type ProxyClient struct { 63 63 server *Server 64 64 logger logr.Logger 65 - grpcClients map[string]hsm.Client // deviceName -> gRPC client 65 + grpcClients map[string]hsm.Client // serialNumber -> gRPC client 66 66 clientsMutex sync.RWMutex 67 67 } 68 68 ··· 256 256 p.clientsMutex.Lock() 257 257 defer p.clientsMutex.Unlock() 258 258 259 - for _, deviceName := range devices { 259 + for _, device := range devices { 260 260 // Try to get existing client for this device 261 - if client, exists := p.grpcClients[deviceName]; exists && client.IsConnected() { 262 - clients[deviceName] = client 261 + if client, exists := p.grpcClients[device.SerialNumber]; exists && client.IsConnected() { 262 + clients[device.SerialNumber] = client 263 263 continue 264 264 } 265 265 266 266 // Close existing client for this device if it exists but is not connected 267 - if oldClient, exists := p.grpcClients[deviceName]; exists { 267 + if oldClient, exists := p.grpcClients[device.SerialNumber]; exists { 268 268 if closeErr := oldClient.Close(); closeErr != nil { 269 - p.logger.V(1).Info("Error closing old gRPC client", "device", deviceName, "error", closeErr) 269 + p.logger.V(1).Info("Error closing old gRPC client", "node", device.NodeName, "serialNumber", device.SerialNumber, "error", closeErr) 270 270 } 271 - delete(p.grpcClients, deviceName) 271 + delete(p.grpcClients, device.SerialNumber) 272 272 } 273 273 274 274 // Create new gRPC client 275 - grpcClient, err := p.server.createGRPCClient(c.Request.Context(), deviceName, namespace) 275 + grpcClient, err := p.server.createGRPCClient(c.Request.Context(), device) 276 276 if err != nil { 277 - p.logger.V(1).Info("Failed to create gRPC client", "device", deviceName, "error", err) 277 + p.logger.V(1).Info("Failed to create gRPC client", "node", device.NodeName, "serialNumber", device.SerialNumber, "error", err) 278 278 continue 279 279 } 280 280 281 281 // Cache and include the client 282 - p.grpcClients[deviceName] = grpcClient 283 - clients[deviceName] = grpcClient 284 - p.logger.V(1).Info("Created new gRPC client", "device", deviceName) 282 + p.grpcClients[device.SerialNumber] = grpcClient 283 + clients[device.SerialNumber] = grpcClient 284 + p.logger.V(1).Info("Created new gRPC client", "node", device.NodeName, "serialNumber", device.SerialNumber) 285 285 } 286 286 287 287 if len(clients) == 0 {
+5 -4
internal/api/server.go
··· 27 27 "github.com/go-playground/validator/v10" 28 28 "sigs.k8s.io/controller-runtime/pkg/client" 29 29 30 + "github.com/evanjarrett/hsm-secrets-operator/api/v1alpha1" 30 31 "github.com/evanjarrett/hsm-secrets-operator/internal/agent" 31 32 "github.com/evanjarrett/hsm-secrets-operator/internal/hsm" 32 33 ) ··· 165 166 } 166 167 167 168 // getAllAvailableAgents finds all available HSM agents for mirroring operations 168 - func (s *Server) getAllAvailableAgents(ctx context.Context, namespace string) ([]string, error) { 169 + func (s *Server) getAllAvailableAgents(ctx context.Context, namespace string) ([]v1alpha1.DiscoveredDevice, error) { 169 170 if s.agentManager == nil { 170 171 return nil, fmt.Errorf("agent manager not available") 171 172 } ··· 174 175 } 175 176 176 177 // createGRPCClient creates a gRPC client for the specified device using AgentManager 177 - func (s *Server) createGRPCClient(ctx context.Context, deviceName, namespace string) (hsm.Client, error) { 178 + func (s *Server) createGRPCClient(ctx context.Context, device v1alpha1.DiscoveredDevice) (hsm.Client, error) { 178 179 // Use the AgentManager to create a gRPC client directly 179 180 if s.agentManager == nil { 180 181 return nil, fmt.Errorf("agent manager not available") 181 182 } 182 183 183 184 // Create gRPC client using AgentManager's existing method 184 - grpcClient, err := s.agentManager.CreateGRPCClient(ctx, deviceName, namespace, s.logger) 185 + grpcClient, err := s.agentManager.CreateGRPCClient(ctx, device, s.logger) 185 186 if err != nil { 186 - return nil, fmt.Errorf("failed to create gRPC client for device %s: %w", deviceName, err) 187 + return nil, fmt.Errorf("failed to create gRPC client for node %s: %w", device.NodeName, err) 187 188 } 188 189 189 190 return grpcClient, nil
+7 -1
internal/api/server_test.go
··· 305 305 } 306 306 307 307 ctx := context.Background() 308 - client, err := server.createGRPCClient(ctx, "test-device", "test-ns") 308 + testDevice := hsmv1alpha1.DiscoveredDevice{ 309 + SerialNumber: "test-device", 310 + DevicePath: "/dev/test/test-device", 311 + NodeName: "test-node", 312 + Available: true, 313 + } 314 + client, err := server.createGRPCClient(ctx, testDevice) // TODO Fix Test 309 315 310 316 assert.Error(t, err) 311 317 assert.Contains(t, err.Error(), "agent manager not available")
+3 -4
internal/controller/hsmsecret_controller.go
··· 161 161 return ctrl.Result{RequeueAfter: time.Minute * 2}, nil 162 162 } 163 163 164 - // Connect to first available device 165 - deviceName := devices[0] 166 - grpcClient, err := r.AgentManager.CreateGRPCClient(ctx, deviceName, r.OperatorNamespace, logger) 164 + device := devices[0] 165 + grpcClient, err := r.AgentManager.CreateGRPCClient(ctx, device, logger) 167 166 if err != nil { 168 - logger.Error(err, "Failed to create gRPC client", "device", deviceName) 167 + logger.Error(err, "Failed to create gRPC client", "node", device.NodeName) 169 168 return ctrl.Result{RequeueAfter: time.Minute * 2}, nil 170 169 } 171 170 defer func() {
+15 -3
internal/controller/hsmsecret_grpc_test.go
··· 83 83 84 84 // Start gRPC server 85 85 logger := logr.Discard() 86 - grpcServer := agent.NewGRPCServer(mockHSMClient, "test-hsm-device", 0, 0, logger) 86 + grpcServer := agent.NewGRPCServer(mockHSMClient, 0, 0, logger) 87 87 88 88 // Start server on agent port 9090 for testing 89 89 server := grpc.NewServer() ··· 225 225 require.NoError(t, err) 226 226 227 227 // Verify data was written to HSM via gRPC 228 - agentClient, err := agentManager.CreateGRPCClient(ctx, "test-hsm-device", "default", logger) 228 + testDevice := hsmv1alpha1.DiscoveredDevice{ 229 + SerialNumber: "test-device", 230 + DevicePath: "/dev/test/test-device", 231 + NodeName: "test-node", 232 + Available: true, 233 + } 234 + agentClient, err := agentManager.CreateGRPCClient(ctx, testDevice, logger) // TODO Fix Test 229 235 require.NoError(t, err) 230 236 defer func() { 231 237 assert.NoError(t, agentClient.Close()) ··· 340 346 require.NoError(t, err) 341 347 342 348 // Verify data exists in HSM 343 - agentClient, err := agentManager.CreateGRPCClient(ctx, "test-hsm-device", "default", logger) 349 + testDevice2 := hsmv1alpha1.DiscoveredDevice{ 350 + SerialNumber: "test-device-2", 351 + DevicePath: "/dev/test/test-device-2", 352 + NodeName: "test-node", 353 + Available: true, 354 + } 355 + agentClient, err := agentManager.CreateGRPCClient(ctx, testDevice2, logger) // TODO Fix Test 344 356 require.NoError(t, err) 345 357 defer func() { 346 358 assert.NoError(t, agentClient.Close())
+70 -83
internal/mirror/manager.go
··· 32 32 33 33 // AgentManagerInterface defines the interface for HSM agent management used by mirror 34 34 type AgentManagerInterface interface { 35 - CreateGRPCClient(ctx context.Context, deviceName, namespace string, logger logr.Logger) (hsm.Client, error) 35 + CreateGRPCClient(ctx context.Context, device hsmv1alpha1.DiscoveredDevice, logger logr.Logger) (hsm.Client, error) 36 + GetAvailableDevices(ctx context.Context, namespace string) ([]hsmv1alpha1.DiscoveredDevice, error) 36 37 } 37 38 38 39 // MirrorManager handles multi-device HSM mirroring and conflict resolution ··· 111 112 ) 112 113 113 114 // buildSecretInventory builds a comprehensive inventory of secrets across all devices 114 - func (mm *MirrorManager) buildSecretInventory(ctx context.Context, secretPaths []string, devices []string, operatorNamespace string, logger logr.Logger) (map[string]*SecretInventory, error) { 115 + func (mm *MirrorManager) buildSecretInventory(ctx context.Context, secretPaths []string, devices []hsmv1alpha1.DiscoveredDevice, logger logr.Logger) (map[string]*SecretInventory, error) { 115 116 inventory := make(map[string]*SecretInventory) 116 117 117 118 // Initialize inventory entries for requested secrets ··· 123 124 } 124 125 125 126 // Check each device for the presence and state of each secret 126 - for _, deviceName := range devices { 127 - logger.Info("Checking device for secrets", "device", deviceName, "secretCount", len(secretPaths)) 127 + for _, device := range devices { 128 + deviceId := device.SerialNumber 129 + logger.Info("Checking device for secrets", "device", deviceId, "secretCount", len(secretPaths)) 128 130 129 131 // Create gRPC client for this device (agents are in operator namespace) 130 - grpcClient, err := mm.agentManager.CreateGRPCClient(ctx, deviceName, operatorNamespace, logger) 132 + grpcClient, err := mm.agentManager.CreateGRPCClient(ctx, device, logger) 131 133 if err != nil { 132 - logger.Error(err, "Failed to create gRPC client", "device", deviceName) 134 + logger.Error(err, "Failed to create gRPC client", "device", deviceId) 133 135 // Mark all secrets as having an error on this device 134 136 for secretPath := range inventory { 135 - inventory[secretPath].DeviceStates[deviceName] = &SecretState{ 137 + inventory[secretPath].DeviceStates[deviceId] = &SecretState{ 136 138 Present: false, 137 139 Version: 0, 138 140 Timestamp: time.Now(), ··· 148 150 if closeErr := client.Close(); closeErr != nil { 149 151 logger.V(1).Info("Failed to close gRPC client", "device", device, "error", closeErr) 150 152 } 151 - }(grpcClient, deviceName) 153 + }(grpcClient, deviceId) 152 154 153 155 // Check if device is connected 154 156 if !grpcClient.IsConnected() { 155 - logger.V(1).Info("Device not connected", "device", deviceName) 157 + logger.V(1).Info("Device not connected", "device", deviceId) 156 158 for secretPath := range inventory { 157 - inventory[secretPath].DeviceStates[deviceName] = &SecretState{ 159 + inventory[secretPath].DeviceStates[deviceId] = &SecretState{ 158 160 Present: false, 159 161 Version: 0, 160 162 Timestamp: time.Now(), ··· 181 183 data, err := grpcClient.ReadSecret(ctx, secretPath) 182 184 if err != nil { 183 185 // Secret doesn't exist on this device 184 - logger.V(1).Info("Secret not found on device", "device", deviceName, "secret", secretPath) 186 + logger.V(1).Info("Secret not found on device", "device", deviceId, "secret", secretPath) 185 187 state.Error = fmt.Errorf("secret not found: %w", err) 186 188 } else { 187 189 // Secret exists, calculate checksum 188 190 state.Present = true 189 191 state.Checksum = mm.calculateChecksum(data) 190 - logger.V(1).Info("Secret found on device", "device", deviceName, "secret", secretPath, "checksum", state.Checksum[:8]) 192 + logger.V(1).Info("Secret found on device", "device", deviceId, "secret", secretPath, "checksum", state.Checksum[:8]) 191 193 192 194 // Try to read metadata 193 195 metadata, metaErr := grpcClient.ReadMetadata(ctx, secretPath) ··· 204 206 state.Timestamp = timestamp 205 207 } 206 208 } 207 - logger.V(1).Info("Metadata found", "device", deviceName, "secret", secretPath, 209 + logger.V(1).Info("Metadata found", "device", deviceId, "secret", secretPath, 208 210 "version", state.Version, "timestamp", state.Timestamp.Format(time.RFC3339)) 209 211 } else { 210 - logger.V(1).Info("No metadata found", "device", deviceName, "secret", secretPath) 212 + logger.V(1).Info("No metadata found", "device", deviceId, "secret", secretPath) 211 213 } 212 214 } 213 215 214 - inventory[secretPath].DeviceStates[deviceName] = state 216 + inventory[secretPath].DeviceStates[deviceId] = state 215 217 } 216 218 } 217 219 ··· 397 399 } 398 400 399 401 // executeMirrorPlans executes sync operations for all planned secret synchronizations 400 - func (mm *MirrorManager) executeMirrorPlans(ctx context.Context, plans []*SecretMirrorPlan, operatorNamespace string, logger logr.Logger) *MirrorResult { 402 + func (mm *MirrorManager) executeMirrorPlans(ctx context.Context, plans []*SecretMirrorPlan, deviceLookup map[string]hsmv1alpha1.DiscoveredDevice, logger logr.Logger) *MirrorResult { 401 403 result := &MirrorResult{ 402 404 Success: true, 403 405 SecretsProcessed: len(plans), ··· 409 411 } 410 412 411 413 for _, plan := range plans { 412 - secretResult := mm.executeMirrorPlan(ctx, plan, operatorNamespace, logger) 414 + secretResult := mm.executeMirrorPlan(ctx, plan, deviceLookup, logger) 413 415 result.SecretResults[plan.SecretPath] = secretResult 414 416 415 417 if secretResult.Success { ··· 433 435 } 434 436 435 437 // executeMirrorPlan executes a single secret sync plan 436 - func (mm *MirrorManager) executeMirrorPlan(ctx context.Context, plan *SecretMirrorPlan, operatorNamespace string, logger logr.Logger) SecretMirrorResult { 438 + func (mm *MirrorManager) executeMirrorPlan(ctx context.Context, plan *SecretMirrorPlan, deviceLookup map[string]hsmv1alpha1.DiscoveredDevice, logger logr.Logger) SecretMirrorResult { 437 439 result := SecretMirrorResult{ 438 440 SecretPath: plan.SecretPath, 439 441 SourceDevice: plan.SourceDevice, ··· 452 454 } 453 455 454 456 // Get source data and metadata 455 - sourceData, sourceMetadata, err := mm.readSecretWithMetadata(ctx, plan.SourceDevice, plan.SecretPath, operatorNamespace, logger) 457 + sourceDevice, ok := deviceLookup[plan.SourceDevice] 458 + if !ok { 459 + result.Error = fmt.Errorf("source device not found in lookup: %s", plan.SourceDevice) 460 + return result 461 + } 462 + sourceData, sourceMetadata, err := mm.readSecretWithMetadata(ctx, sourceDevice, plan.SecretPath, logger) 456 463 if err != nil { 457 464 result.Error = fmt.Errorf("failed to read source secret: %w", err) 458 465 logger.Error(err, "Failed to read source secret", "device", plan.SourceDevice, "secret", plan.SecretPath) ··· 482 489 if plan.MirrorType == MirrorTypeRestoreMetadata { 483 490 if sourceMetadata == nil || sourceMetadata.Labels == nil || sourceMetadata.Labels["sync.version"] == "" { 484 491 logger.Info("Restoring metadata on source device", "device", plan.SourceDevice, "secret", plan.SecretPath) 485 - if err := mm.writeSecretWithMetadata(ctx, plan.SourceDevice, plan.SecretPath, sourceData, syncMetadata, operatorNamespace, logger); err != nil { 492 + if err := mm.writeSecretWithMetadata(ctx, sourceDevice, plan.SecretPath, sourceData, syncMetadata, logger); err != nil { 486 493 result.Error = fmt.Errorf("failed to restore metadata on source: %w", err) 487 494 return result 488 495 } ··· 491 498 492 499 // Sync to target devices 493 500 successfulTargets := 0 494 - for _, targetDevice := range plan.TargetDevices { 495 - logger.Info("Syncing secret to target device", "secret", plan.SecretPath, "source", plan.SourceDevice, "target", targetDevice, "version", newVersion) 501 + for _, targetDeviceId := range plan.TargetDevices { 502 + targetDevice, ok := deviceLookup[targetDeviceId] 503 + if !ok { 504 + logger.Error(fmt.Errorf("target device not found in lookup: %s", targetDeviceId), "Failed to find target device", "target", targetDeviceId, "secret", plan.SecretPath) 505 + continue 506 + } 507 + 508 + logger.Info("Syncing secret to target device", "secret", plan.SecretPath, "source", plan.SourceDevice, "target", targetDeviceId, "version", newVersion) 496 509 497 510 var syncErr error 498 511 switch plan.MirrorType { 499 512 case MirrorTypeCreate, MirrorTypeUpdate: 500 - syncErr = mm.writeSecretWithMetadata(ctx, targetDevice, plan.SecretPath, sourceData, syncMetadata, operatorNamespace, logger) 513 + syncErr = mm.writeSecretWithMetadata(ctx, targetDevice, plan.SecretPath, sourceData, syncMetadata, logger) 501 514 case MirrorTypeRestoreMetadata: 502 515 // For metadata restoration, we just update the metadata without changing the data 503 - syncErr = mm.writeMetadataOnly(ctx, targetDevice, plan.SecretPath, syncMetadata, operatorNamespace, logger) 516 + syncErr = mm.writeMetadataOnly(ctx, targetDevice, plan.SecretPath, syncMetadata, logger) 504 517 } 505 518 506 519 if syncErr != nil { 507 - logger.Error(syncErr, "Failed to sync to target device", "target", targetDevice, "secret", plan.SecretPath) 520 + logger.Error(syncErr, "Failed to sync to target device", "target", targetDeviceId, "secret", plan.SecretPath) 508 521 if result.Error == nil { 509 522 result.Error = syncErr 510 523 } ··· 526 539 } 527 540 528 541 // readSecretWithMetadata reads both secret data and metadata from a device 529 - func (mm *MirrorManager) readSecretWithMetadata(ctx context.Context, deviceName, secretPath, namespace string, logger logr.Logger) (hsm.SecretData, *hsm.SecretMetadata, error) { 530 - grpcClient, err := mm.agentManager.CreateGRPCClient(ctx, deviceName, namespace, logger) 542 + func (mm *MirrorManager) readSecretWithMetadata(ctx context.Context, device hsmv1alpha1.DiscoveredDevice, secretPath string, logger logr.Logger) (hsm.SecretData, *hsm.SecretMetadata, error) { 543 + grpcClient, err := mm.agentManager.CreateGRPCClient(ctx, device, logger) 531 544 if err != nil { 532 545 return nil, nil, fmt.Errorf("failed to create gRPC client: %w", err) 533 546 } ··· 550 563 // Read metadata (may not exist) 551 564 metadata, err := grpcClient.ReadMetadata(ctx, secretPath) 552 565 if err != nil { 553 - logger.V(1).Info("No metadata found for secret", "secret", secretPath, "device", deviceName) 566 + logger.V(1).Info("No metadata found for secret", "secret", secretPath, "device", device.SerialNumber) 554 567 metadata = nil // Not an error - metadata may not exist 555 568 } 556 569 ··· 558 571 } 559 572 560 573 // writeSecretWithMetadata writes both secret data and metadata to a device 561 - func (mm *MirrorManager) writeSecretWithMetadata(ctx context.Context, deviceName, secretPath string, data hsm.SecretData, metadata *hsm.SecretMetadata, namespace string, logger logr.Logger) error { 562 - grpcClient, err := mm.agentManager.CreateGRPCClient(ctx, deviceName, namespace, logger) 574 + func (mm *MirrorManager) writeSecretWithMetadata(ctx context.Context, device hsmv1alpha1.DiscoveredDevice, secretPath string, data hsm.SecretData, metadata *hsm.SecretMetadata, logger logr.Logger) error { 575 + grpcClient, err := mm.agentManager.CreateGRPCClient(ctx, device, logger) 563 576 if err != nil { 564 577 return fmt.Errorf("failed to create gRPC client: %w", err) 565 578 } ··· 582 595 } 583 596 584 597 // writeMetadataOnly updates only the metadata for an existing secret 585 - func (mm *MirrorManager) writeMetadataOnly(ctx context.Context, deviceName, secretPath string, metadata *hsm.SecretMetadata, namespace string, logger logr.Logger) error { 586 - grpcClient, err := mm.agentManager.CreateGRPCClient(ctx, deviceName, namespace, logger) 598 + func (mm *MirrorManager) writeMetadataOnly(ctx context.Context, device hsmv1alpha1.DiscoveredDevice, secretPath string, metadata *hsm.SecretMetadata, logger logr.Logger) error { 599 + grpcClient, err := mm.agentManager.CreateGRPCClient(ctx, device, logger) 587 600 if err != nil { 588 601 return fmt.Errorf("failed to create gRPC client: %w", err) 589 602 } ··· 611 624 return nil 612 625 } 613 626 614 - // getAvailableDevices gets list of available HSM device types from HSMPools cluster-wide 615 - func (mm *MirrorManager) getAvailableDevices(ctx context.Context) ([]string, error) { 616 - var hsmPoolList hsmv1alpha1.HSMPoolList 617 - // List HSMPools cluster-wide since they exist in the same namespace as their HSMDevices 618 - if err := mm.client.List(ctx, &hsmPoolList); err != nil { 619 - return nil, fmt.Errorf("failed to list HSM pools cluster-wide: %w", err) 620 - } 621 - 622 - deviceTypes := make(map[string]bool) 623 - 624 - for _, pool := range hsmPoolList.Items { 625 - if pool.Status.Phase == hsmv1alpha1.HSMPoolPhaseReady && len(pool.Status.AggregatedDevices) > 0 { 626 - // Check if any devices in this pool are available 627 - hasAvailableDevices := false 628 - for _, aggregatedDevice := range pool.Status.AggregatedDevices { 629 - if aggregatedDevice.Available { 630 - hasAvailableDevices = true 631 - break 632 - } 633 - } 634 - 635 - if hasAvailableDevices { 636 - deviceName := pool.OwnerReferences[0].Name 637 - // Use base device name (e.g., "pico-hsm") - agent manager handles multiple instances internally 638 - deviceTypes[deviceName] = true 639 - } 640 - } 641 - } 642 - 643 - // Convert map to sorted slice 644 - devices := make([]string, 0, len(deviceTypes)) 645 - for deviceName := range deviceTypes { 646 - devices = append(devices, deviceName) 647 - } 648 - sort.Strings(devices) // Ensure consistent ordering 649 - return devices, nil 650 - } 651 - 652 627 // MirrorAllSecrets performs device-scoped mirroring of ALL secrets across HSM devices 653 628 // This discovers all secrets on any device and ensures they exist on all other devices 654 629 func (mm *MirrorManager) MirrorAllSecrets(ctx context.Context) (*MirrorResult, error) { ··· 656 631 logger.Info("Starting device-scoped mirroring of all HSM secrets") 657 632 658 633 // Get all available HSM devices 659 - devices, err := mm.getAvailableDevices(ctx) 634 + devices, err := mm.agentManager.GetAvailableDevices(ctx, mm.operatorNamespace) 660 635 if err != nil { 661 636 return nil, fmt.Errorf("failed to get available devices: %w", err) 662 637 } ··· 666 641 return &MirrorResult{Success: true, SecretsProcessed: 0}, nil 667 642 } 668 643 669 - logger.Info("Starting mirroring across devices", "devices", devices, "deviceCount", len(devices)) 644 + // Extract device identifiers for logging 645 + deviceIds := make([]string, len(devices)) 646 + for i, device := range devices { 647 + deviceIds[i] = device.SerialNumber 648 + } 649 + logger.Info("Starting mirroring across devices", "devices", deviceIds, "deviceCount", len(devices)) 670 650 671 651 // Discover all secrets across all devices 672 - allSecretPaths := mm.discoverAllSecrets(ctx, devices, mm.operatorNamespace, logger) 652 + allSecretPaths := mm.discoverAllSecrets(ctx, devices, logger) 673 653 674 654 if len(allSecretPaths) == 0 { 675 655 logger.Info("No secrets found to mirror") ··· 678 658 679 659 logger.Info("Discovered secrets for mirroring", "secretCount", len(allSecretPaths), "secrets", allSecretPaths) 680 660 661 + // Create device lookup map for execution functions 662 + deviceLookup := make(map[string]hsmv1alpha1.DiscoveredDevice) 663 + for _, device := range devices { 664 + deviceLookup[device.SerialNumber] = device 665 + } 666 + 681 667 // Build inventory for all discovered secrets 682 - inventory, err := mm.buildSecretInventory(ctx, allSecretPaths, devices, mm.operatorNamespace, logger) 668 + inventory, err := mm.buildSecretInventory(ctx, allSecretPaths, devices, logger) 683 669 if err != nil { 684 670 return nil, fmt.Errorf("failed to build secret inventory: %w", err) 685 671 } ··· 689 675 logger.Info("Created mirror plans", "planCount", len(plans)) 690 676 691 677 // Execute mirror plans 692 - return mm.executeMirrorPlans(ctx, plans, mm.operatorNamespace, logger), nil 678 + return mm.executeMirrorPlans(ctx, plans, deviceLookup, logger), nil 693 679 } 694 680 695 681 // discoverAllSecrets discovers all secrets present on any HSM device 696 - func (mm *MirrorManager) discoverAllSecrets(ctx context.Context, devices []string, operatorNamespace string, logger logr.Logger) []string { 682 + func (mm *MirrorManager) discoverAllSecrets(ctx context.Context, devices []hsmv1alpha1.DiscoveredDevice, logger logr.Logger) []string { 697 683 secretPaths := make(map[string]bool) 698 684 699 - for _, deviceName := range devices { 700 - deviceLogger := logger.WithValues("device", deviceName) 685 + for _, device := range devices { 686 + deviceId := device.SerialNumber 687 + deviceLogger := logger.WithValues("device", deviceId) 701 688 702 - hsmClient, err := mm.agentManager.CreateGRPCClient(ctx, deviceName, operatorNamespace, deviceLogger) 689 + hsmClient, err := mm.agentManager.CreateGRPCClient(ctx, device, deviceLogger) 703 690 if err != nil { 704 691 deviceLogger.Info("Failed to connect to device for discovery, skipping", "error", err) 705 692 continue ··· 769 756 return false, ctx.Err() 770 757 771 758 case <-ticker.C: 772 - devices, err := mm.getAvailableDevices(ctx) 759 + devices, err := mm.agentManager.GetAvailableDevices(ctx, mm.operatorNamespace) 773 760 if err != nil { 774 761 logger.V(1).Info("Failed to check available devices", "error", err) 775 762 continue ··· 777 764 778 765 if len(devices) > 0 { 779 766 // Try to connect to at least one device to verify agents are actually ready 780 - for _, deviceName := range devices { 781 - grpcClient, err := mm.agentManager.CreateGRPCClient(ctx, deviceName, mm.operatorNamespace, logger) 767 + for _, device := range devices { 768 + grpcClient, err := mm.agentManager.CreateGRPCClient(ctx, device, logger) 782 769 if err != nil { 783 - logger.V(1).Info("Agent not ready yet", "device", deviceName, "error", err) 770 + logger.V(1).Info("Agent not ready yet", "device", device.SerialNumber, "error", err) 784 771 continue 785 772 } 786 773
+46 -60
internal/mirror/manager_test.go
··· 25 25 "github.com/go-logr/logr" 26 26 "github.com/stretchr/testify/assert" 27 27 "github.com/stretchr/testify/require" 28 - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 29 28 "k8s.io/apimachinery/pkg/runtime" 30 29 "sigs.k8s.io/controller-runtime/pkg/client/fake" 31 30 ··· 48 47 } 49 48 } 50 49 51 - func (m *MockAgentManager) CreateGRPCClient(ctx context.Context, deviceName, namespace string, logger logr.Logger) (hsm.Client, error) { 52 - if err, exists := m.creationErrors[deviceName]; exists { 50 + func (m *MockAgentManager) CreateGRPCClient(ctx context.Context, device hsmv1alpha1.DiscoveredDevice, logger logr.Logger) (hsm.Client, error) { 51 + deviceId := device.SerialNumber 52 + if err, exists := m.creationErrors[deviceId]; exists { 53 53 return nil, err 54 54 } 55 55 56 - if client, exists := m.clients[deviceName]; exists { 56 + if client, exists := m.clients[deviceId]; exists { 57 57 return client, nil 58 58 } 59 59 60 60 // Return a mock client for testing 61 61 return hsm.NewMockClient(), nil 62 + } 63 + 64 + func (m *MockAgentManager) GetAvailableDevices(ctx context.Context, namespace string) ([]hsmv1alpha1.DiscoveredDevice, error) { 65 + // Convert device names to DiscoveredDevice objects for testing 66 + devices := make([]hsmv1alpha1.DiscoveredDevice, 0, len(m.clients)) 67 + for deviceId := range m.clients { 68 + devices = append(devices, hsmv1alpha1.DiscoveredDevice{ 69 + SerialNumber: deviceId, 70 + DevicePath: "/dev/test/" + deviceId, 71 + NodeName: "test-node", 72 + Available: true, 73 + }) 74 + } 75 + return devices, nil 62 76 } 63 77 64 78 func (m *MockAgentManager) SetClient(deviceName string, client hsm.Client) { ··· 591 605 } 592 606 } 593 607 594 - // Test getAvailableDevices method 595 - func TestGetAvailableDevices(t *testing.T) { 596 - scheme := runtime.NewScheme() 597 - _ = hsmv1alpha1.AddToScheme(scheme) 598 - 599 - // Create test HSMPool resources 600 - hsmPool1 := &hsmv1alpha1.HSMPool{ 601 - ObjectMeta: metav1.ObjectMeta{ 602 - Name: "pico-hsm", 603 - Namespace: "test-namespace", 604 - OwnerReferences: []metav1.OwnerReference{ 605 - { 606 - Name: "pico-hsm", 607 - }, 608 - }, 609 - }, 610 - Status: hsmv1alpha1.HSMPoolStatus{ 611 - Phase: hsmv1alpha1.HSMPoolPhaseReady, 612 - AggregatedDevices: []hsmv1alpha1.DiscoveredDevice{ 613 - {DevicePath: "/dev/usb1", NodeName: "node1", Available: true, SerialNumber: "0"}, 614 - {DevicePath: "/dev/usb2", NodeName: "node2", Available: true, SerialNumber: "1"}, 615 - }, 616 - }, 617 - } 618 - 619 - hsmPool2 := &hsmv1alpha1.HSMPool{ 620 - ObjectMeta: metav1.ObjectMeta{ 621 - Name: "yubikey", 622 - Namespace: "test-namespace", 623 - OwnerReferences: []metav1.OwnerReference{ 624 - { 625 - Name: "yubikey", 626 - }, 627 - }, 628 - }, 629 - Status: hsmv1alpha1.HSMPoolStatus{ 630 - Phase: hsmv1alpha1.HSMPoolPhaseReady, 631 - AggregatedDevices: []hsmv1alpha1.DiscoveredDevice{ 632 - {DevicePath: "/dev/usb3", NodeName: "node3", Available: false, SerialNumber: "0"}, // Not available 633 - }, 634 - }, 635 - } 608 + // Test agent manager device discovery integration 609 + func TestAgentManagerIntegration(t *testing.T) { 610 + mockAgentManager := NewMockAgentManager() 636 611 637 - client := fake.NewClientBuilder(). 638 - WithScheme(scheme). 639 - WithObjects(hsmPool1, hsmPool2). 640 - Build() 612 + // Test that mirror manager now uses agent manager for device discovery 613 + mockAgentManager.SetClient("device1", hsm.NewMockClient()) 641 614 642 - mockAgentManager := NewMockAgentManager() 643 - mirrorManager := NewMirrorManager(client, mockAgentManager, logr.Discard(), "test-namespace") 644 - 645 - devices, err := mirrorManager.getAvailableDevices(context.Background()) 615 + devices, err := mockAgentManager.GetAvailableDevices(context.Background(), "test-namespace") 646 616 647 617 require.NoError(t, err) 648 - assert.ElementsMatch(t, []string{"pico-hsm-0", "pico-hsm-1"}, devices) 618 + assert.Len(t, devices, 1) 619 + assert.Equal(t, "device1", devices[0].SerialNumber) 649 620 } 650 621 651 622 // Test buildSecretInventory method ··· 683 654 mirrorManager := NewMirrorManager(client, mockAgentManager, logr.Discard(), "test-namespace") 684 655 685 656 secretPaths := []string{"test-secret"} 686 - devices := []string{"device1", "device2"} 657 + devices := []hsmv1alpha1.DiscoveredDevice{ 658 + {SerialNumber: "device1", DevicePath: "/dev/test/device1", NodeName: "test-node", Available: true}, 659 + {SerialNumber: "device2", DevicePath: "/dev/test/device2", NodeName: "test-node", Available: true}, 660 + } 687 661 688 - inventory, err := mirrorManager.buildSecretInventory(ctx, secretPaths, devices, "test-namespace", logr.Discard()) 662 + inventory, err := mirrorManager.buildSecretInventory(ctx, secretPaths, devices, logr.Discard()) 689 663 690 664 require.NoError(t, err) 691 665 assert.Contains(t, inventory, "test-secret") ··· 783 757 }, 784 758 } 785 759 786 - result := mirrorManager.executeMirrorPlans(context.Background(), plans, "test-namespace", logr.Discard()) 760 + // Create device lookup for test 761 + deviceLookup := map[string]hsmv1alpha1.DiscoveredDevice{ 762 + "device1": {SerialNumber: "device1", DevicePath: "/dev/test/device1", NodeName: "test-node", Available: true}, 763 + "device2": {SerialNumber: "device2", DevicePath: "/dev/test/device2", NodeName: "test-node", Available: true}, 764 + } 765 + 766 + result := mirrorManager.executeMirrorPlans(context.Background(), plans, deviceLookup, logr.Discard()) 787 767 788 768 assert.NotNil(t, result) 789 769 // Success may be false if some operations fail, which is expected in testing ··· 841 821 842 822 mirrorManager := NewMirrorManager(client, mockAgentManager, logr.Discard(), "test-namespace") 843 823 824 + failingDevice := hsmv1alpha1.DiscoveredDevice{ 825 + SerialNumber: "failing-device", 826 + DevicePath: "/dev/test/failing-device", 827 + NodeName: "test-node", 828 + Available: true, 829 + } 830 + 844 831 data, metadata, err := mirrorManager.readSecretWithMetadata( 845 832 context.Background(), 846 - "failing-device", 833 + failingDevice, 847 834 "test-secret", 848 - "test-namespace", 849 835 logr.Discard(), 850 836 ) 851 837
+1 -1
internal/modes/agent/agent.go
··· 143 143 // Start gRPC server 144 144 setupLog.Info("HSM agent ready", "device", deviceName) 145 145 146 - grpcServer := agent.NewGRPCServer(hsmClient, deviceName, port, healthPort, setupLog) 146 + grpcServer := agent.NewGRPCServer(hsmClient, port, healthPort, setupLog) 147 147 if err := grpcServer.Start(ctx); err != nil { 148 148 setupLog.Error(err, "gRPC server failed") 149 149 return err