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.

fix references to hsmdevice and hsmpool. remove hsmdevicerefs in favor of native owenrefs

+327 -426
-4
api/v1alpha1/hsmpool_types.go
··· 22 22 23 23 // HSMPoolSpec defines the desired state of HSMPool 24 24 type HSMPoolSpec struct { 25 - // HSMDeviceRefs references the HSMDevice specifications this pool aggregates 26 - HSMDeviceRefs []string `json:"hsmDeviceRefs"` 27 - 28 25 // GracePeriod defines how long to wait before considering a pod's report stale 29 26 // +kubebuilder:default="5m" 30 27 // +optional ··· 115 112 // +kubebuilder:object:root=true 116 113 // +kubebuilder:subresource:status 117 114 // +kubebuilder:resource:shortName=hsmpool 118 - // +kubebuilder:printcolumn:name="HSMDevices",type=string,JSONPath=`.spec.hsmDeviceRefs` 119 115 // +kubebuilder:printcolumn:name="Total",type=integer,JSONPath=`.status.totalDevices` 120 116 // +kubebuilder:printcolumn:name="Available",type=integer,JSONPath=`.status.availableDevices` 121 117 // +kubebuilder:printcolumn:name="Reporting",type=string,JSONPath=`.status.reportingPods[*].podName`
+1 -5
api/v1alpha1/types_test.go
··· 268 268 Namespace: "hsm-secrets-operator-system", 269 269 }, 270 270 Spec: HSMPoolSpec{ 271 - HSMDeviceRefs: []string{"pico-hsm-1", "pico-hsm-2"}, 272 - GracePeriod: &gracePeriod, 271 + GracePeriod: &gracePeriod, 273 272 }, 274 273 } 275 274 276 275 assert.Equal(t, "pico-hsm-pool", pool.Name) 277 276 assert.Equal(t, "HSMPool", pool.Kind) 278 - assert.Len(t, pool.Spec.HSMDeviceRefs, 2) 279 - assert.Contains(t, pool.Spec.HSMDeviceRefs, "pico-hsm-1") 280 - assert.Contains(t, pool.Spec.HSMDeviceRefs, "pico-hsm-2") 281 277 assert.NotNil(t, pool.Spec.GracePeriod) 282 278 assert.Equal(t, 5*time.Minute, pool.Spec.GracePeriod.Duration) 283 279 }
-5
api/v1alpha1/zz_generated.deepcopy.go
··· 269 269 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 270 270 func (in *HSMPoolSpec) DeepCopyInto(out *HSMPoolSpec) { 271 271 *out = *in 272 - if in.HSMDeviceRefs != nil { 273 - in, out := &in.HSMDeviceRefs, &out.HSMDeviceRefs 274 - *out = make([]string, len(*in)) 275 - copy(*out, *in) 276 - } 277 272 if in.GracePeriod != nil { 278 273 in, out := &in.GracePeriod, &out.GracePeriod 279 274 *out = new(v1.Duration)
-11
config/crd/bases/hsm.j5t.io_hsmpools.yaml
··· 17 17 scope: Namespaced 18 18 versions: 19 19 - additionalPrinterColumns: 20 - - jsonPath: .spec.hsmDeviceRefs 21 - name: HSMDevices 22 - type: string 23 20 - jsonPath: .status.totalDevices 24 21 name: Total 25 22 type: integer ··· 68 65 description: GracePeriod defines how long to wait before considering 69 66 a pod's report stale 70 67 type: string 71 - hsmDeviceRefs: 72 - description: HSMDeviceRefs references the HSMDevice specifications 73 - this pool aggregates 74 - items: 75 - type: string 76 - type: array 77 68 mirroring: 78 69 description: Mirroring defines device mirroring configuration for 79 70 this pool ··· 105 96 type: string 106 97 type: array 107 98 type: object 108 - required: 109 - - hsmDeviceRefs 110 99 type: object 111 100 status: 112 101 description: HSMPoolStatus defines the observed state of HSMPool
-11
helm/hsm-secrets-operator/crds/hsm.j5t.io_hsmpools.yaml
··· 17 17 scope: Namespaced 18 18 versions: 19 19 - additionalPrinterColumns: 20 - - jsonPath: .spec.hsmDeviceRefs 21 - name: HSMDevices 22 - type: string 23 20 - jsonPath: .status.totalDevices 24 21 name: Total 25 22 type: integer ··· 68 65 description: GracePeriod defines how long to wait before considering 69 66 a pod's report stale 70 67 type: string 71 - hsmDeviceRefs: 72 - description: HSMDeviceRefs references the HSMDevice specifications 73 - this pool aggregates 74 - items: 75 - type: string 76 - type: array 77 68 mirroring: 78 69 description: Mirroring defines device mirroring configuration for 79 70 this pool ··· 105 96 type: string 106 97 type: array 107 98 type: object 108 - required: 109 - - hsmDeviceRefs 110 99 type: object 111 100 status: 112 101 description: HSMPoolStatus defines the observed state of HSMPool
+92 -73
internal/agent/deployment.go
··· 32 32 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 33 33 "k8s.io/apimachinery/pkg/types" 34 34 "k8s.io/apimachinery/pkg/util/intstr" 35 - ctrl "sigs.k8s.io/controller-runtime" 36 35 "sigs.k8s.io/controller-runtime/pkg/client" 37 36 38 37 hsmv1alpha1 "github.com/evanjarrett/hsm-secrets-operator/api/v1alpha1" ··· 42 41 // ManagerInterface defines the interface for HSM agent management 43 42 // This allows for easier testing with mocks 44 43 type ManagerInterface interface { 45 - EnsureAgent(ctx context.Context, hsmDevice *hsmv1alpha1.HSMDevice, hsmSecret *hsmv1alpha1.HSMSecret) error 44 + EnsureAgent(ctx context.Context, hsmPool *hsmv1alpha1.HSMPool) error 46 45 CleanupAgent(ctx context.Context, hsmDevice *hsmv1alpha1.HSMDevice) error 47 46 } 48 47 ··· 57 56 58 57 // AgentInfo tracks agent state and connections 59 58 type AgentInfo struct { 60 - DeviceName string 61 59 PodIPs []string 62 60 CreatedAt time.Time 63 61 LastHealthCheck time.Time ··· 99 97 GetImage(ctx context.Context, defaultImage string) string 100 98 } 101 99 100 + // deviceWork represents work to be done for a specific device 101 + type deviceWork struct { 102 + device hsmv1alpha1.DiscoveredDevice 103 + agentName string 104 + agentKey string 105 + index int 106 + } 107 + 102 108 // NewManager creates a new agent manager 103 109 func NewManager(k8sClient client.Client, namespace string, imageResolver ImageResolver) *Manager { 104 110 ··· 133 139 return m 134 140 } 135 141 136 - // deviceWork represents work to be done for a specific device 137 - type deviceWork struct { 138 - device hsmv1alpha1.DiscoveredDevice 139 - agentName string 140 - agentKey string 141 - index int 142 + // generateAgentName creates a consistent agent name for an HSM device 143 + func (m *Manager) generateAgentName(hsmPool *hsmv1alpha1.HSMPool) string { 144 + return fmt.Sprintf("%s-%s", AgentNamePrefix, hsmPool.OwnerReferences[0].Name) 142 145 } 143 146 144 147 // EnsureAgent ensures HSM agents are deployed for all available devices in the pool 145 - func (m *Manager) EnsureAgent(ctx context.Context, hsmDevice *hsmv1alpha1.HSMDevice, hsmSecret *hsmv1alpha1.HSMSecret) error { 146 - // Get the HSMPool for this device to find all aggregated devices 147 - poolName := hsmDevice.Name + "-pool" 148 - var hsmPool hsmv1alpha1.HSMPool 149 - if err := m.Get(ctx, types.NamespacedName{ 150 - Name: poolName, 151 - Namespace: hsmDevice.Namespace, 152 - }, &hsmPool); err != nil { 153 - return fmt.Errorf("failed to get HSMPool %s: %w", poolName, err) 154 - } 148 + func (m *Manager) EnsureAgent(ctx context.Context, hsmPool *hsmv1alpha1.HSMPool) error { 155 149 156 150 // Pre-collect available devices to process (no mutex needed) 157 151 workItems := make([]deviceWork, 0, len(hsmPool.Status.AggregatedDevices)) ··· 161 155 } 162 156 workItems = append(workItems, deviceWork{ 163 157 device: aggregatedDevice, 164 - agentName: fmt.Sprintf("%s-%s-%d", AgentNamePrefix, hsmDevice.Name, i), 165 - agentKey: fmt.Sprintf("%s-%s", hsmDevice.Name, aggregatedDevice.SerialNumber), 158 + agentName: fmt.Sprintf("%s-%d", m.generateAgentName(hsmPool), i), 159 + agentKey: aggregatedDevice.SerialNumber, 166 160 index: i, 167 161 }) 168 162 } ··· 199 193 } 200 194 201 195 // Deploy agent for this device (Kubernetes API calls - no mutex needed) 202 - if err := m.deployAgentForDevice(ctx, w, hsmDevice); err != nil { 196 + if err := m.deployAgentForDevice(ctx, w, hsmPool); err != nil { 203 197 errChan <- fmt.Errorf("failed to deploy agent %s: %w", w.agentName, err) 204 198 } 205 199 }(work) ··· 223 217 } 224 218 225 219 // deployAgentForDevice handles the deployment logic for a single device 226 - func (m *Manager) deployAgentForDevice(ctx context.Context, work deviceWork, hsmDevice *hsmv1alpha1.HSMDevice) error { 220 + func (m *Manager) deployAgentForDevice(ctx context.Context, work deviceWork, hsmPool *hsmv1alpha1.HSMPool) error { 227 221 // Check if deployment exists in Kubernetes 228 222 var deployment appsv1.Deployment 229 223 err := m.Get(ctx, types.NamespacedName{ 230 224 Name: work.agentName, 231 - Namespace: hsmDevice.Namespace, 225 + Namespace: hsmPool.Namespace, 232 226 }, &deployment) 233 227 234 228 if err == nil { 235 229 // Agent exists, but check if it needs updating (image version, device/node configuration) 236 - needsUpdate, err := m.agentNeedsUpdate(ctx, &deployment, hsmDevice) 230 + needsUpdate, err := m.agentNeedsUpdate(ctx, &deployment, hsmPool) 237 231 if err != nil { 238 232 return fmt.Errorf("failed to check if agent deployment %s needs update: %w", work.agentName, err) 239 233 } ··· 250 244 } 251 245 } else { 252 246 // Agent exists and is correct - wait for it and track it 253 - podIPs, err := m.waitForAgentReady(ctx, work.agentName, hsmDevice.Namespace) 247 + podIPs, err := m.waitForAgentReady(ctx, work.agentName, hsmPool.Namespace) 254 248 if err != nil { 255 249 return fmt.Errorf("failed waiting for existing agent pods %s: %w", work.agentName, err) 256 250 } ··· 258 252 // Track the existing agent (mutex-protected) 259 253 m.mu.Lock() 260 254 agentInfo := &AgentInfo{ 261 - DeviceName: work.agentKey, 262 255 PodIPs: podIPs, 263 256 CreatedAt: time.Now(), 264 257 LastHealthCheck: time.Now(), 265 258 Status: AgentStatusReady, 266 259 AgentName: work.agentName, 267 - Namespace: hsmDevice.Namespace, 260 + Namespace: hsmPool.Namespace, 268 261 } 269 262 m.activeAgents[work.agentKey] = agentInfo 270 263 m.mu.Unlock() ··· 275 268 } 276 269 277 270 // Create agent deployment for this specific device 278 - if err := m.createAgentDeployment(ctx, hsmDevice, nil, hsmDevice.Namespace, &work.device, work.agentName); err != nil { 271 + if err := m.createAgentDeployment(ctx, hsmPool, &work.device, work.agentName); err != nil { 279 272 return fmt.Errorf("failed to create agent deployment %s: %w", work.agentName, err) 280 273 } 281 274 282 275 // Wait for agent pods to be ready and get their IPs 283 - podIPs, err := m.waitForAgentReady(ctx, work.agentName, hsmDevice.Namespace) 276 + podIPs, err := m.waitForAgentReady(ctx, work.agentName, hsmPool.Namespace) 284 277 if err != nil { 285 278 return fmt.Errorf("failed waiting for agent pods %s: %w", work.agentName, err) 286 279 } ··· 288 281 // Track the new agent (mutex-protected) 289 282 m.mu.Lock() 290 283 agentInfo := &AgentInfo{ 291 - DeviceName: work.agentKey, 292 284 PodIPs: podIPs, 293 285 CreatedAt: time.Now(), 294 286 LastHealthCheck: time.Now(), 295 287 Status: AgentStatusReady, 296 288 AgentName: work.agentName, 297 - Namespace: hsmDevice.Namespace, 289 + Namespace: hsmPool.Namespace, 298 290 } 299 291 m.activeAgents[work.agentKey] = agentInfo 300 292 m.mu.Unlock() ··· 388 380 return nil 389 381 } 390 382 391 - // generateAgentName creates a consistent agent name for an HSM device 392 - func (m *Manager) generateAgentName(hsmDevice *hsmv1alpha1.HSMDevice) string { 393 - return fmt.Sprintf("%s-%s", AgentNamePrefix, hsmDevice.Name) 394 - } 395 - 396 383 // createAgentDeployment creates the HSM agent deployment for a specific device 397 - func (m *Manager) createAgentDeployment(ctx context.Context, hsmDevice *hsmv1alpha1.HSMDevice, hsmSecret *hsmv1alpha1.HSMSecret, namespace string, specificDevice *hsmv1alpha1.DiscoveredDevice, customAgentName string) error { 384 + func (m *Manager) createAgentDeployment(ctx context.Context, hsmPool *hsmv1alpha1.HSMPool, specificDevice *hsmv1alpha1.DiscoveredDevice, customAgentName string) error { 398 385 if specificDevice == nil { 399 386 return fmt.Errorf("specificDevice is required") 400 387 } ··· 403 390 if customAgentName != "" { 404 391 agentName = customAgentName 405 392 } else { 406 - agentName = m.generateAgentName(hsmDevice) 393 + agentName = m.generateAgentName(hsmPool) 407 394 } 408 395 409 396 targetNode := specificDevice.NodeName 410 397 devicePath := specificDevice.DevicePath 398 + deviceName := hsmPool.OwnerReferences[0].Name 411 399 412 400 // Get discovery image from environment, manager image, or use default 413 401 agentImage := m.ImageResolver.GetImage(ctx, "AGENT_IMAGE") ··· 415 403 deployment := &appsv1.Deployment{ 416 404 ObjectMeta: metav1.ObjectMeta{ 417 405 Name: agentName, 418 - Namespace: namespace, 406 + Namespace: hsmPool.Namespace, 419 407 Labels: map[string]string{ 420 408 "app": agentName, 421 409 "app.kubernetes.io/component": "hsm-agent", 422 410 "app.kubernetes.io/instance": agentName, 423 411 "app.kubernetes.io/name": "hsm-agent", 424 412 "app.kubernetes.io/part-of": "hsm-secrets-operator", 425 - "hsm.j5t.io/device": hsmDevice.Name, 426 - "hsm.j5t.io/device-type": string(hsmDevice.Spec.DeviceType), 413 + "hsm.j5t.io/device": deviceName, 414 + "hsm.j5t.io/serial-number": specificDevice.SerialNumber, 415 + "hsm.j5t.io/device-path": sanitizeLabelValue(specificDevice.DevicePath), 427 416 }, 428 417 }, 429 418 Spec: appsv1.DeploymentSpec{ ··· 441 430 "app.kubernetes.io/instance": agentName, 442 431 "app.kubernetes.io/name": "hsm-agent", 443 432 "app.kubernetes.io/part-of": "hsm-secrets-operator", 444 - "hsm.j5t.io/device": hsmDevice.Name, 445 - "hsm.j5t.io/device-type": string(hsmDevice.Spec.DeviceType), 433 + "hsm.j5t.io/device": deviceName, 434 + "hsm.j5t.io/serial-number": specificDevice.SerialNumber, 435 + "hsm.j5t.io/device-path": sanitizeLabelValue(specificDevice.DevicePath), 446 436 }, 447 437 }, 448 438 Spec: corev1.PodSpec{ ··· 483 473 "agent", 484 474 }, 485 475 Args: []string{ 486 - "--device-name=" + hsmDevice.Name, 476 + "--device-name=" + deviceName, 487 477 "--port=" + fmt.Sprintf("%d", AgentPort), 488 478 "--health-port=" + fmt.Sprintf("%d", AgentHealthPort), 489 479 }, 490 - Env: m.buildAgentEnv(hsmDevice), 480 + Env: func() []corev1.EnvVar { 481 + env, err := m.buildAgentEnv(ctx, hsmPool) 482 + if err != nil { 483 + // Log error but continue with empty env to avoid breaking deployment creation 484 + return []corev1.EnvVar{} 485 + } 486 + return env 487 + }(), 491 488 Ports: []corev1.ContainerPort{ 492 489 { 493 490 Name: "grpc", ··· 552 549 }, 553 550 } 554 551 555 - // Set HSMSecret as owner if provided (for cleanup) 556 - if hsmSecret != nil { 557 - if err := ctrl.SetControllerReference(hsmSecret, deployment, m.Scheme()); err != nil { 558 - return fmt.Errorf("failed to set controller reference: %w", err) 559 - } 560 - } 561 - 562 552 return m.Create(ctx, deployment) 563 553 } 564 554 565 555 // buildAgentEnv builds environment variables for the HSM agent 566 - func (m *Manager) buildAgentEnv(hsmDevice *hsmv1alpha1.HSMDevice) []corev1.EnvVar { 556 + func (m *Manager) buildAgentEnv(ctx context.Context, hsmPool *hsmv1alpha1.HSMPool) ([]corev1.EnvVar, error) { 557 + // Get HSMDevice from owner reference 558 + deviceName := hsmPool.OwnerReferences[0].Name 559 + var hsmDevice hsmv1alpha1.HSMDevice 560 + if err := m.Get(ctx, types.NamespacedName{ 561 + Name: deviceName, 562 + Namespace: hsmPool.Namespace, 563 + }, &hsmDevice); err != nil { 564 + return nil, fmt.Errorf("failed to get HSMDevice %s: %w", deviceName, err) 565 + } 567 566 env := []corev1.EnvVar{ 568 567 { 569 568 Name: "HSM_DEVICE_NAME", ··· 608 607 } 609 608 } 610 609 611 - return env 610 + return env, nil 612 611 } 613 612 614 613 // buildAgentVolumeMounts builds volume mounts for the HSM agent ··· 647 646 } 648 647 649 648 // agentNeedsUpdate checks if the agent deployment needs to be updated due to device path or image changes 650 - func (m *Manager) agentNeedsUpdate(ctx context.Context, deployment *appsv1.Deployment, hsmDevice *hsmv1alpha1.HSMDevice) (bool, error) { 649 + func (m *Manager) agentNeedsUpdate(ctx context.Context, deployment *appsv1.Deployment, hsmPool *hsmv1alpha1.HSMPool) (bool, error) { 650 + if hsmPool == nil { 651 + return false, nil // No pool available, no update needed 652 + } 651 653 // Check if container image needs updating 652 654 if len(deployment.Spec.Template.Spec.Containers) == 0 { 653 655 return false, fmt.Errorf("deployment has no containers") ··· 665 667 } 666 668 } 667 669 668 - // Get current HSMPool to check for updated device paths 669 - poolName := hsmDevice.Name + "-pool" 670 - pool := &hsmv1alpha1.HSMPool{} 671 - 672 - if err := m.Get(ctx, types.NamespacedName{ 673 - Name: poolName, 674 - Namespace: hsmDevice.Namespace, 675 - }, pool); err != nil { 676 - // If pool doesn't exist, no devices are available, so agent doesn't need update 677 - if errors.IsNotFound(err) { 678 - return false, nil 679 - } 680 - return false, fmt.Errorf("failed to get HSMPool %s: %w", poolName, err) 681 - } 682 - 683 670 // Extract current volume mounts from deployment 684 671 currentDeviceMounts := make(map[string]string) // mount name -> device path 685 672 ··· 696 683 } 697 684 698 685 // Check if any device paths in the pool differ from current mounts 699 - for _, device := range pool.Status.AggregatedDevices { 686 + for _, device := range hsmPool.Status.AggregatedDevices { 700 687 if device.DevicePath != "" && device.Available { 701 688 // Check if this device path is already mounted 702 689 found := false ··· 716 703 // Check for stale device paths (mounted paths that are no longer in aggregated devices) 717 704 for _, currentPath := range currentDeviceMounts { 718 705 found := false 719 - for _, device := range pool.Status.AggregatedDevices { 706 + for _, device := range hsmPool.Status.AggregatedDevices { 720 707 if device.DevicePath == currentPath && device.Available { 721 708 found = true 722 709 break ··· 913 900 } 914 901 915 902 return allPodIPs, nil 903 + } 904 + 905 + // sanitizeLabelValue sanitizes a string to be a valid Kubernetes label value 906 + // Kubernetes labels must be alphanumeric, '-', '_', or '.' and start/end with alphanumeric 907 + func sanitizeLabelValue(value string) string { 908 + if len(value) == 0 { 909 + return value 910 + } 911 + 912 + // Replace invalid characters with dashes 913 + sanitized := strings.Map(func(r rune) rune { 914 + if (r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '-' || r == '_' || r == '.' { 915 + return r 916 + } 917 + return '-' 918 + }, value) 919 + 920 + // Ensure starts and ends with alphanumeric 921 + sanitized = strings.TrimFunc(sanitized, func(r rune) bool { 922 + return !((r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9')) 923 + }) 924 + 925 + // Kubernetes label values have a 63 character limit 926 + if len(sanitized) > 63 { 927 + sanitized = sanitized[:63] 928 + // Re-trim end if we cut off at a non-alphanumeric 929 + sanitized = strings.TrimFunc(sanitized, func(r rune) bool { 930 + return !((r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9')) 931 + }) 932 + } 933 + 934 + return sanitized 916 935 } 917 936 918 937 // GetGRPCEndpoints returns gRPC endpoints for all agent pods of a device
+19 -24
internal/agent/deployment_test.go
··· 212 212 AgentImage: "test-image", 213 213 } 214 214 215 - needsUpdate, err := manager.agentNeedsUpdate(ctx, tt.deployment, tt.hsmDevice) 215 + needsUpdate, err := manager.agentNeedsUpdate(ctx, tt.deployment, tt.hsmPool) 216 216 217 217 if tt.expectError { 218 218 assert.Error(t, err) ··· 236 236 237 237 // Add agent to tracking 238 238 agentInfo := &AgentInfo{ 239 - DeviceName: "test-device", 240 - PodIPs: []string{"10.1.1.5", "10.1.1.6"}, 241 - Status: AgentStatusReady, 242 - AgentName: "hsm-agent-test-device", 243 - Namespace: "default", 239 + PodIPs: []string{"10.1.1.5", "10.1.1.6"}, 240 + Status: AgentStatusReady, 241 + AgentName: "hsm-agent-test-device", 242 + Namespace: "default", 244 243 } 245 244 manager.activeAgents["test-device"] = agentInfo 246 245 ··· 269 268 270 269 // Add agent to tracking 271 270 agentInfo := &AgentInfo{ 272 - DeviceName: "test-device", 273 - PodIPs: []string{"10.1.1.5"}, 274 - Status: AgentStatusReady, 271 + PodIPs: []string{"10.1.1.5"}, 272 + Status: AgentStatusReady, 275 273 } 276 274 manager.activeAgents["test-device"] = agentInfo 277 275 ··· 317 315 manager := NewManager(fakeClient, "test-namespace", nil) 318 316 319 317 agentInfo := &AgentInfo{ 320 - DeviceName: "test-device", 321 - PodIPs: []string{"10.1.1.5"}, 322 - Status: AgentStatusReady, 323 - AgentName: "hsm-agent-test-device", 324 - Namespace: "default", 318 + PodIPs: []string{"10.1.1.5"}, 319 + Status: AgentStatusReady, 320 + AgentName: "hsm-agent-test-device", 321 + Namespace: "default", 325 322 } 326 323 327 324 healthy := manager.isAgentHealthy(ctx, agentInfo) ··· 334 331 manager := NewManager(fakeClient, "test-namespace", nil) 335 332 336 333 agentInfo := &AgentInfo{ 337 - DeviceName: "test-device", 338 - PodIPs: []string{}, // No pod IPs 339 - Status: AgentStatusReady, 340 - AgentName: "hsm-agent-test-device", 341 - Namespace: "default", 334 + PodIPs: []string{}, // No pod IPs 335 + Status: AgentStatusReady, 336 + AgentName: "hsm-agent-test-device", 337 + Namespace: "default", 342 338 } 343 339 344 340 healthy := manager.isAgentHealthy(ctx, agentInfo) ··· 368 364 manager := NewManager(fakeClient, "test-namespace", nil) 369 365 370 366 agentInfo := &AgentInfo{ 371 - DeviceName: "test-device", 372 - PodIPs: []string{"10.1.1.5"}, 373 - Status: AgentStatusReady, 374 - AgentName: "hsm-agent-test-device", 375 - Namespace: "default", 367 + PodIPs: []string{"10.1.1.5"}, 368 + Status: AgentStatusReady, 369 + AgentName: "hsm-agent-test-device", 370 + Namespace: "default", 376 371 } 377 372 378 373 healthy := manager.isAgentHealthy(ctx, agentInfo)
+4 -4
internal/api/proxy_client.go
··· 78 78 79 79 // DeleteSecretResponse represents the response for deleting a secret 80 80 type DeleteSecretResponse struct { 81 - Path string `json:"path"` 82 - Devices int `json:"devices"` 83 - DeviceResults map[string]any `json:"deviceResults"` 84 - Warnings []string `json:"warnings,omitempty"` 81 + Path string `json:"path"` 82 + Devices int `json:"devices"` 83 + DeviceResults map[string]any `json:"deviceResults"` 84 + Warnings []string `json:"warnings,omitempty"` 85 85 } 86 86 87 87 // ReadMetadataResponse represents the response for reading metadata
+1 -15
internal/controller/discovery_daemonset_controller.go
··· 92 92 }, 93 93 }, 94 94 Spec: hsmv1alpha1.HSMPoolSpec{ 95 - HSMDeviceRefs: []string{hsmDevice.Name}, 96 - GracePeriod: &metav1.Duration{Duration: 5 * time.Minute}, // Default grace period 95 + GracePeriod: &metav1.Duration{Duration: 5 * time.Minute}, // Default grace period 97 96 }, 98 97 } 99 98 ··· 119 118 120 119 // Update existing HSMPool if needed 121 120 needsUpdate := false 122 - 123 - // Check if device reference needs updating 124 - found := false 125 - for _, deviceRef := range existing.Spec.HSMDeviceRefs { 126 - if deviceRef == hsmDevice.Name { 127 - found = true 128 - break 129 - } 130 - } 131 - if !found { 132 - existing.Spec.HSMDeviceRefs = append(existing.Spec.HSMDeviceRefs, hsmDevice.Name) 133 - needsUpdate = true 134 - } 135 121 136 122 // Update grace period if it's nil 137 123 if existing.Spec.GracePeriod == nil {
+76 -67
internal/controller/hsmpool_agent_controller.go
··· 64 64 65 65 // Only deploy agents for ready pools with discovered hardware 66 66 if hsmPool.Status.Phase == hsmv1alpha1.HSMPoolPhaseReady && len(hsmPool.Status.AggregatedDevices) > 0 { 67 - // For each HSMDevice referenced by this pool, ensure agents exist for all aggregated devices 68 - for _, deviceRef := range hsmPool.Spec.HSMDeviceRefs { 69 - // Get the HSMDevice to pass to agent manager 70 - var hsmDevice hsmv1alpha1.HSMDevice 71 - if err := r.Get(ctx, client.ObjectKey{ 72 - Name: deviceRef, 73 - Namespace: hsmPool.Namespace, 74 - }, &hsmDevice); err != nil { 75 - logger.Error(err, "Failed to get referenced HSMDevice", "device", deviceRef) 76 - continue 77 - } 67 + // Ensure owner reference exists and get the HSMDevice 68 + if len(hsmPool.OwnerReferences) == 0 { 69 + logger.Error(fmt.Errorf("no owner references"), "HSMPool has no owner references", "pool", hsmPool.Name) 70 + return ctrl.Result{}, nil 71 + } 78 72 79 - if r.AgentManager != nil { 80 - if err := r.AgentManager.EnsureAgent(ctx, &hsmDevice, nil); err != nil { 81 - logger.Error(err, "Failed to ensure HSM agents for pool", "device", deviceRef) 82 - } 83 - } else { 84 - logger.Error(fmt.Errorf("agent manager not configured"), "Cannot ensure agents without agent manager") 73 + deviceRef := hsmPool.OwnerReferences[0].Name 74 + // Get the HSMDevice to pass to agent manager 75 + var hsmDevice hsmv1alpha1.HSMDevice 76 + if err := r.Get(ctx, client.ObjectKey{ 77 + Name: deviceRef, 78 + Namespace: hsmPool.Namespace, 79 + }, &hsmDevice); err != nil { 80 + logger.Error(err, "Failed to get referenced HSMDevice", "device", deviceRef) 81 + // Don't return error - this allows graceful handling of missing devices 82 + return ctrl.Result{}, nil 83 + } 84 + 85 + if r.AgentManager != nil { 86 + if err := r.AgentManager.EnsureAgent(ctx, &hsmPool); err != nil { 87 + logger.Error(err, "Failed to ensure HSM agents for pool", "device", deviceRef) 85 88 } 89 + } else { 90 + logger.Error(fmt.Errorf("agent manager not configured"), "Cannot ensure agents without agent manager") 86 91 } 87 92 } else { 88 93 logger.V(1).Info("HSMPool not ready for agent deployment", ··· 114 119 absenceTimeout = 2 * gracePeriod // Default to 2x grace period 115 120 } 116 121 117 - // For each HSMDevice referenced by this pool, check if it should be cleaned up 118 - for _, deviceRef := range hsmPool.Spec.HSMDeviceRefs { 119 - // Get the HSMDevice 120 - var hsmDevice hsmv1alpha1.HSMDevice 121 - if err := r.Get(ctx, client.ObjectKey{ 122 - Name: deviceRef, 123 - Namespace: hsmPool.Namespace, 124 - }, &hsmDevice); err != nil { 125 - logger.V(1).Info("HSMDevice not found, skipping cleanup check", "device", deviceRef) 126 - continue 127 - } 122 + // Check if the HSMDevice referenced by this pool should be cleaned up (from ownerReferences) 123 + if len(hsmPool.OwnerReferences) == 0 { 124 + logger.V(1).Info("HSMPool has no owner references, skipping cleanup") 125 + return nil 126 + } 127 + 128 + deviceRef := hsmPool.OwnerReferences[0].Name 129 + // Get the HSMDevice 130 + var hsmDevice hsmv1alpha1.HSMDevice 131 + if err := r.Get(ctx, client.ObjectKey{ 132 + Name: deviceRef, 133 + Namespace: hsmPool.Namespace, 134 + }, &hsmDevice); err != nil { 135 + logger.V(1).Info("HSMDevice not found, skipping cleanup check", "device", deviceRef) 136 + return nil 137 + } 128 138 129 - // Check if this device has available aggregated devices in the pool 130 - deviceAvailable := false 131 - var lastSeenTime time.Time 139 + // Check if this device has available aggregated devices in the pool 140 + deviceAvailable := false 141 + var lastSeenTime time.Time 132 142 133 - for _, aggregatedDevice := range hsmPool.Status.AggregatedDevices { 134 - if aggregatedDevice.Available { 135 - deviceAvailable = true 136 - break 137 - } 138 - // Track the most recent LastSeen time for unavailable devices 139 - if aggregatedDevice.LastSeen.After(lastSeenTime) { 140 - lastSeenTime = aggregatedDevice.LastSeen.Time 141 - } 143 + for _, aggregatedDevice := range hsmPool.Status.AggregatedDevices { 144 + if aggregatedDevice.Available { 145 + deviceAvailable = true 146 + break 142 147 } 148 + // Track the most recent LastSeen time for unavailable devices 149 + if aggregatedDevice.LastSeen.After(lastSeenTime) { 150 + lastSeenTime = aggregatedDevice.LastSeen.Time 151 + } 152 + } 143 153 144 - // If device is not available and hasn't been seen for longer than absence timeout 145 - if !deviceAvailable { 146 - timeSinceLastSeen := time.Since(lastSeenTime) 154 + // If device is not available and hasn't been seen for longer than absence timeout 155 + if !deviceAvailable { 156 + timeSinceLastSeen := time.Since(lastSeenTime) 147 157 148 - if lastSeenTime.IsZero() { 149 - // No devices have ever been seen - check if pool has been around long enough 150 - poolAge := time.Since(hsmPool.CreationTimestamp.Time) 151 - if poolAge > absenceTimeout { 152 - logger.Info("Cleaning up agent for device with no discovered instances", 153 - "device", deviceRef, 154 - "poolAge", poolAge, 155 - "absenceTimeout", absenceTimeout) 156 - 157 - if err := r.cleanupAgentForDevice(ctx, &hsmDevice); err != nil { 158 - logger.Error(err, "Failed to cleanup agent for device with no instances", "device", deviceRef) 159 - } 160 - } 161 - } else if timeSinceLastSeen > absenceTimeout { 162 - logger.Info("Cleaning up agent for device absent too long", 158 + if lastSeenTime.IsZero() { 159 + // No devices have ever been seen - check if pool has been around long enough 160 + poolAge := time.Since(hsmPool.CreationTimestamp.Time) 161 + if poolAge > absenceTimeout { 162 + logger.Info("Cleaning up agent for device with no discovered instances", 163 163 "device", deviceRef, 164 - "timeSinceLastSeen", timeSinceLastSeen, 165 - "absenceTimeout", absenceTimeout, 166 - "lastSeen", lastSeenTime) 164 + "poolAge", poolAge, 165 + "absenceTimeout", absenceTimeout) 167 166 168 167 if err := r.cleanupAgentForDevice(ctx, &hsmDevice); err != nil { 169 - logger.Error(err, "Failed to cleanup agent for absent device", "device", deviceRef) 168 + logger.Error(err, "Failed to cleanup agent for device with no instances", "device", deviceRef) 170 169 } 171 - } else { 172 - logger.V(1).Info("Device unavailable but within tolerance", 173 - "device", deviceRef, 174 - "timeSinceLastSeen", timeSinceLastSeen, 175 - "absenceTimeout", absenceTimeout) 170 + } 171 + } else if timeSinceLastSeen > absenceTimeout { 172 + logger.Info("Cleaning up agent for device absent too long", 173 + "device", deviceRef, 174 + "timeSinceLastSeen", timeSinceLastSeen, 175 + "absenceTimeout", absenceTimeout, 176 + "lastSeen", lastSeenTime) 177 + 178 + if err := r.cleanupAgentForDevice(ctx, &hsmDevice); err != nil { 179 + logger.Error(err, "Failed to cleanup agent for absent device", "device", deviceRef) 176 180 } 181 + } else { 182 + logger.V(1).Info("Device unavailable but within tolerance", 183 + "device", deviceRef, 184 + "timeSinceLastSeen", timeSinceLastSeen, 185 + "absenceTimeout", absenceTimeout) 177 186 } 178 187 } 179 188
+66 -18
internal/controller/hsmpool_agent_controller_test.go
··· 90 90 ObjectMeta: metav1.ObjectMeta{ 91 91 Name: hsmPoolName, 92 92 Namespace: hsmPoolNamespace, 93 + OwnerReferences: []metav1.OwnerReference{ 94 + { 95 + APIVersion: "hsm.j5t.io/v1alpha1", 96 + Kind: "HSMDevice", 97 + Name: hsmDevice.Name, 98 + UID: hsmDevice.UID, 99 + }, 100 + }, 93 101 }, 94 - Spec: hsmv1alpha1.HSMPoolSpec{ 95 - HSMDeviceRefs: []string{hsmDeviceName}, 96 - }, 102 + Spec: hsmv1alpha1.HSMPoolSpec{}, 97 103 Status: hsmv1alpha1.HSMPoolStatus{ 98 104 Phase: hsmv1alpha1.HSMPoolPhaseReady, 99 105 AvailableDevices: 1, ··· 197 203 Expect(deployment.Name).To(Equal(agentName)) 198 204 Expect(deployment.Namespace).To(Equal(hsmPoolNamespace)) 199 205 Expect(deployment.Labels).To(HaveKeyWithValue("hsm.j5t.io/device", hsmDeviceName)) 200 - Expect(deployment.Labels).To(HaveKeyWithValue("hsm.j5t.io/device-type", "PicoHSM")) 206 + Expect(deployment.Labels).To(HaveKey("hsm.j5t.io/serial-number")) 207 + Expect(deployment.Labels).To(HaveKey("hsm.j5t.io/device-path")) 201 208 202 209 // Check pod template 203 210 podSpec := deployment.Spec.Template.Spec ··· 313 320 ObjectMeta: metav1.ObjectMeta{ 314 321 Name: missingDevicePoolName, 315 322 Namespace: hsmPoolNamespace, 316 - }, 317 - Spec: hsmv1alpha1.HSMPoolSpec{ 318 - HSMDeviceRefs: []string{"non-existent-device"}, 323 + OwnerReferences: []metav1.OwnerReference{ 324 + { 325 + APIVersion: "hsm.j5t.io/v1alpha1", 326 + Kind: "HSMDevice", 327 + Name: "non-existent-device", 328 + UID: "fake-uid-123", 329 + }, 330 + }, 319 331 }, 332 + Spec: hsmv1alpha1.HSMPoolSpec{}, 320 333 Status: hsmv1alpha1.HSMPoolStatus{ 321 334 Phase: hsmv1alpha1.HSMPoolPhaseReady, 322 335 AggregatedDevices: []hsmv1alpha1.DiscoveredDevice{ ··· 456 469 CleanupCalls []string // Track which devices were cleaned up 457 470 } 458 471 459 - func (m *MockAgentManager) EnsureAgent(ctx context.Context, hsmDevice *hsmv1alpha1.HSMDevice, hsmSecret *hsmv1alpha1.HSMSecret) error { 472 + func (m *MockAgentManager) EnsureAgent(ctx context.Context, hsmPool *hsmv1alpha1.HSMPool) error { 460 473 return nil 461 474 } 462 475 ··· 491 504 ObjectMeta: metav1.ObjectMeta{ 492 505 Name: "test-pool", 493 506 Namespace: "default", 507 + OwnerReferences: []metav1.OwnerReference{ 508 + { 509 + APIVersion: "hsm.j5t.io/v1alpha1", 510 + Kind: "HSMDevice", 511 + Name: "absent-device", 512 + UID: "test-uid-1", 513 + }, 514 + }, 494 515 }, 495 516 Spec: hsmv1alpha1.HSMPoolSpec{ 496 - HSMDeviceRefs: []string{"absent-device"}, 497 - GracePeriod: &metav1.Duration{Duration: 5 * time.Minute}, 517 + GracePeriod: &metav1.Duration{Duration: 5 * time.Minute}, 498 518 }, 499 519 Status: hsmv1alpha1.HSMPoolStatus{ 500 520 AggregatedDevices: []hsmv1alpha1.DiscoveredDevice{ ··· 524 544 ObjectMeta: metav1.ObjectMeta{ 525 545 Name: "test-pool", 526 546 Namespace: "default", 547 + OwnerReferences: []metav1.OwnerReference{ 548 + { 549 + APIVersion: "hsm.j5t.io/v1alpha1", 550 + Kind: "HSMDevice", 551 + Name: "recent-device", 552 + UID: "test-uid-2", 553 + }, 554 + }, 527 555 }, 528 556 Spec: hsmv1alpha1.HSMPoolSpec{ 529 - HSMDeviceRefs: []string{"recent-device"}, 530 - GracePeriod: &metav1.Duration{Duration: 5 * time.Minute}, 557 + GracePeriod: &metav1.Duration{Duration: 5 * time.Minute}, 531 558 }, 532 559 Status: hsmv1alpha1.HSMPoolStatus{ 533 560 AggregatedDevices: []hsmv1alpha1.DiscoveredDevice{ ··· 557 584 ObjectMeta: metav1.ObjectMeta{ 558 585 Name: "test-pool", 559 586 Namespace: "default", 587 + OwnerReferences: []metav1.OwnerReference{ 588 + { 589 + APIVersion: "hsm.j5t.io/v1alpha1", 590 + Kind: "HSMDevice", 591 + Name: "available-device", 592 + UID: "test-uid-3", 593 + }, 594 + }, 560 595 }, 561 596 Spec: hsmv1alpha1.HSMPoolSpec{ 562 - HSMDeviceRefs: []string{"available-device"}, 563 - GracePeriod: &metav1.Duration{Duration: 5 * time.Minute}, 597 + GracePeriod: &metav1.Duration{Duration: 5 * time.Minute}, 564 598 }, 565 599 Status: hsmv1alpha1.HSMPoolStatus{ 566 600 AggregatedDevices: []hsmv1alpha1.DiscoveredDevice{ ··· 591 625 Name: "test-pool", 592 626 Namespace: "default", 593 627 CreationTimestamp: metav1.NewTime(tenMinutesAgo), // Pool created 10 minutes ago 628 + OwnerReferences: []metav1.OwnerReference{ 629 + { 630 + APIVersion: "hsm.j5t.io/v1alpha1", 631 + Kind: "HSMDevice", 632 + Name: "never-seen-device", 633 + UID: "test-uid-4", 634 + }, 635 + }, 594 636 }, 595 637 Spec: hsmv1alpha1.HSMPoolSpec{ 596 - HSMDeviceRefs: []string{"never-seen-device"}, 597 - GracePeriod: &metav1.Duration{Duration: 5 * time.Minute}, 638 + GracePeriod: &metav1.Duration{Duration: 5 * time.Minute}, 598 639 }, 599 640 Status: hsmv1alpha1.HSMPoolStatus{ 600 641 AggregatedDevices: []hsmv1alpha1.DiscoveredDevice{}, // No devices ever discovered ··· 669 710 ObjectMeta: metav1.ObjectMeta{ 670 711 Name: "test-pool", 671 712 Namespace: "default", 713 + OwnerReferences: []metav1.OwnerReference{ 714 + { 715 + APIVersion: "hsm.j5t.io/v1alpha1", 716 + Kind: "HSMDevice", 717 + Name: "test-device", 718 + UID: "test-uid-5", 719 + }, 720 + }, 672 721 }, 673 722 Spec: hsmv1alpha1.HSMPoolSpec{ 674 - HSMDeviceRefs: []string{"test-device"}, 675 - GracePeriod: &metav1.Duration{Duration: 3 * time.Minute}, // 3 minute grace period 723 + GracePeriod: &metav1.Duration{Duration: 3 * time.Minute}, // 3 minute grace period 676 724 }, 677 725 Status: hsmv1alpha1.HSMPoolStatus{ 678 726 AggregatedDevices: []hsmv1alpha1.DiscoveredDevice{
+23 -23
internal/controller/hsmpool_controller.go
··· 81 81 return ctrl.Result{}, client.IgnoreNotFound(err) 82 82 } 83 83 84 - // Validate that all referenced HSMDevices exist 85 - hsmDevices := make([]*hsmv1alpha1.HSMDevice, 0, len(hsmPool.Spec.HSMDeviceRefs)) 86 - for _, deviceRef := range hsmPool.Spec.HSMDeviceRefs { 87 - hsmDevice := &hsmv1alpha1.HSMDevice{} 88 - if err := r.Get(ctx, client.ObjectKey{ 89 - Name: deviceRef, 90 - Namespace: hsmPool.Namespace, 91 - }, hsmDevice); err != nil { 92 - logger.Error(err, "Unable to fetch referenced HSMDevice", "hsmDevice", deviceRef) 93 - return r.updatePoolStatus(ctx, &hsmPool, hsmv1alpha1.HSMPoolPhaseError, nil, nil, 0, fmt.Sprintf("HSMDevice %s not found", deviceRef)) 94 - } 95 - hsmDevices = append(hsmDevices, hsmDevice) 84 + // Validate that the referenced HSMDevice exists (from ownerReferences) 85 + if len(hsmPool.OwnerReferences) == 0 { 86 + return r.updatePoolStatus(ctx, &hsmPool, hsmv1alpha1.HSMPoolPhaseError, nil, nil, 0, "HSMPool has no owner references") 87 + } 88 + 89 + deviceRef := hsmPool.OwnerReferences[0].Name 90 + hsmDevices := make([]*hsmv1alpha1.HSMDevice, 0, 1) 91 + hsmDevice := &hsmv1alpha1.HSMDevice{} 92 + if err := r.Get(ctx, client.ObjectKey{ 93 + Name: deviceRef, 94 + Namespace: hsmPool.Namespace, 95 + }, hsmDevice); err != nil { 96 + logger.Error(err, "Unable to fetch referenced HSMDevice", "hsmDevice", deviceRef) 97 + return r.updatePoolStatus(ctx, &hsmPool, hsmv1alpha1.HSMPoolPhaseError, nil, nil, 0, fmt.Sprintf("HSMDevice %s not found", deviceRef)) 96 98 } 99 + hsmDevices = append(hsmDevices, hsmDevice) 97 100 98 101 // Find discovery pods and their annotations 99 102 podReports, aggregatedDevices, expectedPods, err := r.collectPodReports(ctx, hsmDevices) ··· 398 401 399 402 var requests []ctrl.Request 400 403 for _, pool := range pools.Items { 401 - // Check if this pool references the HSMDevice in the report 402 - for _, deviceRef := range pool.Spec.HSMDeviceRefs { 403 - if deviceRef == discoveryReport.HSMDeviceName { 404 - requests = append(requests, ctrl.Request{ 405 - NamespacedName: client.ObjectKey{ 406 - Name: pool.Name, 407 - Namespace: pool.Namespace, 408 - }, 409 - }) 410 - break // Don't add the same pool multiple times 411 - } 404 + // Check if this pool references the HSMDevice in the report (from ownerReferences) 405 + if len(pool.OwnerReferences) > 0 && pool.OwnerReferences[0].Name == discoveryReport.HSMDeviceName { 406 + requests = append(requests, ctrl.Request{ 407 + NamespacedName: client.ObjectKey{ 408 + Name: pool.Name, 409 + Namespace: pool.Namespace, 410 + }, 411 + }) 412 412 } 413 413 } 414 414
+18 -150
internal/controller/hsmpool_controller_test.go
··· 140 140 ObjectMeta: metav1.ObjectMeta{ 141 141 Name: hsmPoolName, 142 142 Namespace: hsmPoolNamespace, 143 - }, 144 - Spec: hsmv1alpha1.HSMPoolSpec{ 145 - HSMDeviceRefs: []string{hsmDeviceName}, 143 + OwnerReferences: []metav1.OwnerReference{ 144 + { 145 + APIVersion: "hsm.j5t.io/v1alpha1", 146 + Kind: "HSMDevice", 147 + Name: hsmDevice.Name, 148 + UID: hsmDevice.UID, 149 + }, 150 + }, 146 151 }, 152 + Spec: hsmv1alpha1.HSMPoolSpec{}, 147 153 } 148 154 Expect(k8sClient.Create(ctx, hsmPool)).To(Succeed()) 149 155 }) ··· 286 292 ObjectMeta: metav1.ObjectMeta{ 287 293 Name: missingPoolName, 288 294 Namespace: hsmPoolNamespace, 289 - }, 290 - Spec: hsmv1alpha1.HSMPoolSpec{ 291 - HSMDeviceRefs: []string{"non-existent-device"}, 295 + OwnerReferences: []metav1.OwnerReference{ 296 + { 297 + APIVersion: "hsm.j5t.io/v1alpha1", 298 + Kind: "HSMDevice", 299 + Name: "non-existent-device", 300 + UID: "fake-uid-456", 301 + }, 302 + }, 292 303 }, 304 + Spec: hsmv1alpha1.HSMPoolSpec{}, 293 305 } 294 306 Expect(k8sClient.Create(ctx, missingPool)).To(Succeed()) 295 307 ··· 316 328 }, pool) 317 329 return pool.Status.Phase 318 330 }).Should(Equal(hsmv1alpha1.HSMPoolPhaseError)) 319 - }) 320 - 321 - It("Should aggregate devices from multiple HSMDevices", func() { 322 - By("Creating a second HSMDevice") 323 - secondDeviceName := fmt.Sprintf("second-device-%d", GinkgoRandomSeed()) 324 - secondDevice := &hsmv1alpha1.HSMDevice{ 325 - ObjectMeta: metav1.ObjectMeta{ 326 - Name: secondDeviceName, 327 - Namespace: hsmPoolNamespace, 328 - }, 329 - Spec: hsmv1alpha1.HSMDeviceSpec{ 330 - DeviceType: "SmartCard-HSM", 331 - }, 332 - } 333 - Expect(k8sClient.Create(ctx, secondDevice)).To(Succeed()) 334 - 335 - By("Creating a second DaemonSet") 336 - secondDaemonSet := &appsv1.DaemonSet{ 337 - ObjectMeta: metav1.ObjectMeta{ 338 - Name: fmt.Sprintf("%s-discovery", secondDeviceName), 339 - Namespace: hsmPoolNamespace, 340 - Labels: map[string]string{ 341 - "app.kubernetes.io/name": "hsm-secrets-operator", 342 - "app.kubernetes.io/component": "discovery", 343 - "hsm.j5t.io/device": secondDeviceName, 344 - }, 345 - OwnerReferences: []metav1.OwnerReference{ 346 - { 347 - APIVersion: "hsm.j5t.io/v1alpha1", 348 - Kind: "HSMDevice", 349 - Name: secondDeviceName, 350 - UID: secondDevice.UID, 351 - Controller: &[]bool{true}[0], 352 - }, 353 - }, 354 - }, 355 - Spec: appsv1.DaemonSetSpec{ 356 - Selector: &metav1.LabelSelector{ 357 - MatchLabels: map[string]string{ 358 - "app.kubernetes.io/name": "hsm-secrets-operator", 359 - "app.kubernetes.io/component": "discovery", 360 - "hsm.j5t.io/device": secondDeviceName, 361 - }, 362 - }, 363 - Template: corev1.PodTemplateSpec{ 364 - ObjectMeta: metav1.ObjectMeta{ 365 - Labels: map[string]string{ 366 - "app.kubernetes.io/name": "hsm-secrets-operator", 367 - "app.kubernetes.io/component": "discovery", 368 - "hsm.j5t.io/device": secondDeviceName, 369 - }, 370 - }, 371 - Spec: corev1.PodSpec{ 372 - Containers: []corev1.Container{ 373 - { 374 - Name: "discovery", 375 - Image: "test-discovery:latest", 376 - }, 377 - }, 378 - }, 379 - }, 380 - }, 381 - Status: appsv1.DaemonSetStatus{ 382 - DesiredNumberScheduled: 1, 383 - NumberReady: 1, 384 - }, 385 - } 386 - Expect(k8sClient.Create(ctx, secondDaemonSet)).To(Succeed()) 387 - 388 - // Update DaemonSet status separately (status is not created with the resource) 389 - secondDaemonSet.Status = appsv1.DaemonSetStatus{ 390 - DesiredNumberScheduled: 1, 391 - NumberReady: 1, 392 - } 393 - Expect(k8sClient.Status().Update(ctx, secondDaemonSet)).To(Succeed()) 394 - 395 - By("Updating HSMPool to reference both devices") 396 - Eventually(func() error { 397 - pool := &hsmv1alpha1.HSMPool{} 398 - if err := k8sClient.Get(ctx, types.NamespacedName{ 399 - Name: hsmPoolName, 400 - Namespace: hsmPoolNamespace, 401 - }, pool); err != nil { 402 - return err 403 - } 404 - pool.Spec.HSMDeviceRefs = []string{hsmDeviceName, secondDeviceName} 405 - return k8sClient.Update(ctx, pool) 406 - }).Should(Succeed()) 407 - 408 - By("Creating pods for the second DaemonSet") 409 - secondPod := &corev1.Pod{ 410 - ObjectMeta: metav1.ObjectMeta{ 411 - Name: fmt.Sprintf("%s-pod-1", secondDeviceName), 412 - Namespace: hsmPoolNamespace, 413 - Labels: map[string]string{ 414 - "app.kubernetes.io/name": "hsm-secrets-operator", 415 - "app.kubernetes.io/component": "discovery", 416 - "hsm.j5t.io/device": secondDeviceName, 417 - }, 418 - }, 419 - Spec: corev1.PodSpec{ 420 - NodeName: "node-3", 421 - Containers: []corev1.Container{ 422 - { 423 - Name: "discovery", 424 - Image: "test-discovery:latest", 425 - }, 426 - }, 427 - }, 428 - Status: corev1.PodStatus{ 429 - Phase: corev1.PodRunning, 430 - }, 431 - } 432 - Expect(k8sClient.Create(ctx, secondPod)).To(Succeed()) 433 - 434 - // Update pod status separately (status is not created with the resource) 435 - secondPod.Status = corev1.PodStatus{ 436 - Phase: corev1.PodRunning, 437 - } 438 - Expect(k8sClient.Status().Update(ctx, secondPod)).To(Succeed()) 439 - 440 - By("Reconciling the updated HSMPool") 441 - reconciler := &HSMPoolReconciler{ 442 - Client: k8sClient, 443 - Scheme: k8sClient.Scheme(), 444 - } 445 - 446 - _, err := reconciler.Reconcile(ctx, ctrl.Request{ 447 - NamespacedName: types.NamespacedName{ 448 - Name: hsmPoolName, 449 - Namespace: hsmPoolNamespace, 450 - }, 451 - }) 452 - Expect(err).NotTo(HaveOccurred()) 453 - 454 - By("Checking that HSMPool aggregates pods from both DaemonSets") 455 - pool := &hsmv1alpha1.HSMPool{} 456 - Eventually(func() int32 { 457 - _ = k8sClient.Get(ctx, types.NamespacedName{ 458 - Name: hsmPoolName, 459 - Namespace: hsmPoolNamespace, 460 - }, pool) 461 - return pool.Status.ExpectedPods 462 - }).Should(Equal(int32(3))) // 2 from first DaemonSet + 1 from second DaemonSet 463 331 }) 464 332 465 333 It("Should read device count from pod annotations when available", func() {
+15 -1
internal/controller/hsmsecret_controller.go
··· 192 192 193 193 // Ensure agent pods are running for all devices and create clients 194 194 for _, hsmDevice := range hsmDevices { 195 + // Get the HSMPool for this device 196 + poolName := hsmDevice.Name + "-pool" 197 + var hsmPool hsmv1alpha1.HSMPool 198 + if err := r.Get(ctx, types.NamespacedName{ 199 + Name: poolName, 200 + Namespace: hsmDevice.Namespace, 201 + }, &hsmPool); err != nil { 202 + // Clean up any successful connections before returning error 203 + if err := deviceClients.Close(); err != nil { 204 + logger.Error(err, "Failed to close device clients during cleanup") 205 + } 206 + return nil, fmt.Errorf("failed to get HSMPool %s for device %s: %w", poolName, hsmDevice.Name, err) 207 + } 208 + 195 209 // EnsureAgent ensures agents for all devices in the pool 196 - err = r.AgentManager.EnsureAgent(ctx, hsmDevice, hsmSecret) 210 + err = r.AgentManager.EnsureAgent(ctx, &hsmPool) 197 211 if err != nil { 198 212 // Clean up any successful connections before returning error 199 213 if err := deviceClients.Close(); err != nil {
+12 -15
internal/controller/hsmsecret_grpc_test.go
··· 107 107 // Create agent manager and add fake agent info with correct port mapping 108 108 agentManager := agent.NewManager(nil, "default", nil) 109 109 agentManager.SetAgentInfo("test-hsm-device", &agent.AgentInfo{ 110 - DeviceName: "test-hsm-device", 111 - PodIPs: []string{"127.0.0.1"}, 112 - Status: agent.AgentStatusReady, 113 - AgentName: "hsm-agent-test-hsm-device", 114 - Namespace: "default", 110 + PodIPs: []string{"127.0.0.1"}, 111 + Status: agent.AgentStatusReady, 112 + AgentName: "hsm-agent-test-hsm-device", 113 + Namespace: "default", 115 114 }) 116 115 117 116 // Create fake client with test objects ··· 293 292 294 293 // Restore agent info for cleanup 295 294 agentManager.SetAgentInfo("test-hsm-device", &agent.AgentInfo{ 296 - DeviceName: "test-hsm-device", 297 - PodIPs: []string{"127.0.0.1"}, 298 - Status: agent.AgentStatusReady, 299 - AgentName: "hsm-agent-test-hsm-device", 300 - Namespace: "default", 295 + PodIPs: []string{"127.0.0.1"}, 296 + Status: agent.AgentStatusReady, 297 + AgentName: "hsm-agent-test-hsm-device", 298 + Namespace: "default", 301 299 }) 302 300 }) 303 301 ··· 394 392 // Create agent manager with invalid endpoint 395 393 agentManager := agent.NewManager(nil, "default", nil) 396 394 agentManager.SetAgentInfo("test-hsm-device", &agent.AgentInfo{ 397 - DeviceName: "test-hsm-device", 398 - PodIPs: []string{"127.0.0.1:99999"}, // Non-existent port 399 - Status: agent.AgentStatusReady, 400 - AgentName: "hsm-agent-test-hsm-device", 401 - Namespace: "default", 395 + PodIPs: []string{"127.0.0.1:99999"}, // Non-existent port 396 + Status: agent.AgentStatusReady, 397 + AgentName: "hsm-agent-test-hsm-device", 398 + Namespace: "default", 402 399 }) 403 400 404 401 // Create controller