···5656- **HSMPool-based Agent Discovery**: API and controllers query HSMPool to find all agent instances for a device type
5757- **Multiple Agent Instances**: Each physical device gets its own agent pod (e.g., `hsm-agent-pico-hsm-0`, `hsm-agent-pico-hsm-1`)
5858- **Multi-Agent Operations**: API operations (list, write, delete) work across all agents when mirroring is enabled
5959-- **Automatic Synchronization**: HSMSyncReconciler handles conflict detection and resolution between devices
5959+- **Automatic Synchronization**: HSMMirrorReconciler handles conflict detection and resolution between devices
60606161**gRPC Communication Architecture:**
6262- Protocol definition in `api/proto/hsm/v1/hsm.proto` with 10 HSM operations
···7171├── HSMSecretReconciler - HSM to K8s Secret sync
7272├── HSMPoolReconciler - Aggregates discovery reports from pod annotations
7373├── HSMPoolAgentReconciler - Deploys agents when pools are ready
7474-├── HSMSyncReconciler - Multi-device HSM synchronization and conflict resolution
7474+├── HSMMirrorReconciler - Multi-device HSM mirroring and conflict resolution
7575└── DiscoveryDaemonSetReconciler - Manages discovery DaemonSet lifecycle
76767777Discovery Controllers:
+131-73
internal/agent/deployment.go
···133133 return m
134134}
135135136136+// deviceWork represents work to be done for a specific device
137137+type deviceWork struct {
138138+ device hsmv1alpha1.DiscoveredDevice
139139+ agentName string
140140+ agentKey string
141141+ index int
142142+}
143143+136144// EnsureAgent ensures HSM agents are deployed for all available devices in the pool
137145func (m *Manager) EnsureAgent(ctx context.Context, hsmDevice *hsmv1alpha1.HSMDevice, hsmSecret *hsmv1alpha1.HSMSecret) error {
138146 // Get the HSMPool for this device to find all aggregated devices
···144152 }, &hsmPool); err != nil {
145153 return fmt.Errorf("failed to get HSMPool %s: %w", poolName, err)
146154 }
147147- m.mu.Lock()
148148- defer m.mu.Unlock()
149155150150- // Ensure agents for each available aggregated device in the pool
156156+ // Pre-collect available devices to process (no mutex needed)
157157+ workItems := make([]deviceWork, 0, len(hsmPool.Status.AggregatedDevices))
151158 for i, aggregatedDevice := range hsmPool.Status.AggregatedDevices {
152159 if !aggregatedDevice.Available {
153160 continue
154161 }
162162+ workItems = append(workItems, deviceWork{
163163+ device: aggregatedDevice,
164164+ agentName: fmt.Sprintf("%s-%s-%d", AgentNamePrefix, hsmDevice.Name, i),
165165+ agentKey: fmt.Sprintf("%s-%d", hsmDevice.Name, i),
166166+ index: i,
167167+ })
168168+ }
155169156156- // Create unique agent name for each physical device
157157- agentName := fmt.Sprintf("%s-%s-%d", AgentNamePrefix, hsmDevice.Name, i)
158158- agentKey := fmt.Sprintf("%s-%d", hsmDevice.Name, i) // Unique key for tracking
170170+ if len(workItems) == 0 {
171171+ return nil // No available devices to process
172172+ }
159173160160- // Check if we already have this specific agent tracked
161161- if agentInfo, exists := m.activeAgents[agentKey]; exists {
162162- // Agent exists in tracking, verify it's still healthy
163163- if m.isAgentHealthy(ctx, agentInfo) {
164164- continue // Agent is healthy, skip
165165- }
166166- // Agent unhealthy, remove from tracking and recreate
167167- m.removeAgentFromTracking(agentKey)
168168- }
174174+ // Process devices in parallel
175175+ var wg sync.WaitGroup
176176+ errChan := make(chan error, len(workItems))
169177170170- // Check if deployment exists in Kubernetes
171171- var deployment appsv1.Deployment
172172- err := m.Get(ctx, types.NamespacedName{
173173- Name: agentName,
174174- Namespace: hsmDevice.Namespace,
175175- }, &deployment)
178178+ for _, work := range workItems {
179179+ wg.Add(1)
180180+ go func(w deviceWork) {
181181+ defer wg.Done()
176182177177- if err == nil {
178178- // Agent exists, but check if it needs updating (image version, device/node configuration)
179179- needsUpdate, err := m.agentNeedsUpdate(ctx, &deployment, hsmDevice)
180180- if err != nil {
181181- return fmt.Errorf("failed to check if agent deployment %s needs update: %w", agentName, err)
183183+ // Mutex-protected check and update of activeAgents
184184+ m.mu.Lock()
185185+ needsDeployment := false
186186+ if agentInfo, exists := m.activeAgents[w.agentKey]; exists {
187187+ if !m.isAgentHealthy(ctx, agentInfo) {
188188+ m.removeAgentFromTracking(w.agentKey)
189189+ needsDeployment = true
190190+ }
191191+ } else {
192192+ needsDeployment = true
182193 }
194194+ m.mu.Unlock()
183195184184- // Also check device-specific configuration
185185- if !needsUpdate {
186186- needsUpdate = m.deploymentNeedsUpdateForDevice(&deployment, &aggregatedDevice)
196196+ // Skip if agent is healthy and tracked
197197+ if !needsDeployment {
198198+ return
187199 }
188200189189- if needsUpdate {
190190- // Delete existing deployment to trigger recreation
191191- if err := m.Delete(ctx, &deployment); err != nil {
192192- return fmt.Errorf("failed to delete outdated agent deployment %s: %w", agentName, err)
193193- }
194194- } else {
195195- // Agent exists and is correct - wait for it and track it
196196- podIPs, err := m.waitForAgentReady(ctx, agentName, hsmDevice.Namespace)
197197- if err != nil {
198198- return fmt.Errorf("failed waiting for existing agent pods %s: %w", agentName, err)
199199- }
201201+ // Deploy agent for this device (Kubernetes API calls - no mutex needed)
202202+ if err := m.deployAgentForDevice(ctx, w, hsmDevice); err != nil {
203203+ errChan <- fmt.Errorf("failed to deploy agent %s: %w", w.agentName, err)
204204+ }
205205+ }(work)
206206+ }
200207201201- // Track the existing agent
202202- agentInfo := &AgentInfo{
203203- DeviceName: agentKey,
204204- PodIPs: podIPs,
205205- CreatedAt: time.Now(),
206206- LastHealthCheck: time.Now(),
207207- Status: AgentStatusReady,
208208- AgentName: agentName,
209209- Namespace: hsmDevice.Namespace,
210210- }
208208+ // Wait for all goroutines to complete
209209+ wg.Wait()
210210+ close(errChan)
211211212212- m.activeAgents[agentKey] = agentInfo
213213- continue
214214- }
215215- } else if !errors.IsNotFound(err) {
216216- return fmt.Errorf("failed to check agent deployment %s: %w", agentName, err)
217217- }
212212+ // Collect any errors
213213+ deploymentErrors := make([]error, 0, len(workItems))
214214+ for err := range errChan {
215215+ deploymentErrors = append(deploymentErrors, err)
216216+ }
218217219219- // Create agent deployment for this specific device
220220- if err := m.createAgentDeployment(ctx, hsmDevice, nil, hsmDevice.Namespace, &aggregatedDevice, agentName); err != nil {
221221- return fmt.Errorf("failed to create agent deployment %s: %w", agentName, err)
222222- }
218218+ if len(deploymentErrors) > 0 {
219219+ return fmt.Errorf("agent deployment errors: %v", deploymentErrors)
220220+ }
221221+222222+ return nil
223223+}
224224+225225+// deployAgentForDevice handles the deployment logic for a single device
226226+func (m *Manager) deployAgentForDevice(ctx context.Context, work deviceWork, hsmDevice *hsmv1alpha1.HSMDevice) error {
227227+ // Check if deployment exists in Kubernetes
228228+ var deployment appsv1.Deployment
229229+ err := m.Get(ctx, types.NamespacedName{
230230+ Name: work.agentName,
231231+ Namespace: hsmDevice.Namespace,
232232+ }, &deployment)
223233224224- // Wait for agent pods to be ready and get their IPs
225225- podIPs, err := m.waitForAgentReady(ctx, agentName, hsmDevice.Namespace)
234234+ if err == nil {
235235+ // Agent exists, but check if it needs updating (image version, device/node configuration)
236236+ needsUpdate, err := m.agentNeedsUpdate(ctx, &deployment, hsmDevice)
226237 if err != nil {
227227- return fmt.Errorf("failed waiting for agent pods %s: %w", agentName, err)
238238+ return fmt.Errorf("failed to check if agent deployment %s needs update: %w", work.agentName, err)
228239 }
229240230230- // Track the new agent
231231- agentInfo := &AgentInfo{
232232- DeviceName: agentKey,
233233- PodIPs: podIPs,
234234- CreatedAt: time.Now(),
235235- LastHealthCheck: time.Now(),
236236- Status: AgentStatusReady,
237237- AgentName: agentName,
238238- Namespace: hsmDevice.Namespace,
241241+ // Also check device-specific configuration
242242+ if !needsUpdate {
243243+ needsUpdate = m.deploymentNeedsUpdateForDevice(&deployment, &work.device)
239244 }
240245241241- m.activeAgents[agentKey] = agentInfo
246246+ if needsUpdate {
247247+ // Delete existing deployment to trigger recreation
248248+ if err := m.Delete(ctx, &deployment); err != nil {
249249+ return fmt.Errorf("failed to delete outdated agent deployment %s: %w", work.agentName, err)
250250+ }
251251+ } else {
252252+ // Agent exists and is correct - wait for it and track it
253253+ podIPs, err := m.waitForAgentReady(ctx, work.agentName, hsmDevice.Namespace)
254254+ if err != nil {
255255+ return fmt.Errorf("failed waiting for existing agent pods %s: %w", work.agentName, err)
256256+ }
257257+258258+ // Track the existing agent (mutex-protected)
259259+ m.mu.Lock()
260260+ agentInfo := &AgentInfo{
261261+ DeviceName: work.agentKey,
262262+ PodIPs: podIPs,
263263+ CreatedAt: time.Now(),
264264+ LastHealthCheck: time.Now(),
265265+ Status: AgentStatusReady,
266266+ AgentName: work.agentName,
267267+ Namespace: hsmDevice.Namespace,
268268+ }
269269+ m.activeAgents[work.agentKey] = agentInfo
270270+ m.mu.Unlock()
271271+ return nil
272272+ }
273273+ } else if !errors.IsNotFound(err) {
274274+ return fmt.Errorf("failed to check agent deployment %s: %w", work.agentName, err)
242275 }
276276+277277+ // Create agent deployment for this specific device
278278+ if err := m.createAgentDeployment(ctx, hsmDevice, nil, hsmDevice.Namespace, &work.device, work.agentName); err != nil {
279279+ return fmt.Errorf("failed to create agent deployment %s: %w", work.agentName, err)
280280+ }
281281+282282+ // Wait for agent pods to be ready and get their IPs
283283+ podIPs, err := m.waitForAgentReady(ctx, work.agentName, hsmDevice.Namespace)
284284+ if err != nil {
285285+ return fmt.Errorf("failed waiting for agent pods %s: %w", work.agentName, err)
286286+ }
287287+288288+ // Track the new agent (mutex-protected)
289289+ m.mu.Lock()
290290+ agentInfo := &AgentInfo{
291291+ DeviceName: work.agentKey,
292292+ PodIPs: podIPs,
293293+ CreatedAt: time.Now(),
294294+ LastHealthCheck: time.Now(),
295295+ Status: AgentStatusReady,
296296+ AgentName: work.agentName,
297297+ Namespace: hsmDevice.Namespace,
298298+ }
299299+ m.activeAgents[work.agentKey] = agentInfo
300300+ m.mu.Unlock()
243301244302 return nil
245303}
-126
internal/controller/hsmmirror_controller.go
···11-/*
22-Copyright 2025.
33-44-Licensed under the Apache License, Version 2.0 (the "License");
55-you may not use this file except in compliance with the License.
66-You may obtain a copy of the License at
77-88- http://www.apache.org/licenses/LICENSE-2.0
99-1010-Unless required by applicable law or agreed to in writing, software
1111-distributed under the License is distributed on an "AS IS" BASIS,
1212-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1313-See the License for the specific language governing permissions and
1414-limitations under the License.
1515-*/
1616-1717-package controller
1818-1919-import (
2020- "context"
2121- "time"
2222-2323- "k8s.io/apimachinery/pkg/runtime"
2424- ctrl "sigs.k8s.io/controller-runtime"
2525- "sigs.k8s.io/controller-runtime/pkg/client"
2626- "sigs.k8s.io/controller-runtime/pkg/log"
2727-2828- hsmv1alpha1 "github.com/evanjarrett/hsm-secrets-operator/api/v1alpha1"
2929- "github.com/evanjarrett/hsm-secrets-operator/internal/agent"
3030- "github.com/evanjarrett/hsm-secrets-operator/internal/mirror"
3131-)
3232-3333-// HSMMirrorReconciler handles multi-device HSM mirroring and conflict resolution
3434-type HSMMirrorReconciler struct {
3535- client.Client
3636- Scheme *runtime.Scheme
3737- MirrorManager *mirror.MirrorManager
3838-3939- // MirrorInterval controls how often to perform mirror checks (default: 30 seconds)
4040- MirrorInterval time.Duration
4141-}
4242-4343-// NewHSMMirrorReconciler creates a new HSM mirror reconciler
4444-func NewHSMMirrorReconciler(k8sClient client.Client, scheme *runtime.Scheme, agentManager *agent.Manager, operatorNamespace string) *HSMMirrorReconciler {
4545- logger := ctrl.Log.WithName("hsm-mirror-controller")
4646- mirrorManager := mirror.NewMirrorManager(k8sClient, agentManager, logger, operatorNamespace)
4747-4848- return &HSMMirrorReconciler{
4949- Client: k8sClient,
5050- Scheme: scheme,
5151- MirrorManager: mirrorManager,
5252- MirrorInterval: 30 * time.Second, // Default mirror interval
5353- }
5454-}
5555-5656-// +kubebuilder:rbac:groups=hsm.j5t.io,resources=hsmsecrets,verbs=get;list;watch;update;patch
5757-// +kubebuilder:rbac:groups=hsm.j5t.io,resources=hsmsecrets/status,verbs=get;update;patch
5858-// +kubebuilder:rbac:groups=hsm.j5t.io,resources=hsmpools,verbs=get;list;watch
5959-// +kubebuilder:rbac:groups=hsm.j5t.io,resources=hsmdevices,verbs=get;list;watch
6060-6161-// Reconcile performs HSM device mirroring and conflict resolution
6262-func (r *HSMMirrorReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
6363- logger := log.FromContext(ctx)
6464-6565- // Fetch the HSMSecret instance
6666- var hsmSecret hsmv1alpha1.HSMSecret
6767- if err := r.Get(ctx, req.NamespacedName, &hsmSecret); err != nil {
6868- return ctrl.Result{}, client.IgnoreNotFound(err)
6969- }
7070-7171- // Skip sync if AutoSync is disabled
7272- if !hsmSecret.Spec.AutoSync {
7373- logger.V(1).Info("AutoSync disabled, skipping sync", "secret", hsmSecret.Name)
7474- return ctrl.Result{}, nil
7575- }
7676-7777- logger.Info("Starting multi-device HSM mirror", "secret", hsmSecret.Name)
7878-7979- // Perform the mirror operation
8080- result, err := r.MirrorManager.MirrorSecret(ctx, &hsmSecret)
8181- if err != nil {
8282- logger.Error(err, "Failed to perform HSM mirror", "secret", hsmSecret.Name)
8383-8484- // Update status with error
8585- hsmSecret.Status.SyncStatus = hsmv1alpha1.SyncStatusError
8686- hsmSecret.Status.LastError = err.Error()
8787- if updateErr := r.Status().Update(ctx, &hsmSecret); updateErr != nil {
8888- logger.Error(updateErr, "Failed to update HSMSecret status")
8989- }
9090-9191- // Retry sooner on error
9292- return ctrl.Result{RequeueAfter: r.MirrorInterval / 2}, nil
9393- }
9494-9595- // Update HSMSecret status with mirror results
9696- if err := r.MirrorManager.UpdateHSMSecretStatus(ctx, &hsmSecret, result); err != nil {
9797- logger.Error(err, "Failed to update HSMSecret status", "secret", hsmSecret.Name)
9898- return ctrl.Result{RequeueAfter: r.MirrorInterval / 2}, err
9999- }
100100-101101- // Log mirror results
102102- logger.Info("Per-secret HSM mirror completed",
103103- "secret", hsmSecret.Name,
104104- "success", result.Success,
105105- "secretsProcessed", result.SecretsProcessed,
106106- "secretsUpdated", result.SecretsUpdated,
107107- "secretsCreated", result.SecretsCreated,
108108- "metadataRestored", result.MetadataRestored,
109109- "errors", len(result.Errors))
110110-111111- // Calculate next mirror interval based on HSMSecret spec
112112- mirrorInterval := r.MirrorInterval
113113- if hsmSecret.Spec.SyncInterval > 0 {
114114- mirrorInterval = time.Duration(hsmSecret.Spec.SyncInterval) * time.Second
115115- }
116116-117117- return ctrl.Result{RequeueAfter: mirrorInterval}, nil
118118-}
119119-120120-// SetupWithManager sets up the controller with the Manager.
121121-func (r *HSMMirrorReconciler) SetupWithManager(mgr ctrl.Manager) error {
122122- return ctrl.NewControllerManagedBy(mgr).
123123- For(&hsmv1alpha1.HSMSecret{}).
124124- Named("hsmmirror").
125125- Complete(r)
126126-}
+63-131
internal/mirror/manager.go
···2424 "time"
25252626 "github.com/go-logr/logr"
2727- metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2827 "sigs.k8s.io/controller-runtime/pkg/client"
29283029 hsmv1alpha1 "github.com/evanjarrett/hsm-secrets-operator/api/v1alpha1"
···110109 MirrorTypeCreate // Create missing secret
111110 MirrorTypeRestoreMetadata // Add metadata to existing secret
112111)
113113-114114-// MirrorSecret performs per-secret mirroring across all HSM devices
115115-func (mm *MirrorManager) MirrorSecret(ctx context.Context, hsmSecret *hsmv1alpha1.HSMSecret) (*MirrorResult, error) {
116116- logger := mm.logger.WithValues("secret", hsmSecret.Name, "namespace", hsmSecret.Namespace)
117117- secretPath := hsmSecret.Name
118118-119119- // Get all available HSM devices from HSMPools in the operator namespace
120120- devices, err := mm.getAvailableDevices(ctx, mm.operatorNamespace)
121121- if err != nil {
122122- return nil, fmt.Errorf("failed to get available devices: %w", err)
123123- }
124124-125125- if len(devices) == 0 {
126126- return &MirrorResult{
127127- Success: false,
128128- SecretResults: make(map[string]SecretMirrorResult),
129129- Errors: []string{"no HSM devices available"},
130130- }, fmt.Errorf("no HSM devices available")
131131- }
132132-133133- logger.Info("Starting per-secret mirror", "devices", len(devices), "secretPath", secretPath)
134134-135135- // Build inventory of all secrets across all devices
136136- inventory, err := mm.buildSecretInventory(ctx, []string{secretPath}, devices, mm.operatorNamespace, logger)
137137- if err != nil {
138138- return &MirrorResult{
139139- Success: false,
140140- SecretResults: make(map[string]SecretMirrorResult),
141141- Errors: []string{fmt.Sprintf("failed to build secret inventory: %v", err)},
142142- }, fmt.Errorf("failed to build secret inventory: %w", err)
143143- }
144144-145145- // Create mirror plan for the single secret
146146- mirrorPlans := mm.createMirrorPlans(inventory, logger)
147147-148148- // Execute mirror operations
149149- result := mm.executeMirrorPlans(ctx, mirrorPlans, mm.operatorNamespace, logger)
150150-151151- logger.Info("Per-secret sync completed",
152152- "secretsProcessed", result.SecretsProcessed,
153153- "secretsUpdated", result.SecretsUpdated,
154154- "secretsCreated", result.SecretsCreated,
155155- "metadataRestored", result.MetadataRestored,
156156- "errors", len(result.Errors))
157157-158158- return result, nil
159159-}
160112161113// buildSecretInventory builds a comprehensive inventory of secrets across all devices
162114func (mm *MirrorManager) buildSecretInventory(ctx context.Context, secretPaths []string, devices []string, operatorNamespace string, logger logr.Logger) (map[string]*SecretInventory, error) {
···686638 return devices, nil
687639}
688640689689-// UpdateHSMSecretStatus updates the HSMSecret status with mirror results
690690-func (mm *MirrorManager) UpdateHSMSecretStatus(ctx context.Context, hsmSecret *hsmv1alpha1.HSMSecret, result *MirrorResult) error {
691691- now := metav1.NewTime(time.Now())
641641+// MirrorAllSecrets performs device-scoped mirroring of ALL secrets across HSM devices
642642+// This discovers all secrets on any device and ensures they exist on all other devices
643643+func (mm *MirrorManager) MirrorAllSecrets(ctx context.Context) (*MirrorResult, error) {
644644+ logger := mm.logger.WithValues("operation", "device-scoped-mirror")
645645+ logger.Info("Starting device-scoped mirroring of all HSM secrets")
646646+647647+ // Get all available HSM devices
648648+ devices, err := mm.getAvailableDevices(ctx, mm.operatorNamespace)
649649+ if err != nil {
650650+ return nil, fmt.Errorf("failed to get available devices: %w", err)
651651+ }
692652693693- // Update overall status
694694- if result.Success {
695695- hsmSecret.Status.SyncStatus = hsmv1alpha1.SyncStatusInSync
696696- hsmSecret.Status.LastSyncTime = &now
697697- if len(result.Errors) == 0 {
698698- hsmSecret.Status.LastError = ""
699699- } else {
700700- // Partial success - some errors occurred
701701- hsmSecret.Status.LastError = fmt.Sprintf("Partial sync completed with %d errors", len(result.Errors))
702702- }
703703- } else {
704704- hsmSecret.Status.SyncStatus = hsmv1alpha1.SyncStatusError
705705- if len(result.Errors) > 0 {
706706- hsmSecret.Status.LastError = result.Errors[0] // Show first error
707707- } else {
708708- hsmSecret.Status.LastError = "Failed to sync secret"
709709- }
653653+ if len(devices) <= 1 {
654654+ logger.Info("Skipping mirroring - need at least 2 devices", "devices", len(devices))
655655+ return &MirrorResult{Success: true, SecretsProcessed: 0}, nil
710656 }
711657712712- // Update secret-level sync information
713713- secretResult, hasSecretResult := result.SecretResults[hsmSecret.Name]
714714- if hasSecretResult {
715715- // Calculate checksum from the source device if sync was successful
716716- if secretResult.Success {
717717- // Read the current data to calculate checksum
718718- if devices, err := mm.getAvailableDevices(ctx, mm.operatorNamespace); err == nil && len(devices) > 0 {
719719- if data, _, err := mm.readSecretWithMetadata(ctx, devices[0], hsmSecret.Name, mm.operatorNamespace, mm.logger); err == nil {
720720- hsmSecret.Status.SecretChecksum = mm.calculateChecksum(data)
721721- }
722722- }
723723- }
658658+ logger.Info("Starting mirroring across devices", "devices", devices, "deviceCount", len(devices))
659659+660660+ // Discover all secrets across all devices
661661+ allSecretPaths := mm.discoverAllSecrets(ctx, devices, mm.operatorNamespace, logger)
724662725725- // Set primary device information if available
726726- if secretResult.SourceDevice != "" {
727727- hsmSecret.Status.PrimaryDevice = secretResult.SourceDevice
728728- }
663663+ if len(allSecretPaths) == 0 {
664664+ logger.Info("No secrets found to mirror")
665665+ return &MirrorResult{Success: true, SecretsProcessed: 0}, nil
729666 }
730667731731- // Update device-specific sync status based on available devices
732732- devices, err := mm.getAvailableDevices(ctx, mm.operatorNamespace)
733733- if err == nil {
734734- hsmSecret.Status.DeviceSyncStatus = make([]hsmv1alpha1.HSMDeviceSync, 0, len(devices))
668668+ logger.Info("Discovered secrets for mirroring", "secretCount", len(allSecretPaths), "secrets", allSecretPaths)
735669736736- for _, deviceName := range devices {
737737- deviceSync := hsmv1alpha1.HSMDeviceSync{
738738- DeviceName: deviceName,
739739- LastSyncTime: &now,
740740- Checksum: "",
741741- Online: true, // Assume online if in available devices list
742742- Version: 0,
743743- }
670670+ // Build inventory for all discovered secrets
671671+ inventory, err := mm.buildSecretInventory(ctx, allSecretPaths, devices, mm.operatorNamespace, logger)
672672+ if err != nil {
673673+ return nil, fmt.Errorf("failed to build secret inventory: %w", err)
674674+ }
744675745745- // Check if this device was involved in sync operations
746746- if hasSecretResult {
747747- if secretResult.SourceDevice == deviceName {
748748- deviceSync.Status = hsmv1alpha1.SyncStatusInSync
749749- deviceSync.Version = secretResult.SourceVersion
750750- } else {
751751- // Check if this device was a target
752752- isTarget := false
753753- for _, target := range secretResult.TargetDevices {
754754- if target == deviceName {
755755- isTarget = true
756756- break
757757- }
758758- }
676676+ // Create mirror plans
677677+ plans := mm.createMirrorPlans(inventory, logger)
678678+ logger.Info("Created mirror plans", "planCount", len(plans))
679679+680680+ // Execute mirror plans
681681+ return mm.executeMirrorPlans(ctx, plans, mm.operatorNamespace, logger), nil
682682+}
683683+684684+// discoverAllSecrets discovers all secrets present on any HSM device
685685+func (mm *MirrorManager) discoverAllSecrets(ctx context.Context, devices []string, operatorNamespace string, logger logr.Logger) []string {
686686+ secretPaths := make(map[string]bool)
687687+688688+ for _, deviceName := range devices {
689689+ deviceLogger := logger.WithValues("device", deviceName)
690690+691691+ hsmClient, err := mm.agentManager.CreateSingleGRPCClient(ctx, deviceName, operatorNamespace, deviceLogger)
692692+ if err != nil {
693693+ deviceLogger.Info("Failed to connect to device for discovery, skipping", "error", err)
694694+ continue
695695+ }
759696760760- if isTarget {
761761- if secretResult.Success {
762762- deviceSync.Status = hsmv1alpha1.SyncStatusInSync
763763- deviceSync.Version = secretResult.SourceVersion
764764- } else {
765765- deviceSync.Status = hsmv1alpha1.SyncStatusError
766766- if secretResult.Error != nil {
767767- deviceSync.LastError = secretResult.Error.Error()
768768- }
769769- }
770770- } else {
771771- // Device wasn't involved in sync - assume in sync
772772- deviceSync.Status = hsmv1alpha1.SyncStatusInSync
773773- }
774774- }
775775- } else {
776776- // No secret result - assume in sync
777777- deviceSync.Status = hsmv1alpha1.SyncStatusInSync
778778- }
697697+ secrets, err := hsmClient.ListSecrets(ctx, "")
698698+ if err != nil {
699699+ deviceLogger.Info("Failed to list secrets on device, skipping", "error", err)
700700+ continue
701701+ }
779702780780- hsmSecret.Status.DeviceSyncStatus = append(hsmSecret.Status.DeviceSyncStatus, deviceSync)
703703+ deviceLogger.Info("Discovered secrets on device", "secretCount", len(secrets))
704704+ for _, secretPath := range secrets {
705705+ secretPaths[secretPath] = true
781706 }
782707 }
783708784784- return mm.client.Status().Update(ctx, hsmSecret)
709709+ // Convert to sorted slice
710710+ result := make([]string, 0, len(secretPaths))
711711+ for secretPath := range secretPaths {
712712+ result = append(result, secretPath)
713713+ }
714714+ sort.Strings(result)
715715+716716+ return result
785717}
786718787719// calculateChecksum calculates SHA256 checksum of secret data