/* Copyright 2025. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package controller import ( "context" "fmt" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/intstr" ctrl "sigs.k8s.io/controller-runtime" hsmv1alpha1 "github.com/evanjarrett/hsm-secrets-operator/api/v1alpha1" ) var _ = Describe("HSMPoolReconciler with Manager-Owned DaemonSets", func() { Context("When reconciling an HSMPool with DaemonSet pods", func() { var ( hsmDeviceName string hsmPoolName string hsmPoolNamespace = "default" ) ctx := context.Background() var hsmDevice *hsmv1alpha1.HSMDevice var hsmPool *hsmv1alpha1.HSMPool var daemonSet *appsv1.DaemonSet BeforeEach(func() { // Generate unique resource names for each test hsmDeviceName = fmt.Sprintf("test-pool-device-%d", GinkgoRandomSeed()) hsmPoolName = fmt.Sprintf("test-pool-%d", GinkgoRandomSeed()) // Create HSMDevice hsmDevice = &hsmv1alpha1.HSMDevice{ ObjectMeta: metav1.ObjectMeta{ Name: hsmDeviceName, Namespace: hsmPoolNamespace, }, Spec: hsmv1alpha1.HSMDeviceSpec{ DeviceType: "PicoHSM", Discovery: &hsmv1alpha1.DiscoverySpec{ USB: &hsmv1alpha1.USBDeviceSpec{ VendorID: "20a0", ProductID: "4230", }, }, }, } Expect(k8sClient.Create(ctx, hsmDevice)).To(Succeed()) // Create DaemonSet owned by HSMDevice (simulating DiscoveryDaemonSetReconciler) daemonSet = &appsv1.DaemonSet{ ObjectMeta: metav1.ObjectMeta{ Name: fmt.Sprintf("%s-discovery", hsmDeviceName), Namespace: hsmPoolNamespace, Labels: map[string]string{ "app.kubernetes.io/name": "hsm-secrets-operator", "app.kubernetes.io/component": "discovery", "hsm.j5t.io/device": hsmDeviceName, }, OwnerReferences: []metav1.OwnerReference{ { APIVersion: "hsm.j5t.io/v1alpha1", Kind: "HSMDevice", Name: hsmDeviceName, UID: hsmDevice.UID, Controller: &[]bool{true}[0], }, }, }, Spec: appsv1.DaemonSetSpec{ Selector: &metav1.LabelSelector{ MatchLabels: map[string]string{ "app.kubernetes.io/name": "hsm-secrets-operator", "app.kubernetes.io/component": "discovery", "hsm.j5t.io/device": hsmDeviceName, }, }, Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{ "app.kubernetes.io/name": "hsm-secrets-operator", "app.kubernetes.io/component": "discovery", "hsm.j5t.io/device": hsmDeviceName, }, }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "discovery", Image: "test-discovery:latest", }, }, }, }, UpdateStrategy: appsv1.DaemonSetUpdateStrategy{ Type: appsv1.RollingUpdateDaemonSetStrategyType, RollingUpdate: &appsv1.RollingUpdateDaemonSet{ MaxUnavailable: &intstr.IntOrString{Type: intstr.Int, IntVal: 1}, }, }, }, Status: appsv1.DaemonSetStatus{ DesiredNumberScheduled: 2, NumberReady: 1, }, } Expect(k8sClient.Create(ctx, daemonSet)).To(Succeed()) // Update DaemonSet status separately (status is not created with the resource) daemonSet.Status = appsv1.DaemonSetStatus{ DesiredNumberScheduled: 2, NumberReady: 1, } Expect(k8sClient.Status().Update(ctx, daemonSet)).To(Succeed()) // Create HSMPool referencing the HSMDevice hsmPool = &hsmv1alpha1.HSMPool{ ObjectMeta: metav1.ObjectMeta{ Name: hsmPoolName, Namespace: hsmPoolNamespace, OwnerReferences: []metav1.OwnerReference{ { APIVersion: "hsm.j5t.io/v1alpha1", Kind: "HSMDevice", Name: hsmDevice.Name, UID: hsmDevice.UID, }, }, }, Spec: hsmv1alpha1.HSMPoolSpec{}, } Expect(k8sClient.Create(ctx, hsmPool)).To(Succeed()) }) AfterEach(func() { // Clean up resources if hsmPool != nil { _ = k8sClient.Delete(ctx, hsmPool) } if daemonSet != nil { _ = k8sClient.Delete(ctx, daemonSet) } if hsmDevice != nil { _ = k8sClient.Delete(ctx, hsmDevice) } }) It("Should collect pod reports from DaemonSet pods directly", func() { By("Creating some discovery pods for the DaemonSet") runningPod := &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: fmt.Sprintf("%s-pod-1", hsmDeviceName), Namespace: hsmPoolNamespace, Labels: map[string]string{ "app.kubernetes.io/name": "hsm-secrets-operator", "app.kubernetes.io/component": "discovery", "hsm.j5t.io/device": hsmDeviceName, }, }, Spec: corev1.PodSpec{ NodeName: "node-1", Containers: []corev1.Container{ { Name: "discovery", Image: "test-discovery:latest", }, }, }, Status: corev1.PodStatus{ Phase: corev1.PodRunning, }, } Expect(k8sClient.Create(ctx, runningPod)).To(Succeed()) // Update pod status separately (status is not created with the resource) runningPod.Status = corev1.PodStatus{ Phase: corev1.PodRunning, } Expect(k8sClient.Status().Update(ctx, runningPod)).To(Succeed()) pendingPod := &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: fmt.Sprintf("%s-pod-2", hsmDeviceName), Namespace: hsmPoolNamespace, Labels: map[string]string{ "app.kubernetes.io/name": "hsm-secrets-operator", "app.kubernetes.io/component": "discovery", "hsm.j5t.io/device": hsmDeviceName, }, }, Spec: corev1.PodSpec{ NodeName: "node-2", Containers: []corev1.Container{ { Name: "discovery", Image: "test-discovery:latest", }, }, }, Status: corev1.PodStatus{ Phase: corev1.PodPending, }, } Expect(k8sClient.Create(ctx, pendingPod)).To(Succeed()) // Update pod status separately (status is not created with the resource) pendingPod.Status = corev1.PodStatus{ Phase: corev1.PodPending, } Expect(k8sClient.Status().Update(ctx, pendingPod)).To(Succeed()) By("Reconciling the HSMPool") reconciler := &HSMPoolReconciler{ Client: k8sClient, Scheme: k8sClient.Scheme(), } _, err := reconciler.Reconcile(ctx, ctrl.Request{ NamespacedName: types.NamespacedName{ Name: hsmPoolName, Namespace: hsmPoolNamespace, }, }) Expect(err).NotTo(HaveOccurred()) By("Checking that HSMPool status was updated with pod information") pool := &hsmv1alpha1.HSMPool{} Eventually(func() bool { _ = k8sClient.Get(ctx, types.NamespacedName{ Name: hsmPoolName, Namespace: hsmPoolNamespace, }, pool) return len(pool.Status.ReportingPods) > 0 }).Should(BeTrue()) // Verify reporting pods were collected from DaemonSet Expect(pool.Status.ReportingPods).To(HaveLen(2)) var runningReport, pendingReport *hsmv1alpha1.PodReport for i := range pool.Status.ReportingPods { report := &pool.Status.ReportingPods[i] switch report.PodName { case runningPod.Name: runningReport = report case pendingPod.Name: pendingReport = report } } Expect(runningReport).NotTo(BeNil()) Expect(runningReport.NodeName).To(Equal("node-1")) Expect(runningReport.DiscoveryStatus).To(Equal("completed")) Expect(runningReport.DevicesFound).To(Equal(int32(0))) // Running pod without annotation reports 0 devices Expect(runningReport.Fresh).To(BeTrue()) Expect(pendingReport).NotTo(BeNil()) Expect(pendingReport.NodeName).To(Equal("node-2")) Expect(pendingReport.DiscoveryStatus).To(Equal("pending")) Expect(pendingReport.DevicesFound).To(Equal(int32(0))) // Pending pod reports 0 devices Expect(pendingReport.Fresh).To(BeFalse()) // Pending pods are not considered fresh // Verify expected pods count from DaemonSet status Expect(pool.Status.ExpectedPods).To(Equal(int32(2))) }) It("Should handle missing DaemonSet gracefully", func() { By("Creating HSMPool referencing non-existent HSMDevice") missingPoolName := fmt.Sprintf("missing-pool-%d", GinkgoRandomSeed()) missingPool := &hsmv1alpha1.HSMPool{ ObjectMeta: metav1.ObjectMeta{ Name: missingPoolName, Namespace: hsmPoolNamespace, OwnerReferences: []metav1.OwnerReference{ { APIVersion: "hsm.j5t.io/v1alpha1", Kind: "HSMDevice", Name: "non-existent-device", UID: "fake-uid-456", }, }, }, Spec: hsmv1alpha1.HSMPoolSpec{}, } Expect(k8sClient.Create(ctx, missingPool)).To(Succeed()) By("Reconciling the HSMPool with missing HSMDevice") reconciler := &HSMPoolReconciler{ Client: k8sClient, Scheme: k8sClient.Scheme(), } _, err := reconciler.Reconcile(ctx, ctrl.Request{ NamespacedName: types.NamespacedName{ Name: missingPoolName, Namespace: hsmPoolNamespace, }, }) Expect(err).NotTo(HaveOccurred()) By("Checking that HSMPool status indicates error") pool := &hsmv1alpha1.HSMPool{} Eventually(func() hsmv1alpha1.HSMPoolPhase { _ = k8sClient.Get(ctx, types.NamespacedName{ Name: missingPoolName, Namespace: hsmPoolNamespace, }, pool) return pool.Status.Phase }).Should(Equal(hsmv1alpha1.HSMPoolPhaseError)) }) It("Should read device count from pod annotations when available", func() { By("Creating a discovery pod with annotation-based device report") podWithAnnotation := &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: fmt.Sprintf("%s-annotated-pod", hsmDeviceName), Namespace: hsmPoolNamespace, Labels: map[string]string{ "app.kubernetes.io/name": "hsm-secrets-operator", "app.kubernetes.io/component": "discovery", "hsm.j5t.io/device": hsmDeviceName, }, Annotations: map[string]string{ "hsm.j5t.io/device-report": `{ "hsmDeviceName": "` + hsmDeviceName + `", "reportingNode": "node-1", "discoveredDevices": [ { "devicePath": "/dev/bus/usb/001/015", "serialNumber": "DC6A33145E23A42A", "nodeName": "node-1", "lastSeen": "2025-08-19T10:00:00Z", "available": true }, { "devicePath": "/dev/bus/usb/001/016", "serialNumber": "DC6A33145E23A42B", "nodeName": "node-1", "lastSeen": "2025-08-19T10:00:00Z", "available": true } ], "lastReportTime": "2025-08-19T10:00:00Z", "discoveryStatus": "completed" }`, }, }, Spec: corev1.PodSpec{ NodeName: "node-1", Containers: []corev1.Container{ { Name: "discovery", Image: "test-discovery:latest", }, }, }, Status: corev1.PodStatus{ Phase: corev1.PodRunning, }, } Expect(k8sClient.Create(ctx, podWithAnnotation)).To(Succeed()) // Update pod status separately (status is not created with the resource) podWithAnnotation.Status = corev1.PodStatus{ Phase: corev1.PodRunning, } Expect(k8sClient.Status().Update(ctx, podWithAnnotation)).To(Succeed()) By("Reconciling the HSMPool") reconciler := &HSMPoolReconciler{ Client: k8sClient, Scheme: k8sClient.Scheme(), } _, err := reconciler.Reconcile(ctx, ctrl.Request{ NamespacedName: types.NamespacedName{ Name: hsmPoolName, Namespace: hsmPoolNamespace, }, }) Expect(err).NotTo(HaveOccurred()) By("Checking that HSMPool reads device count from annotation") pool := &hsmv1alpha1.HSMPool{} Eventually(func() bool { _ = k8sClient.Get(ctx, types.NamespacedName{ Name: hsmPoolName, Namespace: hsmPoolNamespace, }, pool) // Find the pod with annotation for _, report := range pool.Status.ReportingPods { if report.PodName == podWithAnnotation.Name { return report.DevicesFound == 2 // Should read 2 devices from annotation } } return false }).Should(BeTrue()) // Verify the specific report details var annotatedReport *hsmv1alpha1.PodReport for i := range pool.Status.ReportingPods { report := &pool.Status.ReportingPods[i] if report.PodName == podWithAnnotation.Name { annotatedReport = report break } } Expect(annotatedReport).NotTo(BeNil()) Expect(annotatedReport.DevicesFound).To(Equal(int32(2))) // 2 devices from annotation Expect(annotatedReport.DiscoveryStatus).To(Equal("completed")) // Status from annotation Expect(annotatedReport.NodeName).To(Equal("node-1")) // Verify that TotalDevices and AvailableDevices are correctly aggregated Expect(pool.Status.TotalDevices).To(Equal(int32(2))) // Should aggregate 2 devices total Expect(pool.Status.AvailableDevices).To(Equal(int32(2))) // Both devices are available }) }) })