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 pod reporting

+221 -16
+63
cmd/discovery/main.go
··· 18 18 19 19 import ( 20 20 "context" 21 + "encoding/json" 21 22 "flag" 22 23 "fmt" 23 24 "os" ··· 28 29 _ "k8s.io/client-go/plugin/pkg/client/auth" 29 30 30 31 "github.com/go-logr/logr" 32 + corev1 "k8s.io/api/core/v1" 31 33 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 32 34 "k8s.io/apimachinery/pkg/runtime" 35 + "k8s.io/apimachinery/pkg/types" 33 36 utilruntime "k8s.io/apimachinery/pkg/util/runtime" 34 37 clientgoscheme "k8s.io/client-go/kubernetes/scheme" 35 38 ctrl "sigs.k8s.io/controller-runtime" ··· 232 235 "node", d.nodeName, 233 236 "devicesFound", len(discoveredDevices)) 234 237 238 + // Update pod annotation with discovery results 239 + if err := d.updatePodAnnotation(ctx, hsmDevice.Name, discoveredDevices); err != nil { 240 + d.logger.Error(err, "Failed to update pod annotation", "device", hsmDevice.Name) 241 + return err 242 + } 243 + 235 244 return nil 236 245 } 237 246 ··· 370 379 371 380 return false 372 381 } 382 + 383 + // updatePodAnnotation updates the pod's annotation with discovery results 384 + func (d *DiscoveryAgent) updatePodAnnotation( 385 + ctx context.Context, hsmDeviceName string, discoveredDevices []hsmv1alpha1.DiscoveredDevice, 386 + ) error { 387 + // Create discovery report 388 + report := PodDiscoveryReport{ 389 + HSMDeviceName: hsmDeviceName, 390 + ReportingNode: d.nodeName, 391 + DiscoveredDevices: discoveredDevices, 392 + LastReportTime: metav1.Now(), 393 + DiscoveryStatus: "completed", 394 + } 395 + 396 + // Marshal report to JSON 397 + reportJSON, err := json.Marshal(report) 398 + if err != nil { 399 + return fmt.Errorf("failed to marshal discovery report: %w", err) 400 + } 401 + 402 + // Get the current pod 403 + pod := &corev1.Pod{} 404 + podKey := types.NamespacedName{ 405 + Name: d.podName, 406 + Namespace: d.podNamespace, 407 + } 408 + 409 + if err := d.client.Get(ctx, podKey, pod); err != nil { 410 + return fmt.Errorf("failed to get pod %s/%s: %w", d.podNamespace, d.podName, err) 411 + } 412 + 413 + // Create a copy for patching 414 + podCopy := pod.DeepCopy() 415 + 416 + // Initialize annotations if nil 417 + if podCopy.Annotations == nil { 418 + podCopy.Annotations = make(map[string]string) 419 + } 420 + 421 + // Update the annotation 422 + podCopy.Annotations[DeviceReportAnnotation] = string(reportJSON) 423 + 424 + // Patch the pod 425 + if err := d.client.Patch(ctx, podCopy, client.MergeFrom(pod)); err != nil { 426 + return fmt.Errorf("failed to update pod annotation: %w", err) 427 + } 428 + 429 + d.logger.V(1).Info("Updated pod annotation with discovery report", 430 + "device", hsmDeviceName, 431 + "devicesFound", len(discoveredDevices), 432 + "pod", d.podName) 433 + 434 + return nil 435 + }
+2 -2
helm/hsm-secrets-operator/Chart.yaml
··· 2 2 name: hsm-secrets-operator 3 3 description: A Kubernetes operator that bridges Pico HSM binary data storage with Kubernetes Secrets 4 4 type: application 5 - version: 0.4.2 6 - appVersion: v0.4.2 5 + version: 0.4.3 6 + appVersion: v0.4.3 7 7 icon: https://raw.githubusercontent.com/cncf/artwork/master/projects/kubernetes/icon/color/kubernetes-icon-color.svg 8 8 home: https://github.com/evanjarrett/hsm-secrets-operator 9 9 sources:
+47 -13
internal/controller/hsmpool_controller.go
··· 96 96 } 97 97 98 98 // Find discovery pods and their annotations 99 - podReports, expectedPods, err := r.collectPodReports(ctx, hsmDevices) 99 + podReports, aggregatedDevices, expectedPods, err := r.collectPodReports(ctx, hsmDevices) 100 100 if err != nil { 101 101 logger.Error(err, "Failed to collect pod reports") 102 102 return r.updatePoolStatus(ctx, &hsmPool, hsmv1alpha1.HSMPoolPhaseError, nil, nil, expectedPods, err.Error()) ··· 105 105 // Aggregate devices from all pod reports 106 106 phase := r.aggregateDevices(podReports, expectedPods) 107 107 108 - // Update pool status (TODO: implement device aggregation when needed) 109 - var aggregatedDevices []hsmv1alpha1.DiscoveredDevice 110 108 return r.updatePoolStatus(ctx, &hsmPool, phase, aggregatedDevices, podReports, expectedPods, "") 111 109 } 112 110 113 111 // collectPodReports finds discovery DaemonSet pods owned by HSMDevices and queries their status 114 - func (r *HSMPoolReconciler) collectPodReports(ctx context.Context, hsmDevices []*hsmv1alpha1.HSMDevice) ([]hsmv1alpha1.PodReport, int32, error) { 112 + func (r *HSMPoolReconciler) collectPodReports(ctx context.Context, hsmDevices []*hsmv1alpha1.HSMDevice) ([]hsmv1alpha1.PodReport, []hsmv1alpha1.DiscoveredDevice, int32, error) { 115 113 logger := log.FromContext(ctx) 116 114 117 115 podReports := make([]hsmv1alpha1.PodReport, 0) 116 + var allDevices []hsmv1alpha1.DiscoveredDevice 118 117 totalExpectedPods := int32(0) 119 118 120 119 // For each HSMDevice referenced by this pool, find its DaemonSet and pods ··· 149 148 } 150 149 151 150 if err := r.List(ctx, pods, listOpts); err != nil { 152 - return nil, totalExpectedPods, fmt.Errorf("failed to list DaemonSet pods for device %s: %w", hsmDevice.Name, err) 151 + return nil, nil, totalExpectedPods, fmt.Errorf("failed to list DaemonSet pods for device %s: %w", hsmDevice.Name, err) 153 152 } 154 153 155 - // Create pod reports from pod status (simplified - no annotation parsing needed) 154 + // Create pod reports from pod annotations 156 155 for _, pod := range pods.Items { 157 156 podReport := hsmv1alpha1.PodReport{ 158 157 PodName: pod.Name, ··· 162 161 Fresh: r.isPodFresh(&pod), 163 162 } 164 163 165 - // For now, assume 1 device found per pod if pod is ready 166 - // TODO: In the future, discovery pods could report via status or configmap 167 - if pod.Status.Phase == corev1.PodRunning { 168 - podReport.DevicesFound = 1 169 - } else { 170 - podReport.DevicesFound = 0 164 + podReport.DevicesFound = 0 165 + // Parse device count from pod annotation if available 166 + if devicesFound, status, reportTime := r.parseDeviceReportAnnotation(&pod); devicesFound >= 0 { 167 + podReport.DevicesFound = devicesFound 168 + if status != "" { 169 + podReport.DiscoveryStatus = status 170 + } 171 + if !reportTime.IsZero() { 172 + podReport.LastReportTime = reportTime 173 + } 174 + 175 + // Also collect the actual discovered devices from annotation 176 + if pod.Annotations != nil { 177 + if reportJSON, exists := pod.Annotations[deviceReportAnnotation]; exists { 178 + var discoveryReport PodDiscoveryReport 179 + if err := json.Unmarshal([]byte(reportJSON), &discoveryReport); err == nil { 180 + allDevices = append(allDevices, discoveryReport.DiscoveredDevices...) 181 + } 182 + } 183 + } 171 184 } 172 185 173 186 podReports = append(podReports, podReport) 174 187 } 175 188 } 176 189 177 - return podReports, totalExpectedPods, nil 190 + return podReports, allDevices, totalExpectedPods, nil 178 191 } 179 192 180 193 // getPodDiscoveryStatus determines the discovery status based on pod phase and conditions ··· 201 214 // For now, consider all running pods as fresh 202 215 // TODO: Could check pod start time or last transition time 203 216 return true 217 + } 218 + 219 + // parseDeviceReportAnnotation parses the device discovery report from pod annotation 220 + // Returns (devicesFound, discoveryStatus, lastReportTime) or (-1, "", time.Time{}) if not found/invalid 221 + func (r *HSMPoolReconciler) parseDeviceReportAnnotation(pod *corev1.Pod) (int32, string, metav1.Time) { 222 + if pod.Annotations == nil { 223 + return -1, "", metav1.Time{} 224 + } 225 + 226 + reportJSON, exists := pod.Annotations[deviceReportAnnotation] 227 + if !exists { 228 + return -1, "", metav1.Time{} 229 + } 230 + 231 + var report PodDiscoveryReport 232 + if err := json.Unmarshal([]byte(reportJSON), &report); err != nil { 233 + // Log error but don't fail - return fallback values 234 + return -1, "", metav1.Time{} 235 + } 236 + 237 + return int32(len(report.DiscoveredDevices)), report.DiscoveryStatus, report.LastReportTime 204 238 } 205 239 206 240 // aggregateDevices determines the pool phase based on pod reports
+109 -1
internal/controller/hsmpool_controller_test.go
··· 266 266 Expect(runningReport).NotTo(BeNil()) 267 267 Expect(runningReport.NodeName).To(Equal("node-1")) 268 268 Expect(runningReport.DiscoveryStatus).To(Equal("completed")) 269 - Expect(runningReport.DevicesFound).To(Equal(int32(1))) // Running pod reports 1 device 269 + Expect(runningReport.DevicesFound).To(Equal(int32(0))) // Running pod without annotation reports 0 devices 270 270 Expect(runningReport.Fresh).To(BeTrue()) 271 271 272 272 Expect(pendingReport).NotTo(BeNil()) ··· 460 460 }, pool) 461 461 return pool.Status.ExpectedPods 462 462 }).Should(Equal(int32(3))) // 2 from first DaemonSet + 1 from second DaemonSet 463 + }) 464 + 465 + It("Should read device count from pod annotations when available", func() { 466 + By("Creating a discovery pod with annotation-based device report") 467 + podWithAnnotation := &corev1.Pod{ 468 + ObjectMeta: metav1.ObjectMeta{ 469 + Name: fmt.Sprintf("%s-annotated-pod", hsmDeviceName), 470 + Namespace: hsmPoolNamespace, 471 + Labels: map[string]string{ 472 + "app.kubernetes.io/name": "hsm-secrets-operator", 473 + "app.kubernetes.io/component": "discovery", 474 + "hsm.j5t.io/device": hsmDeviceName, 475 + }, 476 + Annotations: map[string]string{ 477 + "hsm.j5t.io/device-report": `{ 478 + "hsmDeviceName": "` + hsmDeviceName + `", 479 + "reportingNode": "node-1", 480 + "discoveredDevices": [ 481 + { 482 + "devicePath": "/dev/bus/usb/001/015", 483 + "serialNumber": "DC6A33145E23A42A", 484 + "nodeName": "node-1", 485 + "lastSeen": "2025-08-19T10:00:00Z", 486 + "available": true 487 + }, 488 + { 489 + "devicePath": "/dev/bus/usb/001/016", 490 + "serialNumber": "DC6A33145E23A42B", 491 + "nodeName": "node-1", 492 + "lastSeen": "2025-08-19T10:00:00Z", 493 + "available": true 494 + } 495 + ], 496 + "lastReportTime": "2025-08-19T10:00:00Z", 497 + "discoveryStatus": "completed" 498 + }`, 499 + }, 500 + }, 501 + Spec: corev1.PodSpec{ 502 + NodeName: "node-1", 503 + Containers: []corev1.Container{ 504 + { 505 + Name: "discovery", 506 + Image: "test-discovery:latest", 507 + }, 508 + }, 509 + }, 510 + Status: corev1.PodStatus{ 511 + Phase: corev1.PodRunning, 512 + }, 513 + } 514 + Expect(k8sClient.Create(ctx, podWithAnnotation)).To(Succeed()) 515 + 516 + // Update pod status separately (status is not created with the resource) 517 + podWithAnnotation.Status = corev1.PodStatus{ 518 + Phase: corev1.PodRunning, 519 + } 520 + Expect(k8sClient.Status().Update(ctx, podWithAnnotation)).To(Succeed()) 521 + 522 + By("Reconciling the HSMPool") 523 + reconciler := &HSMPoolReconciler{ 524 + Client: k8sClient, 525 + Scheme: k8sClient.Scheme(), 526 + } 527 + 528 + _, err := reconciler.Reconcile(ctx, ctrl.Request{ 529 + NamespacedName: types.NamespacedName{ 530 + Name: hsmPoolName, 531 + Namespace: hsmPoolNamespace, 532 + }, 533 + }) 534 + Expect(err).NotTo(HaveOccurred()) 535 + 536 + By("Checking that HSMPool reads device count from annotation") 537 + pool := &hsmv1alpha1.HSMPool{} 538 + Eventually(func() bool { 539 + _ = k8sClient.Get(ctx, types.NamespacedName{ 540 + Name: hsmPoolName, 541 + Namespace: hsmPoolNamespace, 542 + }, pool) 543 + 544 + // Find the pod with annotation 545 + for _, report := range pool.Status.ReportingPods { 546 + if report.PodName == podWithAnnotation.Name { 547 + return report.DevicesFound == 2 // Should read 2 devices from annotation 548 + } 549 + } 550 + return false 551 + }).Should(BeTrue()) 552 + 553 + // Verify the specific report details 554 + var annotatedReport *hsmv1alpha1.PodReport 555 + for i := range pool.Status.ReportingPods { 556 + report := &pool.Status.ReportingPods[i] 557 + if report.PodName == podWithAnnotation.Name { 558 + annotatedReport = report 559 + break 560 + } 561 + } 562 + 563 + Expect(annotatedReport).NotTo(BeNil()) 564 + Expect(annotatedReport.DevicesFound).To(Equal(int32(2))) // 2 devices from annotation 565 + Expect(annotatedReport.DiscoveryStatus).To(Equal("completed")) // Status from annotation 566 + Expect(annotatedReport.NodeName).To(Equal("node-1")) 567 + 568 + // Verify that TotalDevices and AvailableDevices are correctly aggregated 569 + Expect(pool.Status.TotalDevices).To(Equal(int32(2))) // Should aggregate 2 devices total 570 + Expect(pool.Status.AvailableDevices).To(Equal(int32(2))) // Both devices are available 463 571 }) 464 572 }) 465 573 })