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.

event driven discovery

+767 -786
+5 -3
Dockerfile
··· 4 4 ARG TARGETOS 5 5 ARG TARGETARCH 6 6 7 - # Install build dependencies for PKCS#11 7 + # Install build dependencies for PKCS#11 and USB event monitoring 8 8 RUN apk add --no-cache \ 9 9 gcc \ 10 - g++ 10 + g++ \ 11 + eudev-dev \ 12 + linux-headers 11 13 12 14 # Return to workspace for Go builds 13 15 WORKDIR /workspace ··· 28 30 RUN CGO_ENABLED=1 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o hsm-operator cmd/hsm-operator/main.go 29 31 30 32 FROM alpine:3.22 31 - RUN apk add --no-cache opensc-dev ccid pcsc-lite openssl libtool libusb ca-certificates 33 + RUN apk add --no-cache opensc-dev ccid pcsc-lite openssl libtool libusb ca-certificates eudev 32 34 33 35 WORKDIR / 34 36 COPY --from=builder /workspace/hsm-operator .
+3 -3
Makefile
··· 3 3 # To re-generate a bundle for another specific version without changing the standard setup, you can: 4 4 # - use the VERSION as arg of the bundle target (e.g make bundle VERSION=0.0.2) 5 5 # - use environment variables to overwrite this value (e.g export VERSION=0.0.2) 6 - VERSION ?= 0.6.3 6 + VERSION ?= 0.6.4 7 7 8 8 # CHANNELS define the bundle channels used in the bundle. 9 9 # Add a new line here if you would like to change its default config. (E.g CHANNELS = "candidate,fast,stable") ··· 136 136 137 137 .PHONY: manifests 138 138 manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. 139 - $(CONTROLLER_GEN) rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases 139 + CGO_ENABLED=0 $(CONTROLLER_GEN) rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases 140 140 141 141 .PHONY: helm-sync 142 142 helm-sync: manifests ## Sync generated CRDs from config/ to helm/crds/ ··· 147 147 148 148 .PHONY: generate 149 149 generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. 150 - $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..." 150 + CGO_ENABLED=0 $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..." 151 151 152 152 .PHONY: fmt 153 153 fmt: ## Run go fmt against code.
+2
go.mod
··· 7 7 github.com/go-logr/logr v1.4.2 8 8 github.com/go-playground/validator/v10 v10.27.0 9 9 github.com/golang-jwt/jwt/v5 v5.3.0 10 + github.com/jochenvg/go-udev v0.0.0-20240801134859-b65ed646224b 10 11 github.com/miekg/pkcs11 v1.1.1 11 12 github.com/onsi/ginkgo/v2 v2.22.0 12 13 github.com/onsi/gomega v1.36.1 ··· 58 59 github.com/google/uuid v1.6.0 // indirect 59 60 github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 // indirect 60 61 github.com/inconshreveable/mousetrap v1.1.0 // indirect 62 + github.com/jkeiser/iter v0.0.0-20200628201005-c8aa0ae784d1 // indirect 61 63 github.com/josharian/intern v1.0.0 // indirect 62 64 github.com/json-iterator/go v1.1.12 // indirect 63 65 github.com/klauspost/cpuid/v2 v2.2.7 // indirect
+4
go.sum
··· 94 94 github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0/go.mod h1:qztMSjm835F2bXf+5HKAPIS5qsmQDqZna/PgVt4rWtI= 95 95 github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 96 96 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 97 + github.com/jkeiser/iter v0.0.0-20200628201005-c8aa0ae784d1 h1:smvLGU3obGU5kny71BtE/ibR0wIXRUiRFDmSn0Nxz1E= 98 + github.com/jkeiser/iter v0.0.0-20200628201005-c8aa0ae784d1/go.mod h1:fP/NdyhRVOv09PLRbVXrSqHhrfQypdZwgE2L4h2U5C8= 99 + github.com/jochenvg/go-udev v0.0.0-20240801134859-b65ed646224b h1:Pzf7tldbCVqwl3NnOnTamEWdh/rL41fsoYCn2HdHgRA= 100 + github.com/jochenvg/go-udev v0.0.0-20240801134859-b65ed646224b/go.mod h1:IBDUGq30U56w969YNPomhMbRje1GrhUsCh7tHdwgLXA= 97 101 github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 98 102 github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 99 103 github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
+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.6.3 6 - appVersion: v0.6.3 5 + version: 0.6.4 6 + appVersion: v0.6.4 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:
+40 -3
internal/controller/discovery_daemonset_controller.go
··· 23 23 24 24 appsv1 "k8s.io/api/apps/v1" 25 25 corev1 "k8s.io/api/core/v1" 26 + "k8s.io/apimachinery/pkg/api/equality" 26 27 "k8s.io/apimachinery/pkg/api/errors" 27 28 "k8s.io/apimachinery/pkg/api/resource" 28 29 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ··· 243 244 MountPath: "/sys", 244 245 ReadOnly: true, 245 246 }, 247 + { 248 + Name: "run-udev", 249 + MountPath: "/run/udev", 250 + ReadOnly: true, 251 + }, 246 252 }, 247 253 Resources: corev1.ResourceRequirements{ 248 254 Requests: corev1.ResourceList{ ··· 279 285 UpdateStrategy: appsv1.DaemonSetUpdateStrategy{ 280 286 Type: appsv1.RollingUpdateDaemonSetStrategyType, 281 287 RollingUpdate: &appsv1.RollingUpdateDaemonSet{ 282 - MaxUnavailable: &intstr.IntOrString{Type: intstr.String, StrVal: "100%"}, 288 + MaxUnavailable: &intstr.IntOrString{Type: intstr.String, StrVal: "50%"}, 283 289 }, 284 290 }, 285 291 }, ··· 305 311 return ctrl.Result{}, fmt.Errorf("failed to get discovery DaemonSet: %w", err) 306 312 } 307 313 308 - // Update existing DaemonSet if needed 314 + // Check if DaemonSet needs updating using Kubernetes-aware equality 315 + specChanged := !equality.Semantic.DeepEqual(existing.Spec, desired.Spec) 316 + labelsChanged := !equality.Semantic.DeepEqual(existing.Labels, desired.Labels) 317 + 318 + if !specChanged && !labelsChanged { 319 + logger.V(1).Info("Discovery DaemonSet spec unchanged, skipping update", 320 + "device", hsmDevice.Name, 321 + "daemonset", daemonSetName) 322 + return ctrl.Result{}, nil 323 + } 324 + 325 + // Update existing DaemonSet 309 326 existing.Spec = desired.Spec 310 327 existing.Labels = desired.Labels 311 328 312 - logger.Info("Updating discovery DaemonSet", "device", hsmDevice.Name, "daemonset", daemonSetName) 329 + logger.Info("Updating discovery DaemonSet", 330 + "device", hsmDevice.Name, 331 + "daemonset", daemonSetName, 332 + "specChanged", specChanged, 333 + "labelsChanged", labelsChanged) 334 + 313 335 if err := r.Update(ctx, existing); err != nil { 314 336 return ctrl.Result{}, fmt.Errorf("failed to update discovery DaemonSet: %w", err) 315 337 } ··· 450 472 EmptyDir: &corev1.EmptyDirVolumeSource{}, 451 473 }, 452 474 }, 475 + corev1.Volume{ 476 + Name: "run-udev", 477 + VolumeSource: corev1.VolumeSource{ 478 + EmptyDir: &corev1.EmptyDirVolumeSource{}, 479 + }, 480 + }, 453 481 ) 454 482 } else { 455 483 // In production, add hostPath volumes for device discovery ··· 468 496 VolumeSource: corev1.VolumeSource{ 469 497 HostPath: &corev1.HostPathVolumeSource{ 470 498 Path: "/sys", 499 + Type: &[]corev1.HostPathType{corev1.HostPathDirectory}[0], 500 + }, 501 + }, 502 + }, 503 + corev1.Volume{ 504 + Name: "run-udev", 505 + VolumeSource: corev1.VolumeSource{ 506 + HostPath: &corev1.HostPathVolumeSource{ 507 + Path: "/run/udev", 471 508 Type: &[]corev1.HostPathType{corev1.HostPathDirectory}[0], 472 509 }, 473 510 },
+9 -4
internal/controller/discovery_daemonset_controller_test.go
··· 159 159 Expect(podNameEnv).NotTo(BeNil()) 160 160 Expect(podNameEnv.ValueFrom.FieldRef.FieldPath).To(Equal("metadata.name")) 161 161 162 - // Check volumes 163 - Expect(podSpec.Volumes).To(HaveLen(2)) 164 - var devVolume, sysVolume *corev1.Volume 162 + // Check volumes - now includes /run/udev for event-driven USB discovery 163 + Expect(podSpec.Volumes).To(HaveLen(3)) 164 + var devVolume, sysVolume, udevVolume *corev1.Volume 165 165 for i := range podSpec.Volumes { 166 166 switch podSpec.Volumes[i].Name { 167 167 case "dev": 168 168 devVolume = &podSpec.Volumes[i] 169 169 case "sys": 170 170 sysVolume = &podSpec.Volumes[i] 171 + case "run-udev": 172 + udevVolume = &podSpec.Volumes[i] 171 173 } 172 174 } 173 175 174 176 Expect(devVolume).NotTo(BeNil()) 175 177 Expect(sysVolume).NotTo(BeNil()) 178 + Expect(udevVolume).NotTo(BeNil()) 176 179 177 180 // In CI environments, volumes use EmptyDir; in production they use HostPath 178 181 if devVolume.HostPath != nil { 179 182 // Production environment - expect HostPath volumes 180 183 Expect(devVolume.HostPath.Path).To(Equal("/dev")) 181 184 Expect(sysVolume.HostPath.Path).To(Equal("/sys")) 185 + Expect(udevVolume.HostPath.Path).To(Equal("/run/udev")) 182 186 } else { 183 187 // CI/test environment - expect EmptyDir volumes 184 188 Expect(devVolume.EmptyDir).NotTo(BeNil()) 185 189 Expect(sysVolume.EmptyDir).NotTo(BeNil()) 190 + Expect(udevVolume.EmptyDir).NotTo(BeNil()) 186 191 } 187 192 188 193 // Check node selector from HSMDevice ··· 190 195 191 196 // Check update strategy 192 197 Expect(daemonSet.Spec.UpdateStrategy.Type).To(Equal(appsv1.RollingUpdateDaemonSetStrategyType)) 193 - Expect(daemonSet.Spec.UpdateStrategy.RollingUpdate.MaxUnavailable).To(Equal(&intstr.IntOrString{Type: intstr.String, StrVal: "100%"})) 198 + Expect(daemonSet.Spec.UpdateStrategy.RollingUpdate.MaxUnavailable).To(Equal(&intstr.IntOrString{Type: intstr.String, StrVal: "50%"})) 194 199 }) 195 200 196 201 It("Should update DaemonSet when HSMDevice is updated", func() {
+225 -426
internal/discovery/usb.go
··· 1 + //go:build cgo 2 + // +build cgo 3 + 1 4 /* 2 5 Copyright 2025. 3 6 ··· 17 20 package discovery 18 21 19 22 import ( 20 - "bufio" 21 23 "context" 22 24 "fmt" 23 - "io/fs" 24 - "os" 25 - "path/filepath" 26 - "regexp" 27 25 "strings" 26 + "time" 28 27 29 28 "github.com/go-logr/logr" 29 + "github.com/jochenvg/go-udev" 30 + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 30 31 ctrl "sigs.k8s.io/controller-runtime" 31 32 32 33 hsmv1alpha1 "github.com/evanjarrett/hsm-secrets-operator/api/v1alpha1" ··· 43 44 DeviceInfo map[string]string 44 45 } 45 46 46 - // USBDiscoverer handles USB device discovery 47 + // USBEvent represents a USB device event 48 + type USBEvent struct { 49 + Action string // "add" or "remove" 50 + Device USBDevice // The device that changed 51 + Timestamp time.Time 52 + HSMDeviceName string // Which HSMDevice spec this event relates to 53 + } 54 + 55 + // USBDiscoverer handles USB device discovery and monitoring 47 56 type USBDiscoverer struct { 48 57 logger logr.Logger 49 - // mutex sync.RWMutex // unused 50 - 51 - // Known USB paths to scan (support both container and host-mounted paths) 52 - usbSysPaths []string 53 - devicePaths []string 58 + udev *udev.Udev 54 59 55 - // Detection method preference: "lsusb", "sysfs", or "auto" 56 - detectionMethod string 60 + // Event monitoring 61 + monitor *udev.Monitor 62 + eventChannel chan USBEvent 63 + activeSpecs map[string]*hsmv1alpha1.USBDeviceSpec 57 64 } 58 65 59 66 // NewUSBDiscoverer creates a new USB device discoverer 60 67 func NewUSBDiscoverer() *USBDiscoverer { 61 - return NewUSBDiscovererWithMethod("auto") 62 - } 68 + logger := ctrl.Log.WithName("usb-discoverer") 63 69 64 - // NewUSBDiscovererWithMethod creates a new USB device discoverer with specific detection method 65 - func NewUSBDiscovererWithMethod(method string) *USBDiscoverer { 70 + udev := &udev.Udev{} 71 + 66 72 return &USBDiscoverer{ 67 - logger: ctrl.Log.WithName("usb-discoverer"), 68 - usbSysPaths: []string{ 69 - "/host/sys/bus/usb/devices", // Host-mounted path (for DaemonSet) 70 - "/sys/bus/usb/devices", // Container path (for regular deployment) 71 - "/host/sys/class/usbmisc", // Alternative host path 72 - "/sys/class/usbmisc", // Alternative container path 73 - }, 74 - devicePaths: []string{ 75 - "/host/dev", // Host-mounted path (for DaemonSet) 76 - "/dev", // Container path (for regular deployment) 77 - }, 78 - detectionMethod: method, 73 + logger: logger, 74 + udev: udev, 75 + eventChannel: make(chan USBEvent, 100), 76 + activeSpecs: make(map[string]*hsmv1alpha1.USBDeviceSpec), 79 77 } 80 78 } 81 79 80 + // NewUSBDiscovererWithMethod creates a new USB device discoverer (method parameter is ignored, kept for compatibility) 81 + func NewUSBDiscovererWithMethod(method string) *USBDiscoverer { 82 + return NewUSBDiscoverer() 83 + } 84 + 82 85 // DiscoverDevices finds USB devices matching the given specification 83 86 func (u *USBDiscoverer) DiscoverDevices(ctx context.Context, spec *hsmv1alpha1.USBDeviceSpec) ([]USBDevice, error) { 84 87 u.logger.V(1).Info("Starting USB device discovery", 85 88 "vendorId", spec.VendorID, 86 - "productId", spec.ProductID, 87 - "method", u.detectionMethod) 89 + "productId", spec.ProductID) 88 90 89 - var devices []USBDevice 90 - 91 - // Choose detection method based on configuration 92 - u.logger.V(1).Info("Using detection method", "method", u.detectionMethod) 91 + // Create enumerate object 92 + enumerate := u.udev.NewEnumerate() 93 93 94 - switch u.detectionMethod { 95 - case "sysfs": 96 - // Use native sysfs reading (recommended method) 97 - u.logger.V(1).Info("Forcing native sysfs detection method") 98 - sysfsDevices, err := u.scanUSBWithSysfs() 99 - if err != nil { 100 - return nil, fmt.Errorf("native sysfs detection method failed: %w", err) 101 - } 102 - devices = u.filterDevices(sysfsDevices, spec, "native-sysfs") 103 - 104 - case "legacy": 105 - // Legacy method removed - use native sysfs instead 106 - u.logger.V(1).Info("Legacy method deprecated, using native sysfs") 107 - sysfsDevices, err := u.scanUSBWithSysfs() 108 - if err != nil { 109 - return nil, fmt.Errorf("native sysfs detection method failed: %w", err) 110 - } 111 - devices = u.filterDevices(sysfsDevices, spec, "native-sysfs") 112 - 113 - case "auto": 114 - fallthrough 115 - default: 116 - // Always use native sysfs - no fallback needed 117 - u.logger.V(1).Info("Auto-detection: using native sysfs") 118 - sysfsDevices, err := u.scanUSBWithSysfs() 119 - if err != nil { 120 - return nil, fmt.Errorf("native sysfs detection method failed: %w", err) 121 - } 122 - u.logger.V(1).Info("Using native sysfs for USB discovery", "foundDevices", len(sysfsDevices)) 123 - devices = u.filterDevices(sysfsDevices, spec, "native-sysfs") 94 + // Filter for USB devices only 95 + if err := enumerate.AddMatchSubsystem("usb"); err != nil { 96 + return nil, fmt.Errorf("failed to add subsystem filter: %w", err) 124 97 } 125 98 126 - u.logger.Info("USB device discovery completed", 127 - "matchedDevices", len(devices), 128 - "method", u.detectionMethod) 129 - 130 - return devices, nil 131 - } 132 - 133 - // filterDevices filters USB devices based on specification 134 - func (u *USBDiscoverer) filterDevices(allDevices []USBDevice, spec *hsmv1alpha1.USBDeviceSpec, method string) []USBDevice { 135 - devices := make([]USBDevice, 0) 136 - 137 - for _, device := range allDevices { 138 - if u.matchesSpec(device, spec) { 139 - u.logger.V(1).Info("Found matching USB device", 140 - "vendorId", device.VendorID, 141 - "productId", device.ProductID, 142 - "serial", device.SerialNumber, 143 - "path", device.DevicePath, 144 - "method", method) 145 - devices = append(devices, device) 146 - } 99 + if err := enumerate.AddMatchProperty("DEVTYPE", "usb_device"); err != nil { 100 + return nil, fmt.Errorf("failed to add device type filter: %w", err) 147 101 } 148 102 149 - return devices 150 - } 151 - 152 - // DiscoverByPath finds devices using path-based discovery 153 - func (u *USBDiscoverer) DiscoverByPath(ctx context.Context, pathSpec *hsmv1alpha1.DevicePathSpec) ([]USBDevice, error) { 154 - u.logger.V(1).Info("Starting path-based device discovery", "path", pathSpec.Path) 155 - 156 - devices := make([]USBDevice, 0) 157 - 158 - // Handle glob patterns 159 - matches, err := filepath.Glob(pathSpec.Path) 103 + // Get all USB devices 104 + devices, err := enumerate.Devices() 160 105 if err != nil { 161 - return nil, fmt.Errorf("failed to glob path %s: %w", pathSpec.Path, err) 106 + return nil, fmt.Errorf("failed to enumerate devices: %w", err) 162 107 } 163 108 164 - for _, match := range matches { 165 - // Check if the path exists and is accessible 166 - if _, err := os.Stat(match); err != nil { 167 - u.logger.V(2).Info("Skipping inaccessible device path", "path", match, "error", err) 168 - continue 169 - } 170 - 171 - // Create USB device entry for path-based discovery 172 - device := USBDevice{ 173 - DevicePath: match, 174 - DeviceInfo: map[string]string{ 175 - "discovery-method": "path", 176 - "permissions": pathSpec.Permissions, 177 - }, 178 - } 109 + u.logger.V(1).Info("Found USB devices", "totalDevices", len(devices)) 179 110 180 - // Try to get additional device info if possible 181 - if info := u.getDeviceInfoFromPath(match); info != nil { 182 - device.VendorID = info["vendor_id"] 183 - device.ProductID = info["product_id"] 184 - device.SerialNumber = info["serial"] 185 - device.Manufacturer = info["manufacturer"] 186 - device.Product = info["product"] 187 - for k, v := range info { 188 - device.DeviceInfo[k] = v 111 + // Convert and filter devices 112 + var matchingDevices []USBDevice 113 + for _, device := range devices { 114 + if usbDev := u.convertUdevDevice(device); usbDev != nil { 115 + if u.matchesSpec(*usbDev, spec) { 116 + u.logger.V(1).Info("Found matching USB device", 117 + "vendorId", usbDev.VendorID, 118 + "productId", usbDev.ProductID, 119 + "serial", usbDev.SerialNumber, 120 + "path", usbDev.DevicePath) 121 + matchingDevices = append(matchingDevices, *usbDev) 189 122 } 190 123 } 191 - 192 - devices = append(devices, device) 193 124 } 194 125 195 - u.logger.Info("Path-based device discovery completed", 196 - "matchedDevices", len(devices)) 126 + u.logger.Info("USB device discovery completed", 127 + "matchedDevices", len(matchingDevices)) 197 128 198 - return devices, nil 129 + return matchingDevices, nil 199 130 } 200 131 201 - // scanUSBWithSysfs uses /sys/bus/usb/devices directly (works without root like lsusb) 202 - func (u *USBDiscoverer) scanUSBWithSysfs() ([]USBDevice, error) { 203 - devices := make([]USBDevice, 0) 204 - 205 - // Try different USB sysfs paths (same as existing logic but focused on bus scan) 206 - var usbSysPath string 207 - for _, path := range u.usbSysPaths { 208 - if _, err := os.Stat(path); err == nil { 209 - usbSysPath = path 210 - u.logger.V(1).Info("Using USB sysfs path for native scan", "path", usbSysPath) 211 - break 212 - } 213 - } 214 - 215 - if usbSysPath == "" { 216 - return nil, fmt.Errorf("no USB sysfs path available, tried: %v", u.usbSysPaths) 132 + // convertUdevDevice converts a go-udev Device to our USBDevice format 133 + func (u *USBDiscoverer) convertUdevDevice(device *udev.Device) *USBDevice { 134 + // Only process actual USB devices, not interfaces 135 + if device.PropertyValue("DEVTYPE") != "usb_device" { 136 + return nil 217 137 } 218 138 219 - // Read the USB bus devices directly (like lsusb does) 220 - u.logger.V(1).Info("Scanning USB devices in sysfs", "path", usbSysPath) 139 + vendorID := device.PropertyValue("ID_VENDOR_ID") 140 + productID := device.PropertyValue("ID_MODEL_ID") 221 141 222 - err := filepath.WalkDir(usbSysPath, func(path string, d fs.DirEntry, err error) error { 223 - if err != nil { 224 - u.logger.V(2).Info("Error walking USB device", "path", path, "error", err) 225 - return err 226 - } 227 - 228 - // Skip the root directory itself 229 - if path == usbSysPath { 230 - return nil 231 - } 232 - 233 - // Check if this is a directory or symlink to a directory (handles sysfs symlinks) 234 - info, err := os.Stat(path) 235 - if err != nil { 236 - u.logger.V(2).Info("Cannot stat USB device path", "path", path, "error", err) 237 - return nil 238 - } 239 - 240 - if !info.IsDir() { 241 - return nil 242 - } 243 - 244 - // Check if this is a USB device directory (e.g., 1-1.2 or usb1) 245 - name := d.Name() 246 - // Match USB device patterns: N-N.N.N (port topology) or usbN (root hub) 247 - if !regexp.MustCompile(`^(\d+-[\d.]+|usb\d+)$`).MatchString(name) { 248 - return nil 249 - } 250 - 251 - // Skip USB root hubs unless they're actual devices 252 - if strings.HasPrefix(name, "usb") { 253 - return nil 254 - } 255 - 256 - u.logger.V(2).Info("Found potential USB device directory", "name", name, "path", path) 257 - 258 - device := u.parseUSBDeviceFromSysfs(path) 259 - if device != nil { 260 - u.logger.V(1).Info("Successfully parsed USB device", 261 - "vendorId", device.VendorID, "productId", device.ProductID, 262 - "manufacturer", device.Manufacturer, "product", device.Product) 263 - devices = append(devices, *device) 264 - } 265 - 142 + // Skip devices without vendor/product IDs 143 + if vendorID == "" || productID == "" { 266 144 return nil 267 - }) 145 + } 268 146 269 - if err != nil { 270 - return nil, fmt.Errorf("failed to scan USB sysfs: %w", err) 147 + return &USBDevice{ 148 + VendorID: vendorID, 149 + ProductID: productID, 150 + SerialNumber: device.PropertyValue("ID_SERIAL_SHORT"), 151 + DevicePath: device.Devnode(), 152 + Manufacturer: device.PropertyValue("ID_VENDOR"), 153 + Product: device.PropertyValue("ID_MODEL"), 154 + DeviceInfo: device.Properties(), 271 155 } 272 - 273 - u.logger.V(1).Info("Native sysfs USB scan completed", "devicesFound", len(devices)) 274 - return devices, nil 275 156 } 276 157 277 - // findHSMDevicePath looks for HSM device paths using sysfs information 278 - func (u *USBDiscoverer) findHSMDevicePath(sysfsPath, vendorID, productID string) string { 279 - // Method 1: Extract bus/device numbers and construct /dev/bus/usb path 280 - if usbDevPath := u.findUSBDevicePath(sysfsPath); usbDevPath != "" { 281 - u.logger.V(2).Info("Found USB device path", "path", usbDevPath, "vendorId", vendorID, "productId", productID) 282 - return usbDevPath 158 + // matchesSpec checks if a USB device matches the given specification 159 + func (u *USBDiscoverer) matchesSpec(device USBDevice, spec *hsmv1alpha1.USBDeviceSpec) bool { 160 + // Check vendor ID 161 + if spec.VendorID != "" && !strings.EqualFold(device.VendorID, spec.VendorID) { 162 + return false 283 163 } 284 164 285 - // Method 2: Check for serial device nodes (ttyUSB, ttyACM) 286 - if serialPath := u.findSerialDevicePath(sysfsPath); serialPath != "" { 287 - u.logger.V(2).Info("Found serial device path", "path", serialPath, "vendorId", vendorID, "productId", productID) 288 - return serialPath 165 + // Check product ID 166 + if spec.ProductID != "" && !strings.EqualFold(device.ProductID, spec.ProductID) { 167 + return false 289 168 } 290 169 291 - // Method 3: Check for HID device nodes (hidraw) 292 - if hidPath := u.findHIDDevicePath(sysfsPath); hidPath != "" { 293 - u.logger.V(2).Info("Found HID device path", "path", hidPath, "vendorId", vendorID, "productId", productID) 294 - return hidPath 295 - } 296 - 297 - // Method 4: Fallback to common paths (legacy compatibility) 298 - if commonPath := u.findCommonDevicePath(vendorID, productID); commonPath != "" { 299 - u.logger.V(2).Info("Found common device path", "path", commonPath, "vendorId", vendorID, "productId", productID) 300 - return commonPath 170 + // Check serial number if specified 171 + if spec.SerialNumber != "" && device.SerialNumber != spec.SerialNumber { 172 + return false 301 173 } 302 174 303 - return "" 175 + return true 304 176 } 305 177 306 - // findUSBDevicePath constructs /dev/bus/usb path from sysfs path 307 - func (u *USBDiscoverer) findUSBDevicePath(sysfsPath string) string { 308 - // Extract bus and device numbers from sysfs path 309 - // sysfsPath looks like: /sys/bus/usb/devices/1-6 310 - // We need to read busnum and devnum files 311 - 312 - busNumPath := filepath.Join(sysfsPath, "busnum") 313 - devNumPath := filepath.Join(sysfsPath, "devnum") 314 - 315 - busNum, err1 := u.readSysfsFile(busNumPath) 316 - devNum, err2 := u.readSysfsFile(devNumPath) 317 - 318 - if err1 != nil || err2 != nil { 319 - u.logger.V(2).Info("Could not read bus/dev numbers", "sysfsPath", sysfsPath, "busErr", err1, "devErr", err2) 320 - return "" 178 + // GetWellKnownHSMSpecs returns USB specifications for well-known HSM devices 179 + func GetWellKnownHSMSpecs() map[hsmv1alpha1.HSMDeviceType]*hsmv1alpha1.USBDeviceSpec { 180 + return map[hsmv1alpha1.HSMDeviceType]*hsmv1alpha1.USBDeviceSpec{ 181 + hsmv1alpha1.HSMDeviceTypePicoHSM: { 182 + VendorID: "20a0", // Pico HSM vendor ID 183 + ProductID: "4230", // Pico HSM product ID 184 + }, 185 + hsmv1alpha1.HSMDeviceTypeSmartCardHSM: { 186 + VendorID: "04e6", // Example SmartCard-HSM vendor ID 187 + ProductID: "5816", // Example SmartCard-HSM product ID 188 + }, 321 189 } 190 + } 322 191 323 - busNum = strings.TrimSpace(busNum) 324 - devNum = strings.TrimSpace(devNum) 192 + // StartEventMonitoring begins real-time USB device event monitoring 193 + func (u *USBDiscoverer) StartEventMonitoring(ctx context.Context) error { 194 + u.logger.Info("Starting USB event monitoring") 325 195 326 - // Format as 3-digit numbers for /dev/bus/usb path 327 - if len(busNum) == 1 { 328 - busNum = "00" + busNum 329 - } else if len(busNum) == 2 { 330 - busNum = "0" + busNum 331 - } 196 + // Create monitor 197 + u.monitor = u.udev.NewMonitorFromNetlink("udev") 332 198 333 - if len(devNum) == 1 { 334 - devNum = "00" + devNum 335 - } else if len(devNum) == 2 { 336 - devNum = "0" + devNum 199 + // Add filters for USB devices 200 + if err := u.monitor.FilterAddMatchSubsystem("usb"); err != nil { 201 + return fmt.Errorf("failed to add subsystem filter to monitor: %w", err) 337 202 } 338 203 339 - // Construct device path 340 - usbDevPath := fmt.Sprintf("/dev/bus/usb/%s/%s", busNum, devNum) 341 - 342 - // Check both container and host-mounted paths 343 - if u.deviceExists(usbDevPath) { 344 - return usbDevPath 345 - } 204 + // Start monitoring goroutine 205 + go u.monitorLoop(ctx) 346 206 347 - return "" 207 + u.logger.Info("USB event monitoring started successfully") 208 + return nil 348 209 } 349 210 350 - // findSerialDevicePath looks for ttyUSB/ttyACM device nodes 351 - func (u *USBDiscoverer) findSerialDevicePath(sysfsPath string) string { 352 - // Look for tty subdirectories in sysfs 353 - ttyPattern := filepath.Join(sysfsPath, "*", "tty", "tty*") 354 - matches, err := filepath.Glob(ttyPattern) 211 + // monitorLoop handles udev events in a separate goroutine 212 + func (u *USBDiscoverer) monitorLoop(ctx context.Context) { 213 + defer close(u.eventChannel) 214 + 215 + deviceChan, errorChan, err := u.monitor.DeviceChan(ctx) 355 216 if err != nil { 356 - return "" 217 + u.logger.Error(err, "Failed to create device channel") 218 + return 357 219 } 358 220 359 - for _, match := range matches { 360 - devName := filepath.Base(match) 361 - devPath := "/dev/" + devName 362 - if u.deviceExists(devPath) { 363 - return devPath 364 - } 365 - } 221 + for { 222 + select { 223 + case <-ctx.Done(): 224 + u.logger.Info("USB event monitoring stopped") 225 + return 366 226 367 - return "" 368 - } 227 + case device, ok := <-deviceChan: 228 + if !ok { 229 + u.logger.Info("USB device channel closed") 230 + return 231 + } 232 + u.handleDeviceEvent(device) 369 233 370 - // findHIDDevicePath looks for hidraw device nodes 371 - func (u *USBDiscoverer) findHIDDevicePath(sysfsPath string) string { 372 - // Look for hidraw subdirectories in sysfs 373 - hidPattern := filepath.Join(sysfsPath, "*", "hidraw", "hidraw*") 374 - matches, err := filepath.Glob(hidPattern) 375 - if err != nil { 376 - return "" 377 - } 378 - 379 - for _, match := range matches { 380 - devName := filepath.Base(match) 381 - devPath := "/dev/" + devName 382 - if u.deviceExists(devPath) { 383 - return devPath 234 + case err, ok := <-errorChan: 235 + if !ok { 236 + u.logger.Info("USB error channel closed") 237 + return 238 + } 239 + u.logger.Error(err, "USB monitoring error") 384 240 } 385 241 } 386 - 387 - return "" 388 242 } 389 243 390 - // findCommonDevicePath checks common device paths (legacy fallback) 391 - func (u *USBDiscoverer) findCommonDevicePath(vendorID, _ string) string { 392 - // Common HSM device paths that might be accessible without root 393 - commonPaths := []string{ 394 - "/dev/ttyUSB0", "/dev/ttyUSB1", "/dev/ttyUSB2", "/dev/ttyUSB3", 395 - "/dev/ttyACM0", "/dev/ttyACM1", "/dev/ttyACM2", "/dev/ttyACM3", 396 - "/dev/sc-hsm", "/dev/pkcs11", 397 - } 244 + // handleDeviceEvent processes a single USB device event 245 + func (u *USBDiscoverer) handleDeviceEvent(device *udev.Device) { 246 + action := device.Action() 398 247 399 - // For specific HSM vendors, try known paths 400 - knownHSMPaths := map[string][]string{ 401 - "20a0": {"/dev/ttyUSB0", "/dev/sc-hsm"}, // Pico HSM 402 - "04e6": {"/dev/ttyACM0"}, // SmartCard-HSM 248 + // Only process add/remove events 249 + if action != "add" && action != "remove" { 250 + return 403 251 } 404 252 405 - if paths, exists := knownHSMPaths[vendorID]; exists { 406 - commonPaths = append(paths, commonPaths...) 253 + // Convert to USBDevice 254 + usbDev := u.convertUdevDevice(device) 255 + if usbDev == nil { 256 + return 407 257 } 408 258 409 - for _, path := range commonPaths { 410 - if u.deviceExists(path) { 411 - return path 412 - } 413 - } 259 + u.logger.V(2).Info("Received USB event", 260 + "action", action, 261 + "vendor", usbDev.VendorID, 262 + "product", usbDev.ProductID, 263 + "serial", usbDev.SerialNumber) 414 264 415 - return "" 416 - } 417 - 418 - // deviceExists checks if a device path exists, trying both container and host-mounted paths 419 - func (u *USBDiscoverer) deviceExists(devicePath string) bool { 420 - // Try both container and host-mounted paths 421 - paths := []string{ 422 - devicePath, // /dev/ttyUSB0 or /dev/bus/usb/001/002 423 - "/host" + devicePath, // /host/dev/ttyUSB0 or /host/dev/bus/usb/001/002 424 - } 265 + // Check which active specs match this device 266 + for hsmDeviceName, spec := range u.activeSpecs { 267 + if u.matchesSpec(*usbDev, spec) { 268 + event := USBEvent{ 269 + Action: action, 270 + Device: *usbDev, 271 + Timestamp: time.Now(), 272 + HSMDeviceName: hsmDeviceName, 273 + } 425 274 426 - for _, path := range paths { 427 - if _, err := os.Stat(path); err == nil { 428 - return true 275 + select { 276 + case u.eventChannel <- event: 277 + u.logger.V(1).Info("Sent USB event", 278 + "action", action, 279 + "device", hsmDeviceName, 280 + "vendor", usbDev.VendorID, 281 + "product", usbDev.ProductID, 282 + "serial", usbDev.SerialNumber) 283 + default: 284 + u.logger.Error(nil, "USB event channel full, dropping event", "action", action) 285 + } 429 286 } 430 287 } 431 - return false 432 288 } 433 289 434 - // parseUSBDeviceFromSysfs parses USB device information directly from sysfs (native lsusb equivalent) 435 - func (u *USBDiscoverer) parseUSBDeviceFromSysfs(devicePath string) *USBDevice { 436 - device := &USBDevice{ 437 - DeviceInfo: make(map[string]string), 438 - } 290 + // AddSpecForMonitoring registers an HSMDevice spec for event monitoring 291 + func (u *USBDiscoverer) AddSpecForMonitoring(hsmDeviceName string, spec *hsmv1alpha1.USBDeviceSpec) { 292 + u.activeSpecs[hsmDeviceName] = spec 293 + u.logger.V(2).Info("Added USB spec for monitoring", 294 + "device", hsmDeviceName, 295 + "vendor", spec.VendorID, 296 + "product", spec.ProductID) 297 + } 439 298 440 - // Read vendor ID 441 - if vendorID, err := u.readSysfsFile(filepath.Join(devicePath, "idVendor")); err == nil { 442 - device.VendorID = strings.TrimSpace(vendorID) 443 - } 299 + // RemoveSpecFromMonitoring unregisters an HSMDevice spec from event monitoring 300 + func (u *USBDiscoverer) RemoveSpecFromMonitoring(hsmDeviceName string) { 301 + delete(u.activeSpecs, hsmDeviceName) 302 + u.logger.V(2).Info("Removed USB spec from monitoring", "device", hsmDeviceName) 303 + } 444 304 445 - // Read product ID 446 - if productID, err := u.readSysfsFile(filepath.Join(devicePath, "idProduct")); err == nil { 447 - device.ProductID = strings.TrimSpace(productID) 448 - } 449 - 450 - // Read serial number 451 - if serial, err := u.readSysfsFile(filepath.Join(devicePath, "serial")); err == nil { 452 - device.SerialNumber = strings.TrimSpace(serial) 453 - } 454 - 455 - // Read manufacturer 456 - if manufacturer, err := u.readSysfsFile(filepath.Join(devicePath, "manufacturer")); err == nil { 457 - device.Manufacturer = strings.TrimSpace(manufacturer) 458 - } 459 - 460 - // Read product name 461 - if product, err := u.readSysfsFile(filepath.Join(devicePath, "product")); err == nil { 462 - device.Product = strings.TrimSpace(product) 463 - } 464 - 465 - // Skip devices without vendor/product IDs 466 - if device.VendorID == "" || device.ProductID == "" { 467 - return nil 468 - } 469 - 470 - // Try to find associated device paths 471 - device.DevicePath = u.findHSMDevicePath(devicePath, device.VendorID, device.ProductID) 472 - 473 - // Add additional device info 474 - device.DeviceInfo["sysfs-path"] = devicePath 475 - device.DeviceInfo["discovery-method"] = "native-sysfs" 476 - 477 - // Extract bus and device numbers from path if possible 478 - deviceName := filepath.Base(devicePath) 479 - device.DeviceInfo["device-address"] = deviceName 480 - 481 - u.logger.V(2).Info("Parsed USB device from sysfs", 482 - "path", devicePath, 483 - "vendorId", device.VendorID, 484 - "productId", device.ProductID, 485 - "manufacturer", device.Manufacturer, 486 - "product", device.Product) 487 - 488 - return device 305 + // GetEventChannel returns the channel for receiving USB device events 306 + func (u *USBDiscoverer) GetEventChannel() <-chan USBEvent { 307 + return u.eventChannel 489 308 } 490 309 491 - // readSysfsFile reads a single-line file from sysfs 492 - func (u *USBDiscoverer) readSysfsFile(path string) (string, error) { 493 - file, err := os.Open(path) 494 - if err != nil { 495 - return "", err 496 - } 497 - defer func() { 498 - if err := file.Close(); err != nil { 499 - // Log the error but don't fail the operation 500 - u.logger.V(2).Info("Failed to close sysfs file", "path", path, "error", err) 501 - } 502 - }() 503 - 504 - scanner := bufio.NewScanner(file) 505 - if scanner.Scan() { 506 - return scanner.Text(), nil 310 + // StopEventMonitoring stops the USB event monitor 311 + func (u *USBDiscoverer) StopEventMonitoring() { 312 + u.logger.Info("Stopping USB event monitor") 313 + if u.monitor != nil { 314 + u.monitor = nil 507 315 } 316 + } 508 317 509 - return "", fmt.Errorf("empty file or read error") 318 + // IsEventMonitoringActive returns whether event monitoring is currently active 319 + func (u *USBDiscoverer) IsEventMonitoringActive() bool { 320 + return u.monitor != nil 510 321 } 511 322 512 - // getDeviceInfoFromPath attempts to get device info from a device path 513 - func (u *USBDiscoverer) getDeviceInfoFromPath(devicePath string) map[string]string { 514 - // This is a placeholder implementation 515 - // In a real implementation, you'd use udev or similar to get device info 516 - info := make(map[string]string) 323 + // ConvertToDiscoveredDevice converts a USBDevice to a DiscoveredDevice 324 + func ConvertToDiscoveredDevice(usbDevice USBDevice, nodeName string) hsmv1alpha1.DiscoveredDevice { 325 + device := hsmv1alpha1.DiscoveredDevice{ 326 + DevicePath: usbDevice.DevicePath, 327 + SerialNumber: usbDevice.SerialNumber, 328 + NodeName: nodeName, 329 + LastSeen: metav1.Now(), 330 + Available: true, 331 + DeviceInfo: map[string]string{ 332 + "vendor-id": usbDevice.VendorID, 333 + "product-id": usbDevice.ProductID, 334 + "manufacturer": usbDevice.Manufacturer, 335 + "product": usbDevice.Product, 336 + }, 337 + } 517 338 518 - // Try to determine device type from path 519 - if strings.Contains(devicePath, "ttyUSB") || strings.Contains(devicePath, "ttyACM") { 520 - info["device_type"] = "serial" 521 - } else if strings.Contains(devicePath, "sc-hsm") { 522 - info["device_type"] = "hsm" 523 - info["vendor_id"] = "20a0" // Example: Pico HSM vendor ID 524 - info["product_id"] = "4230" // Example: Pico HSM product ID 339 + // Add additional device info 340 + for k, v := range usbDevice.DeviceInfo { 341 + device.DeviceInfo[k] = v 525 342 } 526 343 527 - return info 344 + return device 528 345 } 529 346 530 - // matchesSpec checks if a USB device matches the given specification 531 - func (u *USBDiscoverer) matchesSpec(device USBDevice, spec *hsmv1alpha1.USBDeviceSpec) bool { 532 - // Check vendor ID 533 - if spec.VendorID != "" && !strings.EqualFold(device.VendorID, spec.VendorID) { 534 - return false 535 - } 536 - 537 - // Check product ID 538 - if spec.ProductID != "" && !strings.EqualFold(device.ProductID, spec.ProductID) { 539 - return false 347 + // IsSameDevice checks if two devices are the same (by serial number or device path) 348 + func IsSameDevice(device1, device2 hsmv1alpha1.DiscoveredDevice) bool { 349 + // Compare by serial number if both have one 350 + if device1.SerialNumber != "" && device2.SerialNumber != "" { 351 + return device1.SerialNumber == device2.SerialNumber 540 352 } 541 353 542 - // Check serial number if specified 543 - if spec.SerialNumber != "" && device.SerialNumber != spec.SerialNumber { 544 - return false 354 + // Fall back to device path comparison 355 + if device1.DevicePath != "" && device2.DevicePath != "" { 356 + return device1.DevicePath == device2.DevicePath 545 357 } 546 358 547 - return true 548 - } 549 - 550 - // GetWellKnownHSMSpecs returns USB specifications for well-known HSM devices 551 - func GetWellKnownHSMSpecs() map[hsmv1alpha1.HSMDeviceType]*hsmv1alpha1.USBDeviceSpec { 552 - return map[hsmv1alpha1.HSMDeviceType]*hsmv1alpha1.USBDeviceSpec{ 553 - hsmv1alpha1.HSMDeviceTypePicoHSM: { 554 - VendorID: "20a0", // Pico HSM vendor ID 555 - ProductID: "4230", // Pico HSM product ID 556 - }, 557 - hsmv1alpha1.HSMDeviceTypeSmartCardHSM: { 558 - VendorID: "04e6", // Example SmartCard-HSM vendor ID 559 - ProductID: "5816", // Example SmartCard-HSM product ID 560 - }, 561 - } 359 + // If we can't identify devices uniquely, assume they're different 360 + return false 562 361 }
+117
internal/discovery/usb_nocgo.go
··· 1 + //go:build !cgo 2 + // +build !cgo 3 + 4 + /* 5 + Copyright 2025. 6 + 7 + Licensed under the Apache License, Version 2.0 (the "License"); 8 + you may not use this file except in compliance with the License. 9 + You may obtain a copy of the License at 10 + 11 + http://www.apache.org/licenses/LICENSE-2.0 12 + 13 + Unless required by applicable law or agreed to in writing, software 14 + distributed under the License is distributed on an "AS IS" BASIS, 15 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 + See the License for the specific language governing permissions and 17 + limitations under the License. 18 + */ 19 + 20 + package discovery 21 + 22 + import ( 23 + "context" 24 + "fmt" 25 + "time" 26 + 27 + "github.com/go-logr/logr" 28 + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 29 + ctrl "sigs.k8s.io/controller-runtime" 30 + 31 + hsmv1alpha1 "github.com/evanjarrett/hsm-secrets-operator/api/v1alpha1" 32 + ) 33 + 34 + // USBDevice represents a discovered USB device 35 + type USBDevice struct { 36 + VendorID string 37 + ProductID string 38 + SerialNumber string 39 + DevicePath string 40 + Manufacturer string 41 + Product string 42 + DeviceInfo map[string]string 43 + } 44 + 45 + // USBEvent represents a USB device event 46 + type USBEvent struct { 47 + Action string // "add" or "remove" 48 + Device USBDevice // The device that changed 49 + Timestamp time.Time 50 + HSMDeviceName string // Which HSMDevice spec this event relates to 51 + } 52 + 53 + // USBDiscoverer handles USB device discovery and monitoring (no-CGO stub) 54 + type USBDiscoverer struct { 55 + logger logr.Logger 56 + eventChannel chan USBEvent 57 + activeSpecs map[string]*hsmv1alpha1.USBDeviceSpec 58 + } 59 + 60 + // NewUSBDiscoverer creates a new USB device discoverer (no-CGO stub) 61 + func NewUSBDiscoverer() *USBDiscoverer { 62 + logger := ctrl.Log.WithName("usb-discoverer-nocgo") 63 + 64 + return &USBDiscoverer{ 65 + logger: logger, 66 + eventChannel: make(chan USBEvent, 100), 67 + activeSpecs: make(map[string]*hsmv1alpha1.USBDeviceSpec), 68 + } 69 + } 70 + 71 + // NewUSBDiscovererWithMethod creates a new USB device discoverer (method parameter is ignored, kept for compatibility) 72 + func NewUSBDiscovererWithMethod(method string) *USBDiscoverer { 73 + return NewUSBDiscoverer() 74 + } 75 + 76 + // DiscoverDevices finds USB devices matching the given specification (no-CGO stub - returns empty) 77 + func (u *USBDiscoverer) DiscoverDevices(ctx context.Context, spec *hsmv1alpha1.USBDeviceSpec) ([]USBDevice, error) { 78 + u.logger.Info("USB device discovery not available (CGO disabled)", 79 + "vendorId", spec.VendorID, 80 + "productId", spec.ProductID) 81 + 82 + // Return empty slice - no devices found without udev 83 + return []USBDevice{}, nil 84 + } 85 + 86 + // StartEventMonitoring starts monitoring for USB device events (no-CGO stub - does nothing) 87 + func (u *USBDiscoverer) StartEventMonitoring(ctx context.Context) error { 88 + u.logger.Info("USB event monitoring not available (CGO disabled)") 89 + return nil 90 + } 91 + 92 + // AddSpecForMonitoring adds a device spec to monitor for events (no-CGO stub) 93 + func (u *USBDiscoverer) AddSpecForMonitoring(hsmDeviceName string, spec *hsmv1alpha1.USBDeviceSpec) { 94 + u.logger.V(1).Info("USB monitoring not available (CGO disabled)", "device", hsmDeviceName) 95 + u.activeSpecs[hsmDeviceName] = spec 96 + } 97 + 98 + // RemoveSpecFromMonitoring removes a device spec from event monitoring (no-CGO stub) 99 + func (u *USBDiscoverer) RemoveSpecFromMonitoring(hsmDeviceName string) { 100 + u.logger.V(1).Info("USB monitoring not available (CGO disabled)", "device", hsmDeviceName) 101 + delete(u.activeSpecs, hsmDeviceName) 102 + } 103 + 104 + // GetEventChannel returns the channel for USB events (no-CGO stub) 105 + func (u *USBDiscoverer) GetEventChannel() <-chan USBEvent { 106 + return u.eventChannel 107 + } 108 + 109 + // StopEventMonitoring stops USB device event monitoring (no-CGO stub) 110 + func (u *USBDiscoverer) StopEventMonitoring() { 111 + u.logger.Info("USB event monitoring not available (CGO disabled)") 112 + } 113 + 114 + // IsEventMonitoringActive returns whether event monitoring is active (no-CGO stub - always false) 115 + func (u *USBDiscoverer) IsEventMonitoringActive() bool { 116 + return false 117 + }
+43 -299
internal/discovery/usb_test.go
··· 17 17 package discovery 18 18 19 19 import ( 20 - "context" 21 - "os" 22 - "path/filepath" 23 20 "testing" 24 21 25 - "github.com/go-logr/logr" 26 22 "github.com/stretchr/testify/assert" 27 - "github.com/stretchr/testify/require" 28 23 29 24 hsmv1alpha1 "github.com/evanjarrett/hsm-secrets-operator/api/v1alpha1" 30 25 ) ··· 33 28 discoverer := NewUSBDiscoverer() 34 29 35 30 assert.NotNil(t, discoverer) 36 - assert.Equal(t, "auto", discoverer.detectionMethod) 37 - assert.NotEmpty(t, discoverer.usbSysPaths) 38 - assert.NotEmpty(t, discoverer.devicePaths) 31 + assert.NotNil(t, discoverer.udev) 32 + assert.NotNil(t, discoverer.eventChannel) 33 + assert.NotNil(t, discoverer.activeSpecs) 39 34 } 40 35 41 36 func TestNewUSBDiscovererWithMethod(t *testing.T) { ··· 53 48 t.Run(tt.name, func(t *testing.T) { 54 49 discoverer := NewUSBDiscovererWithMethod(tt.method) 55 50 assert.NotNil(t, discoverer) 56 - assert.Equal(t, tt.method, discoverer.detectionMethod) 51 + // Method parameter is ignored for compatibility, udev is always used 57 52 }) 58 53 } 59 54 } ··· 167 162 } 168 163 } 169 164 170 - func TestFilterDevices(t *testing.T) { 171 - discoverer := NewUSBDiscoverer() 172 - 173 - allDevices := []USBDevice{ 174 - {VendorID: "20a0", ProductID: "4230", SerialNumber: "TEST1"}, 175 - {VendorID: "20a0", ProductID: "4230", SerialNumber: "TEST2"}, 176 - {VendorID: "1234", ProductID: "5678", SerialNumber: "DIFF"}, 177 - {VendorID: "20a0", ProductID: "1111", SerialNumber: "DIFF2"}, 178 - } 179 - 180 - spec := &hsmv1alpha1.USBDeviceSpec{ 181 - VendorID: "20a0", 182 - ProductID: "4230", 183 - } 184 - 185 - filtered := discoverer.filterDevices(allDevices, spec, "test") 186 - 187 - assert.Len(t, filtered, 2) 188 - assert.Equal(t, "TEST1", filtered[0].SerialNumber) 189 - assert.Equal(t, "TEST2", filtered[1].SerialNumber) 190 - } 191 - 192 - func TestDiscoverByPath(t *testing.T) { 193 - discoverer := NewUSBDiscoverer() 194 - ctx := context.Background() 195 - 196 - // Create a temporary file to test with 197 - tempDir := t.TempDir() 198 - testDevice := filepath.Join(tempDir, "test-device") 199 - err := os.WriteFile(testDevice, []byte("test"), 0644) 200 - require.NoError(t, err) 201 - 202 - pathSpec := &hsmv1alpha1.DevicePathSpec{ 203 - Path: testDevice, 204 - Permissions: "rw", 205 - } 206 - 207 - devices, err := discoverer.DiscoverByPath(ctx, pathSpec) 208 - require.NoError(t, err) 209 - 210 - assert.Len(t, devices, 1) 211 - assert.Equal(t, testDevice, devices[0].DevicePath) 212 - assert.Equal(t, "path", devices[0].DeviceInfo["discovery-method"]) 213 - assert.Equal(t, "rw", devices[0].DeviceInfo["permissions"]) 214 - } 215 - 216 - func TestDiscoverByPathWithGlob(t *testing.T) { 217 - discoverer := NewUSBDiscoverer() 218 - ctx := context.Background() 219 - 220 - // Create temporary files to test with 221 - tempDir := t.TempDir() 222 - testDevice1 := filepath.Join(tempDir, "test-device1") 223 - testDevice2 := filepath.Join(tempDir, "test-device2") 224 - otherFile := filepath.Join(tempDir, "other-file") 225 - 226 - for _, file := range []string{testDevice1, testDevice2, otherFile} { 227 - err := os.WriteFile(file, []byte("test"), 0644) 228 - require.NoError(t, err) 229 - } 230 - 231 - // Use glob pattern 232 - pathSpec := &hsmv1alpha1.DevicePathSpec{ 233 - Path: filepath.Join(tempDir, "test-device*"), 234 - } 235 - 236 - devices, err := discoverer.DiscoverByPath(ctx, pathSpec) 237 - require.NoError(t, err) 238 - 239 - assert.Len(t, devices, 2) 240 - 241 - // Check that both test devices were found 242 - devicePaths := make([]string, len(devices)) 243 - for i, device := range devices { 244 - devicePaths[i] = device.DevicePath 245 - } 246 - assert.Contains(t, devicePaths, testDevice1) 247 - assert.Contains(t, devicePaths, testDevice2) 248 - } 249 - 250 - func TestDiscoverByPathNonexistent(t *testing.T) { 251 - discoverer := NewUSBDiscoverer() 252 - ctx := context.Background() 253 - 254 - pathSpec := &hsmv1alpha1.DevicePathSpec{ 255 - Path: "/nonexistent/device/path", 256 - } 257 - 258 - devices, err := discoverer.DiscoverByPath(ctx, pathSpec) 259 - require.NoError(t, err) 260 - 261 - // Should return empty list for nonexistent files 262 - assert.Empty(t, devices) 263 - } 264 - 265 - func TestDeviceExists(t *testing.T) { 266 - discoverer := NewUSBDiscoverer() 267 - 268 - // Create a temporary file 269 - tempDir := t.TempDir() 270 - tempFile := filepath.Join(tempDir, "test-file") 271 - err := os.WriteFile(tempFile, []byte("test"), 0644) 272 - require.NoError(t, err) 273 - 274 - // Test existing file 275 - assert.True(t, discoverer.deviceExists(tempFile)) 276 - 277 - // Test non-existent file 278 - assert.False(t, discoverer.deviceExists("/nonexistent/path")) 279 - } 280 - 281 - func TestGetDeviceInfoFromPath(t *testing.T) { 282 - discoverer := NewUSBDiscoverer() 283 - 284 - tests := []struct { 285 - name string 286 - path string 287 - expected map[string]string 288 - }{ 289 - { 290 - name: "ttyUSB device", 291 - path: "/dev/ttyUSB0", 292 - expected: map[string]string{ 293 - "device_type": "serial", 294 - }, 295 - }, 296 - { 297 - name: "ttyACM device", 298 - path: "/dev/ttyACM1", 299 - expected: map[string]string{ 300 - "device_type": "serial", 301 - }, 302 - }, 303 - { 304 - name: "sc-hsm device", 305 - path: "/dev/sc-hsm", 306 - expected: map[string]string{ 307 - "device_type": "hsm", 308 - "vendor_id": "20a0", 309 - "product_id": "4230", 310 - }, 311 - }, 312 - { 313 - name: "unknown device", 314 - path: "/dev/random", 315 - expected: map[string]string{}, 316 - }, 317 - } 318 - 319 - for _, tt := range tests { 320 - t.Run(tt.name, func(t *testing.T) { 321 - info := discoverer.getDeviceInfoFromPath(tt.path) 322 - assert.Equal(t, tt.expected, info) 323 - }) 324 - } 325 - } 326 - 327 165 func TestGetWellKnownHSMSpecs(t *testing.T) { 328 166 specs := GetWellKnownHSMSpecs() 329 167 ··· 342 180 assert.Equal(t, "5816", smartCardSpec.ProductID) 343 181 } 344 182 345 - func TestReadSysfsFile(t *testing.T) { 346 - discoverer := NewUSBDiscoverer() 183 + func TestUSBEvent(t *testing.T) { 184 + device := USBDevice{ 185 + VendorID: "20a0", 186 + ProductID: "4230", 187 + SerialNumber: "TEST123", 188 + DevicePath: "/dev/ttyUSB0", 189 + } 347 190 348 - // Create a temporary file with test content 349 - tempDir := t.TempDir() 350 - tempFile := filepath.Join(tempDir, "test-sysfs-file") 351 - testContent := "test-value\n" 352 - err := os.WriteFile(tempFile, []byte(testContent), 0644) 353 - require.NoError(t, err) 191 + event := USBEvent{ 192 + Action: "add", 193 + Device: device, 194 + HSMDeviceName: "test-hsm", 195 + } 354 196 355 - // Test reading existing file 356 - content, err := discoverer.readSysfsFile(tempFile) 357 - require.NoError(t, err) 358 - assert.Equal(t, "test-value", content) 359 - 360 - // Test reading non-existent file 361 - _, err = discoverer.readSysfsFile("/nonexistent/file") 362 - assert.Error(t, err) 363 - 364 - // Test reading empty file 365 - emptyFile := filepath.Join(tempDir, "empty-file") 366 - err = os.WriteFile(emptyFile, []byte(""), 0644) 367 - require.NoError(t, err) 368 - 369 - _, err = discoverer.readSysfsFile(emptyFile) 370 - assert.Error(t, err) 197 + assert.Equal(t, "add", event.Action) 198 + assert.Equal(t, device, event.Device) 199 + assert.Equal(t, "test-hsm", event.HSMDeviceName) 371 200 } 372 201 373 - func TestFindUSBDevicePath(t *testing.T) { 202 + func TestAddSpecForMonitoring(t *testing.T) { 374 203 discoverer := NewUSBDiscoverer() 375 204 376 - // Create temporary directory structure for testing 377 - tempDir := t.TempDir() 378 - 379 - // Create busnum and devnum files 380 - busnumFile := filepath.Join(tempDir, "busnum") 381 - devnumFile := filepath.Join(tempDir, "devnum") 205 + spec := &hsmv1alpha1.USBDeviceSpec{ 206 + VendorID: "20a0", 207 + ProductID: "4230", 208 + } 382 209 383 - err := os.WriteFile(busnumFile, []byte("001"), 0644) 384 - require.NoError(t, err) 385 - err = os.WriteFile(devnumFile, []byte("002"), 0644) 386 - require.NoError(t, err) 210 + // Test adding spec 211 + discoverer.AddSpecForMonitoring("test-device", spec) 212 + assert.Equal(t, spec, discoverer.activeSpecs["test-device"]) 387 213 388 - // Test finding USB device path 389 - path := discoverer.findUSBDevicePath(tempDir) 390 - // The path won't exist in test environment, so it should return empty 391 - // This is expected behavior since deviceExists() will return false 392 - assert.Empty(t, path) 214 + // Test removing spec 215 + discoverer.RemoveSpecFromMonitoring("test-device") 216 + _, exists := discoverer.activeSpecs["test-device"] 217 + assert.False(t, exists) 393 218 } 394 219 395 - func TestFindUSBDevicePathMissingFiles(t *testing.T) { 220 + func TestGetEventChannel(t *testing.T) { 396 221 discoverer := NewUSBDiscoverer() 397 - 398 - // Test with directory that doesn't have the required files 399 - tempDir := t.TempDir() 400 - path := discoverer.findUSBDevicePath(tempDir) 401 - assert.Empty(t, path) 222 + channel := discoverer.GetEventChannel() 223 + assert.NotNil(t, channel) 402 224 } 403 225 404 - func TestFindCommonDevicePath(t *testing.T) { 405 - // Create a test discoverer 406 - discoverer := &USBDiscoverer{ 407 - logger: logr.Discard(), 408 - } 409 - 410 - // Test with unknown vendor ID - this should always return empty since no known paths exist for it 411 - unknownPath := discoverer.findCommonDevicePath("unknown", "unknown") 412 - 413 - // The function checks actual filesystem paths, so we can't guarantee it returns empty 414 - // Instead, let's verify it returns a string (empty or path) 415 - assert.IsType(t, "", unknownPath) 416 - 417 - // Test that if a path is returned, it's one of the expected common paths 418 - if unknownPath != "" { 419 - expectedPaths := []string{ 420 - "/dev/ttyUSB0", "/dev/ttyUSB1", "/dev/ttyUSB2", "/dev/ttyUSB3", 421 - "/dev/ttyACM0", "/dev/ttyACM1", "/dev/ttyACM2", "/dev/ttyACM3", 422 - "/dev/sc-hsm", "/dev/pkcs11", 423 - } 424 - assert.Contains(t, expectedPaths, unknownPath) 425 - } 426 - } 427 - 428 - func TestParseUSBDeviceFromSysfs(t *testing.T) { 226 + func TestIsEventMonitoringActive(t *testing.T) { 429 227 discoverer := NewUSBDiscoverer() 430 228 431 - // Create temporary sysfs structure 432 - tempDir := t.TempDir() 229 + // Initially inactive 230 + assert.False(t, discoverer.IsEventMonitoringActive()) 433 231 434 - // Create device attribute files 435 - files := map[string]string{ 436 - "idVendor": "20a0", 437 - "idProduct": "4230", 438 - "serial": "TEST123", 439 - "manufacturer": "Test Manufacturer", 440 - "product": "Test Product", 441 - } 232 + // After setting monitor (simulated) 233 + discoverer.monitor = discoverer.udev.NewMonitorFromNetlink("udev") 234 + assert.True(t, discoverer.IsEventMonitoringActive()) 442 235 443 - for filename, content := range files { 444 - filePath := filepath.Join(tempDir, filename) 445 - err := os.WriteFile(filePath, []byte(content), 0644) 446 - require.NoError(t, err) 447 - } 448 - 449 - device := discoverer.parseUSBDeviceFromSysfs(tempDir) 450 - require.NotNil(t, device) 451 - 452 - assert.Equal(t, "20a0", device.VendorID) 453 - assert.Equal(t, "4230", device.ProductID) 454 - assert.Equal(t, "TEST123", device.SerialNumber) 455 - assert.Equal(t, "Test Manufacturer", device.Manufacturer) 456 - assert.Equal(t, "Test Product", device.Product) 457 - assert.Equal(t, tempDir, device.DeviceInfo["sysfs-path"]) 458 - assert.Equal(t, "native-sysfs", device.DeviceInfo["discovery-method"]) 459 - } 460 - 461 - func TestParseUSBDeviceFromSysfsMissingVendor(t *testing.T) { 462 - discoverer := NewUSBDiscoverer() 463 - 464 - // Create temporary directory with only product ID (missing vendor ID) 465 - tempDir := t.TempDir() 466 - productFile := filepath.Join(tempDir, "idProduct") 467 - err := os.WriteFile(productFile, []byte("4230"), 0644) 468 - require.NoError(t, err) 469 - 470 - device := discoverer.parseUSBDeviceFromSysfs(tempDir) 471 - assert.Nil(t, device) // Should return nil when vendor ID is missing 236 + // After stopping 237 + discoverer.StopEventMonitoring() 238 + assert.False(t, discoverer.IsEventMonitoringActive()) 472 239 } 473 240 474 241 // Benchmark tests ··· 489 256 discoverer.matchesSpec(device, spec) 490 257 } 491 258 } 492 - 493 - func BenchmarkFilterDevices(b *testing.B) { 494 - discoverer := NewUSBDiscoverer() 495 - 496 - // Create a large slice of devices 497 - devices := make([]USBDevice, 1000) 498 - for i := 0; i < 1000; i++ { 499 - devices[i] = USBDevice{ 500 - VendorID: "20a0", 501 - ProductID: "4230", 502 - } 503 - } 504 - 505 - spec := &hsmv1alpha1.USBDeviceSpec{ 506 - VendorID: "20a0", 507 - ProductID: "4230", 508 - } 509 - 510 - b.ResetTimer() 511 - for i := 0; i < b.N; i++ { 512 - discoverer.filterDevices(devices, spec, "test") 513 - } 514 - }
+317 -46
internal/modes/discovery/discovery.go
··· 139 139 podNamespace string 140 140 usbDiscoverer *discovery.USBDiscoverer 141 141 syncInterval time.Duration 142 + 143 + // Event-driven discovery support 144 + eventMonitoringActive bool 145 + deviceCache map[string][]hsmv1alpha1.DiscoveredDevice // Cache current device state per HSMDevice 146 + deviceLookup map[string]map[string]int // Fast lookup: HSMDevice -> (serial/path -> index) 142 147 } 143 148 144 - // Run starts the discovery loop 149 + // Run starts the discovery loop with event-driven monitoring 145 150 func (d *DiscoveryAgent) Run(ctx context.Context) error { 146 - ticker := time.NewTicker(d.syncInterval) 147 - defer ticker.Stop() 151 + // Initialize device cache 152 + d.deviceCache = make(map[string][]hsmv1alpha1.DiscoveredDevice) 153 + d.deviceLookup = make(map[string]map[string]int) 148 154 149 - // Run initial discovery 155 + // Step 1: Mandatory initial discovery to detect already-connected devices 156 + d.logger.Info("Performing initial device discovery scan") 150 157 if err := d.performDiscovery(ctx); err != nil { 151 - d.logger.Error(err, "Initial discovery failed") 158 + d.logger.Error(err, "Initial discovery failed - continuing with event monitoring") 159 + } else { 160 + d.logger.Info("Initial device discovery completed successfully") 152 161 } 153 162 154 - // Start periodic discovery 163 + // Step 2: Start event monitoring for real-time changes 164 + if err := d.startEventMonitoring(ctx); err != nil { 165 + d.logger.Error(err, "Failed to start USB event monitoring - falling back to polling only") 166 + d.eventMonitoringActive = false 167 + } else { 168 + d.logger.Info("USB event monitoring started successfully") 169 + d.eventMonitoringActive = true 170 + } 171 + 172 + // Step 3: Set up periodic reconciliation (reduced frequency - acts as safety net) 173 + reconcileTicker := time.NewTicker(d.syncInterval) 174 + defer reconcileTicker.Stop() 175 + 176 + // Step 4: Main event loop 177 + eventChan := d.usbDiscoverer.GetEventChannel() 178 + 155 179 for { 156 180 select { 157 181 case <-ctx.Done(): 158 182 d.logger.Info("Discovery agent shutting down") 183 + d.usbDiscoverer.StopEventMonitoring() 159 184 return nil 160 - case <-ticker.C: 161 - if err := d.performDiscovery(ctx); err != nil { 162 - d.logger.Error(err, "Discovery iteration failed") 185 + 186 + case event, ok := <-eventChan: 187 + if !ok { 188 + d.logger.Info("USB event channel closed, restarting event monitoring") 189 + if err := d.restartEventMonitoring(ctx); err != nil { 190 + d.logger.Error(err, "Failed to restart event monitoring") 191 + d.eventMonitoringActive = false 192 + } 193 + continue 194 + } 195 + d.handleUSBEvent(ctx, event) 196 + 197 + case <-reconcileTicker.C: 198 + // Periodic reconciliation - safety net and health check 199 + d.logger.V(1).Info("Performing periodic reconciliation") 200 + if err := d.reconcileDevices(ctx); err != nil { 201 + d.logger.Error(err, "Reconciliation failed") 163 202 } 164 203 } 165 204 } ··· 180 219 if err := d.processHSMDevice(ctx, &hsmDevice); err != nil { 181 220 d.logger.Error(err, "Failed to process HSMDevice", "device", hsmDevice.Name) 182 221 } 222 + 223 + // Register USB specs for event monitoring 224 + d.registerSpecForMonitoring(&hsmDevice) 183 225 } 184 226 185 227 return nil ··· 207 249 "node", d.nodeName, 208 250 "devicesFound", len(discoveredDevices)) 209 251 252 + // Update device cache 253 + d.updateDeviceCache(hsmDevice.Name, discoveredDevices) 254 + 210 255 // Update pod annotation with discovery results 211 256 if err := d.updatePodAnnotation(ctx, hsmDevice.Name, discoveredDevices); err != nil { 212 257 d.logger.Error(err, "Failed to update pod annotation", "device", hsmDevice.Name) ··· 226 271 // Perform discovery based on specification 227 272 if hsmDevice.Spec.Discovery != nil && hsmDevice.Spec.Discovery.USB != nil { 228 273 devices, err = d.discoverUSBDevices(ctx, hsmDevice) 229 - } else if hsmDevice.Spec.Discovery != nil && hsmDevice.Spec.Discovery.DevicePath != nil { 230 - devices, err = d.discoverPathDevices(ctx, hsmDevice) 231 274 } else if hsmDevice.Spec.Discovery != nil && hsmDevice.Spec.Discovery.AutoDiscovery { 232 275 devices, err = d.autoDiscoverDevices(ctx, hsmDevice) 233 276 } else { ··· 291 334 return devices, nil 292 335 } 293 336 294 - // discoverPathDevices discovers devices using path-based specifications 295 - func (d *DiscoveryAgent) discoverPathDevices( 296 - ctx context.Context, hsmDevice *hsmv1alpha1.HSMDevice, 297 - ) ([]hsmv1alpha1.DiscoveredDevice, error) { 298 - usbDevices, err := d.usbDiscoverer.DiscoverByPath(ctx, hsmDevice.Spec.Discovery.DevicePath) 299 - if err != nil { 300 - return nil, fmt.Errorf("path discovery failed: %w", err) 301 - } 302 - 303 - devices := make([]hsmv1alpha1.DiscoveredDevice, 0, len(usbDevices)) 304 - for _, usbDev := range usbDevices { 305 - device := hsmv1alpha1.DiscoveredDevice{ 306 - DevicePath: usbDev.DevicePath, 307 - SerialNumber: usbDev.SerialNumber, 308 - NodeName: d.nodeName, 309 - LastSeen: metav1.Now(), 310 - Available: true, 311 - DeviceInfo: map[string]string{ 312 - "discovery-type": "path", 313 - "path-pattern": hsmDevice.Spec.Discovery.DevicePath.Path, 314 - }, 315 - } 316 - 317 - // Add additional device info 318 - for k, v := range usbDev.DeviceInfo { 319 - device.DeviceInfo[k] = v 320 - } 321 - 322 - devices = append(devices, device) 323 - } 324 - 325 - d.logger.V(1).Info("Path device discovery completed", "devicesFound", len(devices)) 326 - return devices, nil 327 - } 328 - 329 337 // autoDiscoverDevices performs auto-discovery based on device type 330 338 func (d *DiscoveryAgent) autoDiscoverDevices( 331 339 ctx context.Context, hsmDevice *hsmv1alpha1.HSMDevice, ··· 425 433 426 434 return nil 427 435 } 436 + 437 + // startEventMonitoring initializes and starts USB event monitoring 438 + func (d *DiscoveryAgent) startEventMonitoring(ctx context.Context) error { 439 + // Start the USB event monitor 440 + if err := d.usbDiscoverer.StartEventMonitoring(ctx); err != nil { 441 + return fmt.Errorf("failed to start USB event monitoring: %w", err) 442 + } 443 + 444 + d.logger.Info("USB event monitoring initialized successfully") 445 + return nil 446 + } 447 + 448 + // restartEventMonitoring attempts to restart USB event monitoring 449 + func (d *DiscoveryAgent) restartEventMonitoring(ctx context.Context) error { 450 + d.logger.Info("Restarting USB event monitoring") 451 + 452 + // Stop current monitoring 453 + d.usbDiscoverer.StopEventMonitoring() 454 + 455 + // Re-register all specs 456 + if err := d.reregisterAllSpecs(); err != nil { 457 + return fmt.Errorf("failed to re-register specs: %w", err) 458 + } 459 + 460 + // Start monitoring again 461 + if err := d.usbDiscoverer.StartEventMonitoring(ctx); err != nil { 462 + return fmt.Errorf("failed to restart USB event monitoring: %w", err) 463 + } 464 + 465 + d.eventMonitoringActive = true 466 + d.logger.Info("USB event monitoring restarted successfully") 467 + return nil 468 + } 469 + 470 + // handleUSBEvent processes a USB device event (add/remove) 471 + func (d *DiscoveryAgent) handleUSBEvent(ctx context.Context, event discovery.USBEvent) { 472 + d.logger.V(1).Info("Processing USB event", 473 + "action", event.Action, 474 + "device", event.HSMDeviceName, 475 + "vendor", event.Device.VendorID, 476 + "product", event.Device.ProductID, 477 + "serial", event.Device.SerialNumber) 478 + 479 + switch event.Action { 480 + case "add": 481 + d.handleDeviceAdd(ctx, event) 482 + case "remove": 483 + d.handleDeviceRemove(ctx, event) 484 + default: 485 + d.logger.V(2).Info("Ignoring USB event with unknown action", "action", event.Action) 486 + } 487 + } 488 + 489 + // handleDeviceAdd handles USB device addition events 490 + func (d *DiscoveryAgent) handleDeviceAdd(ctx context.Context, event discovery.USBEvent) { 491 + // Convert USBDevice to DiscoveredDevice 492 + device := discovery.ConvertToDiscoveredDevice(event.Device, d.nodeName) 493 + 494 + // Update device cache 495 + currentDevices, exists := d.deviceCache[event.HSMDeviceName] 496 + if !exists { 497 + currentDevices = []hsmv1alpha1.DiscoveredDevice{} 498 + } 499 + 500 + // Check if device already exists using fast lookup 501 + if existingIdx := d.findDeviceIndex(event.HSMDeviceName, device); existingIdx != -1 { 502 + d.logger.V(2).Info("Device already in cache, updating last seen", 503 + "device", event.HSMDeviceName, 504 + "serial", device.SerialNumber) 505 + currentDevices[existingIdx].LastSeen = device.LastSeen 506 + d.updateDeviceCache(event.HSMDeviceName, currentDevices) 507 + if err := d.updatePodAnnotation(ctx, event.HSMDeviceName, currentDevices); err != nil { 508 + d.logger.Error(err, "Failed to update pod annotation after updating device last seen") 509 + } 510 + return 511 + } 512 + 513 + // Add new device 514 + currentDevices = append(currentDevices, device) 515 + d.updateDeviceCache(event.HSMDeviceName, currentDevices) 516 + 517 + d.logger.Info("Added device to cache", 518 + "device", event.HSMDeviceName, 519 + "serial", device.SerialNumber, 520 + "path", device.DevicePath, 521 + "totalDevices", len(currentDevices)) 522 + 523 + // Update pod annotation 524 + if err := d.updatePodAnnotation(ctx, event.HSMDeviceName, currentDevices); err != nil { 525 + d.logger.Error(err, "Failed to update pod annotation after device add") 526 + } 527 + } 528 + 529 + // handleDeviceRemove handles USB device removal events 530 + func (d *DiscoveryAgent) handleDeviceRemove(ctx context.Context, event discovery.USBEvent) { 531 + currentDevices, exists := d.deviceCache[event.HSMDeviceName] 532 + if !exists { 533 + d.logger.V(2).Info("No devices in cache for device removal", "device", event.HSMDeviceName) 534 + return 535 + } 536 + 537 + device := discovery.ConvertToDiscoveredDevice(event.Device, d.nodeName) 538 + 539 + // Find device using fast lookup 540 + deviceIdx := d.findDeviceIndex(event.HSMDeviceName, device) 541 + if deviceIdx == -1 { 542 + d.logger.V(2).Info("Device not found in cache for removal", 543 + "device", event.HSMDeviceName, 544 + "serial", device.SerialNumber) 545 + return 546 + } 547 + 548 + d.logger.Info("Removed device from cache", 549 + "device", event.HSMDeviceName, 550 + "serial", device.SerialNumber, 551 + "path", device.DevicePath) 552 + 553 + // Remove device efficiently by swapping with last element 554 + lastIdx := len(currentDevices) - 1 555 + if deviceIdx != lastIdx { 556 + currentDevices[deviceIdx] = currentDevices[lastIdx] 557 + } 558 + currentDevices = currentDevices[:lastIdx] 559 + 560 + d.updateDeviceCache(event.HSMDeviceName, currentDevices) 561 + 562 + d.logger.Info("Updated device cache after removal", 563 + "device", event.HSMDeviceName, 564 + "remainingDevices", len(currentDevices)) 565 + 566 + // Update pod annotation 567 + if err := d.updatePodAnnotation(ctx, event.HSMDeviceName, currentDevices); err != nil { 568 + d.logger.Error(err, "Failed to update pod annotation after device remove") 569 + } 570 + } 571 + 572 + // reconcileDevices performs periodic reconciliation as a safety net 573 + func (d *DiscoveryAgent) reconcileDevices(ctx context.Context) error { 574 + if !d.eventMonitoringActive { 575 + // If event monitoring is not active, fall back to regular discovery 576 + d.logger.V(1).Info("Event monitoring inactive, performing full discovery") 577 + return d.performDiscovery(ctx) 578 + } 579 + 580 + // Light reconciliation - just verify our cached state matches reality 581 + d.logger.V(2).Info("Performing light reconciliation check") 582 + 583 + // List all HSMDevice resources 584 + var hsmDeviceList hsmv1alpha1.HSMDeviceList 585 + if err := d.client.List(ctx, &hsmDeviceList); err != nil { 586 + return fmt.Errorf("failed to list HSMDevice resources during reconciliation: %w", err) 587 + } 588 + 589 + // Re-register specs in case any were missed 590 + for _, hsmDevice := range hsmDeviceList.Items { 591 + d.registerSpecForMonitoring(&hsmDevice) 592 + } 593 + 594 + return nil 595 + } 596 + 597 + // registerSpecForMonitoring registers an HSMDevice specification for event monitoring 598 + func (d *DiscoveryAgent) registerSpecForMonitoring(hsmDevice *hsmv1alpha1.HSMDevice) { 599 + // Skip if device should not be discovered on this node 600 + if !d.shouldDiscoverOnNode(hsmDevice) { 601 + return 602 + } 603 + 604 + var spec *hsmv1alpha1.USBDeviceSpec 605 + 606 + // Determine the USB spec to monitor 607 + if hsmDevice.Spec.Discovery != nil && hsmDevice.Spec.Discovery.USB != nil { 608 + spec = hsmDevice.Spec.Discovery.USB 609 + } else if hsmDevice.Spec.Discovery != nil && hsmDevice.Spec.Discovery.AutoDiscovery { 610 + // Use well-known specs for auto-discovery 611 + wellKnownSpecs := discovery.GetWellKnownHSMSpecs() 612 + if wellKnownSpec, exists := wellKnownSpecs[hsmDevice.Spec.DeviceType]; exists { 613 + spec = wellKnownSpec 614 + } 615 + } else { 616 + // Default to auto-discovery with well-known specs 617 + wellKnownSpecs := discovery.GetWellKnownHSMSpecs() 618 + if wellKnownSpec, exists := wellKnownSpecs[hsmDevice.Spec.DeviceType]; exists { 619 + spec = wellKnownSpec 620 + } 621 + } 622 + 623 + // Register the spec if we have one 624 + if spec != nil { 625 + d.usbDiscoverer.AddSpecForMonitoring(hsmDevice.Name, spec) 626 + d.logger.V(2).Info("Registered USB spec for event monitoring", 627 + "device", hsmDevice.Name, 628 + "vendor", spec.VendorID, 629 + "product", spec.ProductID) 630 + } 631 + } 632 + 633 + // reregisterAllSpecs re-registers all current HSMDevice specs for monitoring 634 + func (d *DiscoveryAgent) reregisterAllSpecs() error { 635 + ctx := context.Background() 636 + 637 + var hsmDeviceList hsmv1alpha1.HSMDeviceList 638 + if err := d.client.List(ctx, &hsmDeviceList); err != nil { 639 + return fmt.Errorf("failed to list HSMDevice resources: %w", err) 640 + } 641 + 642 + for _, hsmDevice := range hsmDeviceList.Items { 643 + d.registerSpecForMonitoring(&hsmDevice) 644 + } 645 + 646 + return nil 647 + } 648 + 649 + // updateDeviceCache updates both the device cache and lookup index 650 + func (d *DiscoveryAgent) updateDeviceCache(hsmDeviceName string, devices []hsmv1alpha1.DiscoveredDevice) { 651 + d.deviceCache[hsmDeviceName] = devices 652 + 653 + // Rebuild lookup index for this HSM device 654 + if d.deviceLookup[hsmDeviceName] == nil { 655 + d.deviceLookup[hsmDeviceName] = make(map[string]int) 656 + } else { 657 + // Clear existing entries 658 + for k := range d.deviceLookup[hsmDeviceName] { 659 + delete(d.deviceLookup[hsmDeviceName], k) 660 + } 661 + } 662 + 663 + // Build lookup index 664 + for i, device := range devices { 665 + // Index by serial number if available 666 + if device.SerialNumber != "" { 667 + d.deviceLookup[hsmDeviceName]["serial:"+device.SerialNumber] = i 668 + } 669 + // Index by device path if available 670 + if device.DevicePath != "" { 671 + d.deviceLookup[hsmDeviceName]["path:"+device.DevicePath] = i 672 + } 673 + } 674 + } 675 + 676 + // findDeviceIndex returns the index of a device in the cache, or -1 if not found 677 + func (d *DiscoveryAgent) findDeviceIndex(hsmDeviceName string, device hsmv1alpha1.DiscoveredDevice) int { 678 + lookupMap, exists := d.deviceLookup[hsmDeviceName] 679 + if !exists { 680 + return -1 681 + } 682 + 683 + // Try serial number lookup first 684 + if device.SerialNumber != "" { 685 + if idx, found := lookupMap["serial:"+device.SerialNumber]; found { 686 + return idx 687 + } 688 + } 689 + 690 + // Fall back to device path lookup 691 + if device.DevicePath != "" { 692 + if idx, found := lookupMap["path:"+device.DevicePath]; found { 693 + return idx 694 + } 695 + } 696 + 697 + return -1 698 + }