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.

clean up proxy_client to check all devices

+582 -389
-1
go.mod
··· 75 75 github.com/spf13/cobra v1.8.1 // indirect 76 76 github.com/spf13/pflag v1.0.5 // indirect 77 77 github.com/stoewer/go-strcase v1.3.0 // indirect 78 - github.com/stretchr/objx v0.5.2 // indirect 79 78 github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 80 79 github.com/ugorji/go/codec v1.2.12 // indirect 81 80 github.com/x448/float16 v0.8.4 // indirect
+7 -7
internal/agent/deployment.go
··· 162 162 workItems = append(workItems, deviceWork{ 163 163 device: aggregatedDevice, 164 164 agentName: fmt.Sprintf("%s-%s-%d", AgentNamePrefix, hsmDevice.Name, i), 165 - agentKey: fmt.Sprintf("%s-%d", hsmDevice.Name, i), 165 + agentKey: fmt.Sprintf("%s-%s", hsmDevice.Name, aggregatedDevice.SerialNumber), 166 166 index: i, 167 167 }) 168 168 } ··· 332 332 } 333 333 334 334 // Clean up all agent deployments (one per aggregated device) 335 - for i := range hsmPool.Status.AggregatedDevices { 335 + for i, aggregatedDevice := range hsmPool.Status.AggregatedDevices { 336 336 agentName := fmt.Sprintf("%s-%s-%d", AgentNamePrefix, hsmDevice.Name, i) 337 - agentKey := fmt.Sprintf("%s-%d", hsmDevice.Name, i) 337 + agentKey := fmt.Sprintf("%s-%s", hsmDevice.Name, aggregatedDevice.SerialNumber) 338 338 339 339 // Remove from internal tracking 340 340 m.removeAgentFromTracking(agentKey) ··· 901 901 var allPodIPs []string 902 902 903 903 // Collect pod IPs from all agent instances for this device 904 - for i := range hsmPool.Status.AggregatedDevices { 905 - agentKey := fmt.Sprintf("%s-%d", deviceName, i) 904 + for _, aggregatedDevice := range hsmPool.Status.AggregatedDevices { 905 + agentKey := fmt.Sprintf("%s-%s", deviceName, aggregatedDevice.SerialNumber) 906 906 if agentInfo, exists := m.activeAgents[agentKey]; exists && len(agentInfo.PodIPs) > 0 { 907 907 allPodIPs = append(allPodIPs, agentInfo.PodIPs...) 908 908 } ··· 930 930 return endpoints, nil 931 931 } 932 932 933 - // CreateSingleGRPCClient creates a gRPC client for the first available agent pod of a device 934 - func (m *Manager) CreateSingleGRPCClient(ctx context.Context, deviceName, namespace string, logger logr.Logger) (hsm.Client, error) { 933 + // CreateGRPCClient creates a gRPC client for the first available agent pod of a device 934 + func (m *Manager) CreateGRPCClient(ctx context.Context, deviceName, namespace string, logger logr.Logger) (hsm.Client, error) { 935 935 endpoints, err := m.GetGRPCEndpoints(ctx, deviceName, namespace) 936 936 if err != nil { 937 937 return nil, err
+543 -343
internal/api/proxy_client.go
··· 35 35 Error error 36 36 } 37 37 38 + // checksumResult represents the result of getting checksum from a single device 39 + type checksumResult struct { 40 + deviceName string 41 + checksum string 42 + err error 43 + } 44 + 45 + // secretResult represents the result of reading secret from a single device 46 + type secretResult struct { 47 + deviceName string 48 + data hsm.SecretData 49 + metadata *hsm.SecretMetadata 50 + err error 51 + } 52 + 53 + // metadataResult represents the result of reading metadata from a single device 54 + type metadataResult struct { 55 + deviceName string 56 + metadata *hsm.SecretMetadata 57 + err error 58 + } 59 + 60 + // ListSecretsResponse represents the response for listing secrets 61 + type ListSecretsResponse struct { 62 + Secrets []string `json:"secrets"` 63 + Count int `json:"count"` 64 + Prefix string `json:"prefix,omitempty"` 65 + } 66 + 67 + // ReadSecretResponse represents the response for reading a secret 68 + type ReadSecretResponse struct { 69 + Path string `json:"path"` 70 + Data map[string][]byte `json:"data"` 71 + } 72 + 73 + // WriteSecretResponse represents the response for writing a secret 74 + type WriteSecretResponse struct { 75 + Path string `json:"path"` 76 + Keys int `json:"keys"` 77 + } 78 + 79 + // DeleteSecretResponse represents the response for deleting a secret 80 + type DeleteSecretResponse struct { 81 + Path string `json:"path"` 82 + Devices int `json:"devices"` 83 + DeviceResults map[string]any `json:"deviceResults"` 84 + Warnings []string `json:"warnings,omitempty"` 85 + } 86 + 87 + // ReadMetadataResponse represents the response for reading metadata 88 + type ReadMetadataResponse struct { 89 + Path string `json:"path"` 90 + Metadata *hsm.SecretMetadata `json:"metadata"` 91 + } 92 + 93 + // GetChecksumResponse represents the response for getting a checksum 94 + type GetChecksumResponse struct { 95 + Path string `json:"path"` 96 + Checksum string `json:"checksum"` 97 + } 98 + 99 + type IsConnectedResponse struct { 100 + Devices map[string]bool `json:"devices"` 101 + TotalDevices int `json:"totalDevices"` 102 + } 103 + 104 + // GetInfoResponse represents the response for getting HSM info 105 + type GetInfoResponse struct { 106 + DeviceInfos map[string]*hsm.HSMInfo `json:"deviceInfos"` // deviceName -> HSMInfo 107 + } 108 + 38 109 // ProxyClient handles HTTP requests and proxies them to gRPC clients 39 110 // It has methods that match the HTTP endpoints and handle the full request/response cycle 40 111 type ProxyClient struct { ··· 53 124 } 54 125 } 55 126 56 - // getOrCreateGRPCClient returns the cached gRPC client for a device or creates a new one 57 - func (p *ProxyClient) getOrCreateGRPCClient(c *gin.Context) (hsm.Client, error) { 58 - // Extract namespace 59 - namespace := c.GetHeader("X-Namespace") 60 - if namespace == "" { 61 - namespace = "secrets" 127 + // Helper function to parse timestamp from metadata 128 + func parseTimestampFromMetadata(metadata *hsm.SecretMetadata) int64 { 129 + if metadata == nil || metadata.Labels == nil { 130 + return 0 62 131 } 63 132 64 - // Find available agent 65 - deviceName, err := p.server.findAvailableAgent(c.Request.Context(), namespace) 66 - if err != nil { 67 - return nil, err 133 + // Try RFC3339 timestamp first 134 + if syncTimestamp, exists := metadata.Labels["sync.timestamp"]; exists { 135 + if parsedTime, err := time.Parse(time.RFC3339, syncTimestamp); err == nil { 136 + return parsedTime.Unix() 137 + } 68 138 } 69 139 70 - // Try to get existing client for this device with read lock 71 - p.clientsMutex.RLock() 72 - if client, exists := p.grpcClients[deviceName]; exists && client.IsConnected() { 73 - p.clientsMutex.RUnlock() 74 - return client, nil 140 + // Fall back to Unix timestamp in sync.version 141 + if syncVersion, exists := metadata.Labels["sync.version"]; exists { 142 + var timestamp int64 143 + if n, err := fmt.Sscanf(syncVersion, "%d", &timestamp); n == 1 && err == nil { 144 + return timestamp 145 + } 75 146 } 76 - p.clientsMutex.RUnlock() 77 147 78 - // Need to create/recreate client with write lock 79 - p.clientsMutex.Lock() 80 - defer p.clientsMutex.Unlock() 148 + return 0 149 + } 81 150 82 - // Double-check in case another goroutine created it 83 - if client, exists := p.grpcClients[deviceName]; exists && client.IsConnected() { 84 - return client, nil 151 + // isSecretDeleted checks if a secret is marked as deleted via tombstone metadata 152 + func isSecretDeleted(metadata *hsm.SecretMetadata) bool { 153 + if metadata == nil || metadata.Labels == nil { 154 + return false 155 + } 156 + return metadata.Labels["sync.deleted"] == "true" 157 + } 158 + 159 + // findConsensusChecksum finds the most common checksum among results and logs inconsistencies 160 + func (p *ProxyClient) findConsensusChecksum(results []checksumResult, path string) (string, error) { 161 + if len(results) == 0 { 162 + return "", fmt.Errorf("checksum not found on any HSM device") 85 163 } 86 164 87 - // Close existing client for this device if it exists 88 - if oldClient, exists := p.grpcClients[deviceName]; exists { 89 - if closeErr := oldClient.Close(); closeErr != nil { 90 - p.logger.V(1).Info("Error closing old gRPC client", "device", deviceName, "error", closeErr) 165 + // Count occurrences of each checksum 166 + checksumCounts := make(map[string]int) 167 + for _, result := range results { 168 + checksumCounts[result.checksum]++ 169 + } 170 + 171 + // Find the most common checksum (consensus) 172 + var consensusChecksum string 173 + var maxCount int 174 + for checksum, count := range checksumCounts { 175 + if count > maxCount { 176 + consensusChecksum = checksum 177 + maxCount = count 178 + } 179 + } 180 + 181 + // Log checksum inconsistencies 182 + if len(checksumCounts) > 1 { 183 + inconsistentDevices := make([]string, 0) 184 + for _, result := range results { 185 + if result.checksum != consensusChecksum { 186 + inconsistentDevices = append(inconsistentDevices, result.deviceName) 187 + } 188 + } 189 + p.logger.Info("Checksum inconsistency detected, using consensus", 190 + "path", path, 191 + "consensus", consensusChecksum, 192 + "consensus_count", maxCount, 193 + "total_devices", len(results), 194 + "inconsistent_devices", inconsistentDevices) 195 + } 196 + 197 + return consensusChecksum, nil 198 + } 199 + 200 + // logMultiDeviceOperation logs when operations are performed across multiple devices with sync information 201 + func (p *ProxyClient) logMultiDeviceOperation(deviceNames []string, selectedDevice, operationName, path, syncDetails string) { 202 + p.logger.Info(fmt.Sprintf("%s found on multiple devices, using most recent version", operationName), 203 + "path", path, 204 + "devices", deviceNames, 205 + "selected", selectedDevice, 206 + "syncDetails", syncDetails) 207 + } 208 + 209 + // findMostRecentSecretResult finds the most recent secret result based on metadata timestamps 210 + func (p *ProxyClient) findMostRecentSecretResult(results []secretResult, path string) (hsm.SecretData, error) { 211 + if len(results) == 0 { 212 + return nil, fmt.Errorf("secret not found on any HSM device") 213 + } 214 + 215 + // Find most recent version based on metadata timestamps 216 + bestResult := results[0] 217 + bestTimestamp := parseTimestampFromMetadata(bestResult.metadata) 218 + 219 + for _, result := range results[1:] { 220 + timestamp := parseTimestampFromMetadata(result.metadata) 221 + if timestamp > bestTimestamp { 222 + bestResult = result 223 + bestTimestamp = timestamp 224 + } 225 + } 226 + 227 + // Log sync issues when multiple devices have different versions 228 + if len(results) > 1 { 229 + deviceNames := make([]string, len(results)) 230 + for i, result := range results { 231 + deviceNames[i] = result.deviceName 232 + } 233 + p.logMultiDeviceOperation(deviceNames, bestResult.deviceName, "Secret", path, fmt.Sprintf("timestamp: %d", bestTimestamp)) 234 + } 235 + 236 + // Check if the most recent result is a tombstone (deleted secret) 237 + if isSecretDeleted(bestResult.metadata) { 238 + return nil, fmt.Errorf("secret not found on any HSM device") 239 + } 240 + 241 + return bestResult.data, nil 242 + } 243 + 244 + // findMostRecentMetadataResult finds the most recent metadata result based on timestamps 245 + func (p *ProxyClient) findMostRecentMetadataResult(results []metadataResult, path string) (*hsm.SecretMetadata, error) { 246 + if len(results) == 0 { 247 + return nil, fmt.Errorf("metadata not found on any HSM device") 248 + } 249 + 250 + // Find most recent version based on metadata timestamps 251 + bestResult := results[0] 252 + bestTimestamp := parseTimestampFromMetadata(bestResult.metadata) 253 + 254 + for _, result := range results[1:] { 255 + timestamp := parseTimestampFromMetadata(result.metadata) 256 + if timestamp > bestTimestamp { 257 + bestResult = result 258 + bestTimestamp = timestamp 91 259 } 92 - delete(p.grpcClients, deviceName) 93 260 } 94 261 95 - // Create new gRPC client 96 - grpcClient, err := p.server.createGRPCClient(c.Request.Context(), deviceName, namespace) 97 - if err != nil { 98 - return nil, err 262 + // Log sync issues when multiple devices have different versions 263 + if len(results) > 1 { 264 + deviceNames := make([]string, len(results)) 265 + for i, result := range results { 266 + deviceNames[i] = result.deviceName 267 + } 268 + p.logMultiDeviceOperation(deviceNames, bestResult.deviceName, "Metadata", path, fmt.Sprintf("timestamp: %d", bestTimestamp)) 99 269 } 100 270 101 - // Cache the client for this device 102 - p.grpcClients[deviceName] = grpcClient 103 - p.logger.V(1).Info("Created new gRPC client", "device", deviceName) 104 - return grpcClient, nil 271 + // Return the most recent metadata, even if it's a tombstone 272 + // This allows callers to see deletion information when needed 273 + return bestResult.metadata, nil 105 274 } 106 275 107 - // getAllAvailableGRPCClients returns all available gRPC clients for mirroring operations 108 - func (p *ProxyClient) getAllAvailableGRPCClients(c *gin.Context) (map[string]hsm.Client, error) { 109 - // Extract namespace 110 - namespace := c.GetHeader("X-Namespace") 111 - if namespace == "" { 112 - namespace = "secrets" 276 + // validatePathParam validates the path parameter and sends error if missing 277 + // Returns (path, true) on success, or ("", false) if path is missing (error already sent to client) 278 + func (p *ProxyClient) validatePathParam(c *gin.Context) (string, bool) { 279 + path := c.Param("path") 280 + if path == "" { 281 + p.server.sendError(c, http.StatusBadRequest, "missing_path", "Secret path is required", nil) 282 + return "", false 113 283 } 284 + return path, true 285 + } 286 + 287 + // getAllAvailableGRPCClients returns all available gRPC clients for mirroring operations 288 + // Returns (clients, true) on success, or (nil, false) if no agents available (error already sent to client) 289 + func (p *ProxyClient) getAllAvailableGRPCClients(c *gin.Context) (map[string]hsm.Client, bool) { 290 + // Use operator namespace where HSMPools and agents are located 291 + namespace := p.server.operatorNamespace 114 292 115 293 // Get all available devices 116 294 devices, err := p.server.getAllAvailableAgents(c.Request.Context(), namespace) 117 295 if err != nil { 118 - return nil, err 296 + p.server.sendError(c, http.StatusServiceUnavailable, "no_agents", "No HSM agents available", map[string]any{ 297 + "error": err.Error(), 298 + }) 299 + return nil, false 119 300 } 120 301 121 302 clients := make(map[string]hsm.Client) ··· 150 331 p.logger.V(1).Info("Created new gRPC client", "device", deviceName) 151 332 } 152 333 153 - return clients, nil 334 + if len(clients) == 0 { 335 + p.server.sendError(c, http.StatusServiceUnavailable, "no_agents", "No HSM agents available", nil) 336 + return nil, false 337 + } 338 + 339 + return clients, true 154 340 } 155 341 156 342 // GetInfo handles GET /hsm/info 157 343 func (p *ProxyClient) GetInfo(c *gin.Context) { 158 - grpcClient, err := p.getOrCreateGRPCClient(c) 159 - if err != nil { 160 - p.server.sendError(c, http.StatusServiceUnavailable, "no_agent", "No HSM agents available", map[string]any{ 161 - "error": err.Error(), 162 - }) 344 + clients, ok := p.getAllAvailableGRPCClients(c) 345 + if !ok { 163 346 return 164 347 } 165 348 166 - info, err := grpcClient.GetInfo(c.Request.Context()) 167 - if err != nil { 168 - p.server.sendError(c, http.StatusInternalServerError, "grpc_error", "Failed to get HSM info", map[string]any{ 169 - "error": err.Error(), 170 - }) 349 + type infoResult struct { 350 + deviceName string 351 + info *hsm.HSMInfo 352 + err error 353 + } 354 + 355 + resultsChan := make(chan infoResult, len(clients)) 356 + for deviceName, grpcClient := range clients { 357 + go func(deviceName string, grpcClient hsm.Client) { 358 + info, err := grpcClient.GetInfo(c) 359 + resultsChan <- infoResult{deviceName, info, err} 360 + }(deviceName, grpcClient) 361 + } 362 + 363 + // Collect successful results 364 + deviceInfos := make(map[string]*hsm.HSMInfo, len(clients)) 365 + for i := 0; i < len(clients); i++ { 366 + result := <-resultsChan 367 + if result.err == nil { 368 + deviceInfos[result.deviceName] = result.info 369 + } else { 370 + p.logger.V(1).Info("Failed to get info from device", "device", result.deviceName, "error", result.err) 371 + } 372 + } 373 + 374 + if len(deviceInfos) == 0 { 375 + p.server.sendError(c, http.StatusInternalServerError, "info_failed", "Failed to get info from any HSM device", nil) 171 376 return 172 377 } 173 378 174 - p.server.sendResponse(c, http.StatusOK, "HSM info retrieved successfully", info) 379 + response := GetInfoResponse{DeviceInfos: deviceInfos} 380 + p.server.sendResponse(c, http.StatusOK, "HSM info retrieved successfully", response) 175 381 } 176 382 177 383 // ListSecrets handles GET /hsm/secrets 178 384 func (p *ProxyClient) ListSecrets(c *gin.Context) { 179 385 prefix := c.Query("prefix") 180 386 181 - grpcClient, err := p.getOrCreateGRPCClient(c) 182 - if err != nil { 183 - p.server.sendError(c, http.StatusServiceUnavailable, "no_agent", "No HSM agents available", map[string]any{ 184 - "error": err.Error(), 185 - }) 387 + clients, ok := p.getAllAvailableGRPCClients(c) 388 + if !ok { 186 389 return 187 390 } 391 + type secretsResult struct { 392 + deviceName string 393 + secrets []string 394 + err error 395 + } 188 396 189 - secrets, err := grpcClient.ListSecrets(c.Request.Context(), prefix) 190 - if err != nil { 191 - p.server.sendError(c, http.StatusInternalServerError, "grpc_error", "Failed to list secrets from HSM", map[string]any{ 192 - "error": err.Error(), 193 - }) 194 - return 397 + resultsChan := make(chan secretsResult, len(clients)) 398 + for deviceName, grpcClient := range clients { 399 + go func(deviceName string, grpcClient hsm.Client) { 400 + secrets, err := grpcClient.ListSecrets(c, prefix) 401 + resultsChan <- secretsResult{deviceName, secrets, err} 402 + }(deviceName, grpcClient) 403 + } 404 + 405 + // Collect results and deduplicate 406 + secretsSet := make(map[string]bool) 407 + for i := 0; i < len(clients); i++ { 408 + result := <-resultsChan 409 + if result.err != nil { 410 + p.logger.V(1).Info("Failed to list secrets from device", "device", result.deviceName, "error", result.err) 411 + continue 412 + } 413 + 414 + // Add all secrets from this device to the union set 415 + for _, secretPath := range result.secrets { 416 + secretsSet[secretPath] = true 417 + } 195 418 } 196 419 197 - response := map[string]any{ 198 - "secrets": secrets, 199 - "count": len(secrets), 200 - "prefix": prefix, 420 + // Convert set to slice 421 + allSecrets := make([]string, 0, len(secretsSet)) 422 + for secretPath := range secretsSet { 423 + allSecrets = append(allSecrets, secretPath) 424 + } 425 + 426 + response := ListSecretsResponse{ 427 + Secrets: allSecrets, 428 + Count: len(allSecrets), 429 + Prefix: prefix, 201 430 } 202 431 p.server.sendResponse(c, http.StatusOK, "Secrets listed successfully", response) 203 432 } 204 433 205 434 // ReadSecret handles GET /hsm/secrets/:path 206 435 func (p *ProxyClient) ReadSecret(c *gin.Context) { 207 - path := c.Param("path") 208 - if path == "" { 209 - p.server.sendError(c, http.StatusBadRequest, "missing_path", "Secret path is required", nil) 436 + path, ok := p.validatePathParam(c) 437 + if !ok { 210 438 return 211 439 } 212 440 213 - grpcClient, err := p.getOrCreateGRPCClient(c) 214 - if err != nil { 215 - p.server.sendError(c, http.StatusServiceUnavailable, "no_agent", "No HSM agents available", map[string]any{ 216 - "error": err.Error(), 217 - }) 441 + clients, ok := p.getAllAvailableGRPCClients(c) 442 + if !ok { 218 443 return 219 444 } 220 445 221 - data, err := grpcClient.ReadSecret(c.Request.Context(), path) 446 + // Read from all devices in parallel 447 + resultsChan := make(chan secretResult, len(clients)) 448 + for deviceName, grpcClient := range clients { 449 + go func(deviceName string, grpcClient hsm.Client) { 450 + // Read secret data 451 + data, err := grpcClient.ReadSecret(c.Request.Context(), path) 452 + if err != nil { 453 + resultsChan <- secretResult{deviceName: deviceName, err: err} 454 + return 455 + } 456 + 457 + // Read metadata for timestamp comparison 458 + metadata, metaErr := grpcClient.ReadMetadata(c.Request.Context(), path) 459 + if metaErr != nil { 460 + p.logger.V(1).Info("Failed to read metadata for version comparison", "device", deviceName, "path", path, "error", metaErr) 461 + } 462 + 463 + resultsChan <- secretResult{ 464 + deviceName: deviceName, 465 + data: data, 466 + metadata: metadata, 467 + } 468 + }(deviceName, grpcClient) 469 + } 470 + 471 + // Collect successful results 472 + var successfulResults []secretResult 473 + for i := 0; i < len(clients); i++ { 474 + result := <-resultsChan 475 + if result.err != nil { 476 + p.logger.V(1).Info("Failed to read secret from device", "device", result.deviceName, "path", path, "error", result.err) 477 + continue 478 + } 479 + successfulResults = append(successfulResults, result) 480 + } 481 + 482 + // Use helper to find most recent result based on timestamps 483 + data, err := p.findMostRecentSecretResult(successfulResults, path) 222 484 if err != nil { 223 - p.server.sendError(c, http.StatusInternalServerError, "grpc_error", "Failed to read secret from HSM", map[string]any{ 224 - "error": err.Error(), 225 - "path": path, 226 - }) 485 + p.server.sendError(c, http.StatusNotFound, "secret_not_found", err.Error(), nil) 227 486 return 228 487 } 229 488 230 - response := map[string]any{ 231 - "path": path, 232 - "data": data, 233 - } 489 + response := ReadSecretResponse{Path: path, Data: data} 234 490 p.server.sendResponse(c, http.StatusOK, "Secret read successfully", response) 235 491 } 236 492 237 493 // WriteSecret handles POST/PUT /hsm/secrets/:path with mirroring support 238 494 func (p *ProxyClient) WriteSecret(c *gin.Context) { 239 - path := c.Param("path") 240 - if path == "" { 241 - p.server.sendError(c, http.StatusBadRequest, "missing_path", "Secret path is required", nil) 495 + path, ok := p.validatePathParam(c) 496 + if !ok { 242 497 return 243 498 } 244 499 ··· 261 516 data[key] = []byte(value) 262 517 } 263 518 264 - // Determine if we should mirror this write (default: true) 265 - shouldMirror := req.Mirror == nil || *req.Mirror 266 - 267 - if shouldMirror { 268 - // Get all available clients for mirroring 269 - clients, err := p.getAllAvailableGRPCClients(c) 270 - if err != nil { 271 - p.server.sendError(c, http.StatusServiceUnavailable, "no_agents", "No HSM agents available for mirroring", map[string]any{ 272 - "error": err.Error(), 273 - }) 274 - return 275 - } 276 - 277 - if len(clients) == 0 { 278 - p.server.sendError(c, http.StatusServiceUnavailable, "no_agents", "No HSM agents available", nil) 279 - return 280 - } 281 - 282 - // Add mirroring metadata 283 - metadata := req.Metadata 284 - if metadata == nil { 285 - metadata = &hsm.SecretMetadata{Labels: make(map[string]string)} 286 - } 287 - if metadata.Labels == nil { 288 - metadata.Labels = make(map[string]string) 289 - } 290 - metadata.Labels["sync.version"] = fmt.Sprintf("%d", time.Now().Unix()) 291 - metadata.Labels["sync.timestamp"] = time.Now().Format(time.RFC3339) 292 - metadata.Labels["sync.mirrored"] = "true" 519 + // Get all available clients for mirroring 520 + clients, ok := p.getAllAvailableGRPCClients(c) 521 + if !ok { 522 + return 523 + } 293 524 294 - // Write to all devices in parallel 295 - results := p.writeToAllDevices(c.Request.Context(), clients, path, data, metadata) 525 + // Add mirroring metadata 526 + metadata := req.Metadata 527 + if metadata == nil { 528 + metadata = &hsm.SecretMetadata{Labels: make(map[string]string)} 529 + } 530 + if metadata.Labels == nil { 531 + metadata.Labels = make(map[string]string) 532 + } 533 + metadata.Labels["sync.version"] = fmt.Sprintf("%d", time.Now().Unix()) 534 + metadata.Labels["sync.timestamp"] = time.Now().Format(time.RFC3339) 296 535 297 - // Check results 298 - successful := 0 299 - var errors []string 300 - deviceResults := make(map[string]any) 536 + // Write to all devices in parallel 537 + results := p.writeToAllDevices(c.Request.Context(), clients, path, data, metadata) 301 538 302 - for deviceName, result := range results { 303 - deviceResults[deviceName] = map[string]any{ 304 - "success": result.Error == nil, 305 - "error": func() string { 306 - if result.Error != nil { 307 - return result.Error.Error() 308 - } 309 - return "" 310 - }(), 311 - } 312 - 313 - if result.Error == nil { 314 - successful++ 315 - } else { 316 - errors = append(errors, fmt.Sprintf("%s: %v", deviceName, result.Error)) 317 - p.logger.Error(result.Error, "Failed to write to device", "device", deviceName, "path", path) 318 - } 319 - } 320 - 321 - // Consider the operation successful if we wrote to at least one device 322 - if successful > 0 { 323 - response := map[string]any{ 324 - "path": path, 325 - "keys": len(data), 326 - "mirrored": true, 327 - "devices": len(clients), 328 - "successful": successful, 329 - "deviceResults": deviceResults, 330 - } 331 - if metadata != nil { 332 - response["metadata"] = metadata 333 - } 334 - if len(errors) > 0 { 335 - response["warnings"] = errors 336 - } 337 - 338 - statusCode := http.StatusCreated 339 - message := "Secret written successfully" 340 - if successful < len(clients) { 341 - statusCode = http.StatusPartialContent // 206 indicates partial success 342 - message = fmt.Sprintf("Secret written to %d/%d devices", successful, len(clients)) 343 - } 344 - 345 - p.server.sendResponse(c, statusCode, message, response) 539 + // Check results - log failures but succeed if at least one device works 540 + successful := 0 541 + for deviceName, result := range results { 542 + if result.Error == nil { 543 + successful++ 346 544 } else { 347 - // All devices failed 348 - p.server.sendError(c, http.StatusInternalServerError, "write_failed", "Failed to write secret to any HSM device", map[string]any{ 349 - "errors": errors, 350 - "deviceResults": deviceResults, 351 - "path": path, 352 - }) 545 + p.logger.Error(result.Error, "Failed to write to device", "device", deviceName, "path", path) 353 546 } 354 - } else { 355 - // Single-device write (no mirroring) 356 - grpcClient, err := p.getOrCreateGRPCClient(c) 357 - if err != nil { 358 - p.server.sendError(c, http.StatusServiceUnavailable, "no_agent", "No HSM agents available", map[string]any{ 359 - "error": err.Error(), 360 - }) 361 - return 362 - } 547 + } 363 548 364 - if req.Metadata != nil { 365 - err = grpcClient.WriteSecretWithMetadata(c.Request.Context(), path, data, req.Metadata) 366 - } else { 367 - err = grpcClient.WriteSecret(c.Request.Context(), path, data) 549 + // Succeed if we wrote to at least one device 550 + if successful > 0 { 551 + if successful < len(clients) { 552 + p.logger.Info("Secret written to subset of devices", 553 + "path", path, 554 + "successful", successful, 555 + "total", len(clients)) 368 556 } 369 557 370 - if err != nil { 371 - p.server.sendError(c, http.StatusInternalServerError, "grpc_error", "Failed to write secret to HSM", map[string]any{ 372 - "error": err.Error(), 373 - "path": path, 374 - }) 375 - return 558 + response := WriteSecretResponse{ 559 + Path: path, 560 + Keys: len(data), 376 561 } 377 562 378 - response := map[string]any{ 379 - "path": path, 380 - "keys": len(data), 381 - "mirrored": false, 382 - } 383 - if req.Metadata != nil { 384 - response["metadata"] = req.Metadata 385 - } 386 563 p.server.sendResponse(c, http.StatusCreated, "Secret written successfully", response) 564 + } else { 565 + // All devices failed 566 + p.server.sendError(c, http.StatusInternalServerError, "write_failed", "Failed to write secret to any HSM device", nil) 387 567 } 388 568 } 389 569 390 570 // DeleteSecret handles DELETE /hsm/secrets/:path with mirroring support 391 571 func (p *ProxyClient) DeleteSecret(c *gin.Context) { 392 - path := c.Param("path") 393 - if path == "" { 394 - p.server.sendError(c, http.StatusBadRequest, "missing_path", "Secret path is required", nil) 572 + path, ok := p.validatePathParam(c) 573 + if !ok { 395 574 return 396 575 } 397 576 398 - // Check if mirroring is explicitly disabled 399 - mirror := c.Query("mirror") 400 - shouldMirror := mirror != "false" // Default to true unless explicitly set to false 577 + // Get all available clients for mirroring delete 578 + clients, ok := p.getAllAvailableGRPCClients(c) 579 + if !ok { 580 + return 581 + } 401 582 402 - if shouldMirror { 403 - // Get all available clients for mirroring delete 404 - clients, err := p.getAllAvailableGRPCClients(c) 405 - if err != nil { 406 - p.server.sendError(c, http.StatusServiceUnavailable, "no_agents", "No HSM agents available for mirroring", map[string]any{ 407 - "error": err.Error(), 408 - }) 409 - return 410 - } 583 + // Perform tombstone deletion from all devices in parallel 584 + results := p.tombstoneDeleteFromAllDevices(c.Request.Context(), clients, path) 585 + 586 + // Check results 587 + successful := 0 588 + var errors []string 589 + deviceResults := make(map[string]any) 411 590 412 - if len(clients) == 0 { 413 - p.server.sendError(c, http.StatusServiceUnavailable, "no_agents", "No HSM agents available", nil) 414 - return 591 + for deviceName, result := range results { 592 + deviceResults[deviceName] = map[string]any{ 593 + "success": result.Error == nil, 594 + "error": func() string { 595 + if result.Error != nil { 596 + return result.Error.Error() 597 + } 598 + return "" 599 + }(), 415 600 } 416 601 417 - // Delete from all devices in parallel 418 - results := p.deleteFromAllDevices(c.Request.Context(), clients, path) 419 - 420 - // Check results 421 - successful := 0 422 - var errors []string 423 - deviceResults := make(map[string]any) 424 - 425 - for deviceName, result := range results { 426 - deviceResults[deviceName] = map[string]any{ 427 - "success": result.Error == nil, 428 - "error": func() string { 429 - if result.Error != nil { 430 - return result.Error.Error() 431 - } 432 - return "" 433 - }(), 434 - } 435 - 436 - if result.Error == nil { 437 - successful++ 438 - } else { 439 - errors = append(errors, fmt.Sprintf("%s: %v", deviceName, result.Error)) 440 - p.logger.Error(result.Error, "Failed to delete from device", "device", deviceName, "path", path) 441 - } 602 + if result.Error == nil { 603 + successful++ 604 + } else { 605 + errors = append(errors, fmt.Sprintf("%s: %v", deviceName, result.Error)) 606 + p.logger.Error(result.Error, "Failed to delete from device", "device", deviceName, "path", path) 442 607 } 608 + } 443 609 444 - // Consider the operation successful if we deleted from at least one device 445 - if successful > 0 { 446 - response := map[string]any{ 447 - "path": path, 448 - "mirrored": true, 449 - "devices": len(clients), 450 - "successful": successful, 451 - "deviceResults": deviceResults, 452 - } 453 - if len(errors) > 0 { 454 - response["warnings"] = errors 455 - } 456 - 457 - statusCode := http.StatusOK 458 - message := "Secret deleted successfully" 459 - if successful < len(clients) { 460 - statusCode = http.StatusPartialContent // 206 indicates partial success 461 - message = fmt.Sprintf("Secret deleted from %d/%d devices", successful, len(clients)) 462 - } 463 - 464 - p.server.sendResponse(c, statusCode, message, response) 465 - } else { 466 - // All devices failed 467 - p.server.sendError(c, http.StatusInternalServerError, "delete_failed", "Failed to delete secret from any HSM device", map[string]any{ 468 - "errors": errors, 469 - "deviceResults": deviceResults, 470 - "path": path, 471 - }) 610 + // Consider the operation successful if we deleted from at least one device 611 + if successful > 0 { 612 + response := DeleteSecretResponse{ 613 + Path: path, 614 + Devices: len(clients), 615 + DeviceResults: deviceResults, 472 616 } 473 - } else { 474 - // Single-device delete (no mirroring) 475 - grpcClient, err := p.getOrCreateGRPCClient(c) 476 - if err != nil { 477 - p.server.sendError(c, http.StatusServiceUnavailable, "no_agent", "No HSM agents available", map[string]any{ 478 - "error": err.Error(), 479 - }) 480 - return 617 + if len(errors) > 0 { 618 + response.Warnings = errors 481 619 } 482 620 483 - err = grpcClient.DeleteSecret(c.Request.Context(), path) 484 - if err != nil { 485 - p.server.sendError(c, http.StatusInternalServerError, "grpc_error", "Failed to delete secret from HSM", map[string]any{ 486 - "error": err.Error(), 487 - "path": path, 488 - }) 489 - return 621 + statusCode := http.StatusOK 622 + message := "Secret deleted successfully" 623 + if successful < len(clients) { 624 + statusCode = http.StatusPartialContent // 206 indicates partial success 625 + message = fmt.Sprintf("Secret deleted from %d/%d devices", successful, len(clients)) 490 626 } 491 627 492 - response := map[string]any{ 493 - "path": path, 494 - "mirrored": false, 495 - } 496 - p.server.sendResponse(c, http.StatusOK, "Secret deleted successfully", response) 628 + p.server.sendResponse(c, statusCode, message, response) 629 + } else { 630 + // All devices failed 631 + p.server.sendError(c, http.StatusInternalServerError, "delete_failed", "Failed to delete secret from any HSM device", map[string]any{ 632 + "errors": errors, 633 + "deviceResults": deviceResults, 634 + "path": path, 635 + }) 497 636 } 498 637 } 499 638 500 639 // ReadMetadata handles GET /hsm/secrets/:path/metadata 501 640 func (p *ProxyClient) ReadMetadata(c *gin.Context) { 502 - path := c.Param("path") 503 - if path == "" { 504 - p.server.sendError(c, http.StatusBadRequest, "missing_path", "Secret path is required", nil) 641 + path, ok := p.validatePathParam(c) 642 + if !ok { 505 643 return 506 644 } 507 645 508 - grpcClient, err := p.getOrCreateGRPCClient(c) 509 - if err != nil { 510 - p.server.sendError(c, http.StatusServiceUnavailable, "no_agent", "No HSM agents available", map[string]any{ 511 - "error": err.Error(), 512 - }) 646 + clients, ok := p.getAllAvailableGRPCClients(c) 647 + if !ok { 513 648 return 514 649 } 515 650 516 - metadata, err := grpcClient.ReadMetadata(c.Request.Context(), path) 651 + // Read metadata from all devices in parallel 652 + resultsChan := make(chan metadataResult, len(clients)) 653 + for deviceName, grpcClient := range clients { 654 + go func(deviceName string, grpcClient hsm.Client) { 655 + metadata, err := grpcClient.ReadMetadata(c.Request.Context(), path) 656 + resultsChan <- metadataResult{ 657 + deviceName: deviceName, 658 + metadata: metadata, 659 + err: err, 660 + } 661 + }(deviceName, grpcClient) 662 + } 663 + 664 + // Collect successful results 665 + var successfulResults []metadataResult 666 + for i := 0; i < len(clients); i++ { 667 + result := <-resultsChan 668 + if result.err != nil { 669 + p.logger.V(1).Info("Failed to read metadata from device", "device", result.deviceName, "path", path, "error", result.err) 670 + continue 671 + } 672 + successfulResults = append(successfulResults, result) 673 + } 674 + 675 + // Use helper to find most recent result based on timestamps 676 + metadata, err := p.findMostRecentMetadataResult(successfulResults, path) 517 677 if err != nil { 518 - p.server.sendError(c, http.StatusInternalServerError, "grpc_error", "Failed to read metadata from HSM", map[string]any{ 519 - "error": err.Error(), 520 - "path": path, 521 - }) 678 + p.server.sendError(c, http.StatusNotFound, "metadata_not_found", err.Error(), nil) 522 679 return 523 680 } 524 681 525 - response := map[string]any{ 526 - "path": path, 527 - "metadata": metadata, 528 - } 682 + response := ReadMetadataResponse{Path: path, Metadata: metadata} 529 683 p.server.sendResponse(c, http.StatusOK, "Metadata read successfully", response) 530 684 } 531 685 532 686 // GetChecksum handles GET /hsm/secrets/:path/checksum 533 687 func (p *ProxyClient) GetChecksum(c *gin.Context) { 534 - path := c.Param("path") 535 - if path == "" { 536 - p.server.sendError(c, http.StatusBadRequest, "missing_path", "Secret path is required", nil) 688 + path, ok := p.validatePathParam(c) 689 + if !ok { 537 690 return 538 691 } 539 692 540 - grpcClient, err := p.getOrCreateGRPCClient(c) 541 - if err != nil { 542 - p.server.sendError(c, http.StatusServiceUnavailable, "no_agent", "No HSM agents available", map[string]any{ 543 - "error": err.Error(), 544 - }) 693 + clients, ok := p.getAllAvailableGRPCClients(c) 694 + if !ok { 545 695 return 546 696 } 547 697 548 - checksum, err := grpcClient.GetChecksum(c.Request.Context(), path) 698 + // Get checksums from all devices in parallel 699 + resultsChan := make(chan checksumResult, len(clients)) 700 + for deviceName, grpcClient := range clients { 701 + go func(deviceName string, grpcClient hsm.Client) { 702 + checksum, err := grpcClient.GetChecksum(c.Request.Context(), path) 703 + resultsChan <- checksumResult{ 704 + deviceName: deviceName, 705 + checksum: checksum, 706 + err: err, 707 + } 708 + }(deviceName, grpcClient) 709 + } 710 + 711 + // Collect successful results 712 + var successfulResults []checksumResult 713 + for i := 0; i < len(clients); i++ { 714 + result := <-resultsChan 715 + if result.err != nil { 716 + p.logger.V(1).Info("Failed to get checksum from device", "device", result.deviceName, "path", path, "error", result.err) 717 + continue 718 + } 719 + successfulResults = append(successfulResults, result) 720 + } 721 + 722 + // Use helper to find consensus checksum 723 + consensusChecksum, err := p.findConsensusChecksum(successfulResults, path) 549 724 if err != nil { 550 - p.server.sendError(c, http.StatusInternalServerError, "grpc_error", "Failed to get checksum from HSM", map[string]any{ 551 - "error": err.Error(), 552 - "path": path, 553 - }) 725 + p.server.sendError(c, http.StatusNotFound, "checksum_not_found", err.Error(), nil) 554 726 return 555 727 } 556 728 557 - response := map[string]any{ 558 - "path": path, 559 - "checksum": checksum, 560 - } 729 + response := GetChecksumResponse{Path: path, Checksum: consensusChecksum} 561 730 p.server.sendResponse(c, http.StatusOK, "Checksum retrieved successfully", response) 562 731 } 563 732 564 733 // IsConnected handles GET /hsm/status 565 734 func (p *ProxyClient) IsConnected(c *gin.Context) { 566 - grpcClient, err := p.getOrCreateGRPCClient(c) 567 - if err != nil { 568 - p.server.sendError(c, http.StatusServiceUnavailable, "no_agent", "No HSM agents available", map[string]any{ 569 - "error": err.Error(), 570 - }) 735 + clients, ok := p.getAllAvailableGRPCClients(c) 736 + if !ok { 571 737 return 572 738 } 573 739 574 - connected := grpcClient.IsConnected() 740 + devices := make(map[string]bool, len(clients)) 741 + connectedCount := 0 575 742 576 - response := map[string]any{ 577 - "connected": connected, 743 + for deviceName, grpcClient := range clients { 744 + connected := grpcClient.IsConnected() 745 + devices[deviceName] = connected 746 + 747 + if connected { 748 + connectedCount++ 749 + } 578 750 } 579 751 580 - status := http.StatusOK 581 - message := "HSM connection status retrieved" 582 - if !connected { 583 - status = http.StatusServiceUnavailable 584 - message = "HSM is not connected" 752 + // Log connectivity issues for operational visibility 753 + if connectedCount == 0 { 754 + p.logger.Info("No HSM devices are connected", "totalDevices", len(clients)) 755 + } else if connectedCount < len(clients) { 756 + p.logger.Info("Partial HSM connectivity detected", 757 + "connectedDevices", connectedCount, 758 + "totalDevices", len(clients)) 585 759 } 586 760 587 - p.server.sendResponse(c, status, message, response) 761 + response := IsConnectedResponse{ 762 + Devices: devices, 763 + TotalDevices: len(clients), 764 + } 765 + 766 + p.server.sendResponse(c, http.StatusOK, "HSM connection status retrieved", response) 588 767 } 589 768 590 769 // Close closes all cached gRPC clients ··· 659 838 return results 660 839 } 661 840 662 - // deleteFromAllDevices deletes secret data from all devices in parallel 663 - func (p *ProxyClient) deleteFromAllDevices(ctx context.Context, clients map[string]hsm.Client, path string) map[string]WriteResult { 841 + // tombstoneDeleteFromAllDevices performs tombstone deletion on all devices in parallel 842 + // Deletes secret data but leaves tombstone metadata to prevent resurrection 843 + func (p *ProxyClient) tombstoneDeleteFromAllDevices(ctx context.Context, clients map[string]hsm.Client, path string) map[string]WriteResult { 664 844 results := make(map[string]WriteResult) 665 845 resultsMutex := sync.Mutex{} 666 846 wg := sync.WaitGroup{} 847 + 848 + // Create tombstone metadata 849 + tombstoneMetadata := &hsm.SecretMetadata{ 850 + Labels: map[string]string{ 851 + "sync.deleted": "true", 852 + "sync.timestamp": time.Now().Format(time.RFC3339), 853 + "sync.version": "0", 854 + }, 855 + } 667 856 668 857 for deviceName, client := range clients { 669 858 wg.Add(1) 670 859 go func(deviceName string, client hsm.Client) { 671 860 defer wg.Done() 672 861 673 - err := client.DeleteSecret(ctx, path) 862 + var err error 863 + 864 + // First, delete the secret data 865 + if deleteErr := client.DeleteSecret(ctx, path); deleteErr != nil { 866 + // If delete fails, still try to write tombstone metadata 867 + p.logger.V(1).Info("Failed to delete secret data, will still create tombstone", 868 + "device", deviceName, "path", path, "error", deleteErr) 869 + } 870 + 871 + // Write tombstone metadata (empty data with deletion markers) 872 + emptyData := make(hsm.SecretData) 873 + err = client.WriteSecretWithMetadata(ctx, path, emptyData, tombstoneMetadata) 674 874 675 875 resultsMutex.Lock() 676 876 results[deviceName] = WriteResult{
+15 -25
internal/api/server.go
··· 35 35 36 36 // Server represents the HSM REST API server that proxies requests to agent pods 37 37 type Server struct { 38 - client client.Client 39 - agentManager *agent.Manager 40 - validator *validator.Validate 41 - logger logr.Logger 42 - router *gin.Engine 43 - proxyClient *ProxyClient 38 + client client.Client 39 + agentManager *agent.Manager 40 + validator *validator.Validate 41 + logger logr.Logger 42 + router *gin.Engine 43 + proxyClient *ProxyClient 44 + operatorNamespace string 44 45 } 45 46 46 47 // NewServer creates a new API server instance that proxies to agents 47 - func NewServer(k8sClient client.Client, agentManager *agent.Manager, logger logr.Logger) *Server { 48 + func NewServer(k8sClient client.Client, agentManager *agent.Manager, operatorNamespace string, logger logr.Logger) *Server { 48 49 s := &Server{ 49 - client: k8sClient, 50 - agentManager: agentManager, 51 - validator: validator.New(), 52 - logger: logger.WithName("api-server"), 50 + client: k8sClient, 51 + agentManager: agentManager, 52 + validator: validator.New(), 53 + logger: logger.WithName("api-server"), 54 + operatorNamespace: operatorNamespace, 53 55 } 54 56 55 57 // Create ProxyClient instance ··· 85 87 // handleHealth handles health check requests 86 88 func (s *Server) handleHealth(c *gin.Context) { 87 89 // Check if multiple agents are available for replication 88 - agents, _ := s.getAllAvailableAgents(c.Request.Context(), "secrets") 90 + agents, _ := s.getAllAvailableAgents(c.Request.Context(), s.operatorNamespace) 89 91 hsmConnected := len(agents) > 0 90 92 replicationEnabled := len(agents) > 1 91 93 activeNodes := len(agents) ··· 164 166 } 165 167 } 166 168 167 - // findAvailableAgent finds an available HSM agent for handling requests 168 - func (s *Server) findAvailableAgent(ctx context.Context, namespace string) (string, error) { 169 - agents, err := s.getAllAvailableAgents(ctx, namespace) 170 - if err != nil { 171 - return "", err 172 - } 173 - if len(agents) == 0 { 174 - return "", fmt.Errorf("no available HSM agents found") 175 - } 176 - return agents[0], nil 177 - } 178 - 179 169 // getAllAvailableAgents finds all available HSM agents for mirroring operations 180 170 func (s *Server) getAllAvailableAgents(ctx context.Context, namespace string) ([]string, error) { 181 171 if s.agentManager == nil { ··· 218 208 } 219 209 220 210 // Create gRPC client using AgentManager's existing method 221 - grpcClient, err := s.agentManager.CreateSingleGRPCClient(ctx, deviceName, namespace, s.logger) 211 + grpcClient, err := s.agentManager.CreateGRPCClient(ctx, deviceName, namespace, s.logger) 222 212 if err != nil { 223 213 return nil, fmt.Errorf("failed to create gRPC client for device %s: %w", deviceName, err) 224 214 }
+1 -2
internal/controller/hsmsecret_controller.go
··· 203 203 } 204 204 205 205 // Create gRPC client using agent manager's direct pod connections 206 - agentClient, err := r.AgentManager.CreateSingleGRPCClient(ctx, hsmDevice.Name, hsmSecret.Namespace, logger) 206 + agentClient, err := r.AgentManager.CreateGRPCClient(ctx, hsmDevice.Name, hsmSecret.Namespace, logger) 207 207 if err != nil { 208 208 // Clean up any successful connections before returning error 209 209 if err := deviceClients.Close(); err != nil { ··· 404 404 metadata := &hsm.SecretMetadata{ 405 405 Labels: map[string]string{ 406 406 "sync.version": fmt.Sprintf("%d", newVersion), 407 - "sync.primary": primaryDevice, 408 407 "sync.timestamp": time.Now().Format(time.RFC3339), 409 408 }, 410 409 }
+2 -2
internal/controller/hsmsecret_grpc_test.go
··· 226 226 require.NoError(t, err) 227 227 228 228 // Verify data was written to HSM via gRPC 229 - agentClient, err := agentManager.CreateSingleGRPCClient(ctx, "test-hsm-device", "default", logger) 229 + agentClient, err := agentManager.CreateGRPCClient(ctx, "test-hsm-device", "default", logger) 230 230 require.NoError(t, err) 231 231 defer func() { 232 232 assert.NoError(t, agentClient.Close()) ··· 342 342 require.NoError(t, err) 343 343 344 344 // Verify data exists in HSM 345 - agentClient, err := agentManager.CreateSingleGRPCClient(ctx, "test-hsm-device", "default", logger) 345 + agentClient, err := agentManager.CreateGRPCClient(ctx, "test-hsm-device", "default", logger) 346 346 require.NoError(t, err) 347 347 defer func() { 348 348 assert.NoError(t, agentClient.Close())
+5
internal/hsm/pkcs11_client.go
··· 691 691 692 692 label := string(labelAttr[0].Value) 693 693 694 + // Skip metadata objects when listing secrets 695 + if strings.HasSuffix(label, metadataKeySuffix) { 696 + continue 697 + } 698 + 694 699 // Extract the base path (remove key suffix) 695 700 path := label 696 701 if strings.Contains(label, "/") {
+7 -7
internal/mirror/manager.go
··· 32 32 33 33 // AgentManagerInterface defines the interface for HSM agent management used by mirror 34 34 type AgentManagerInterface interface { 35 - CreateSingleGRPCClient(ctx context.Context, deviceName, namespace string, logger logr.Logger) (hsm.Client, error) 35 + CreateGRPCClient(ctx context.Context, deviceName, namespace string, logger logr.Logger) (hsm.Client, error) 36 36 } 37 37 38 38 // MirrorManager handles multi-device HSM mirroring and conflict resolution ··· 127 127 logger.Info("Checking device for secrets", "device", deviceName, "secretCount", len(secretPaths)) 128 128 129 129 // Create gRPC client for this device (agents are in operator namespace) 130 - grpcClient, err := mm.agentManager.CreateSingleGRPCClient(ctx, deviceName, operatorNamespace, logger) 130 + grpcClient, err := mm.agentManager.CreateGRPCClient(ctx, deviceName, operatorNamespace, logger) 131 131 if err != nil { 132 132 logger.Error(err, "Failed to create gRPC client", "device", deviceName) 133 133 // Mark all secrets as having an error on this device ··· 527 527 528 528 // readSecretWithMetadata reads both secret data and metadata from a device 529 529 func (mm *MirrorManager) readSecretWithMetadata(ctx context.Context, deviceName, secretPath, namespace string, logger logr.Logger) (hsm.SecretData, *hsm.SecretMetadata, error) { 530 - grpcClient, err := mm.agentManager.CreateSingleGRPCClient(ctx, deviceName, namespace, logger) 530 + grpcClient, err := mm.agentManager.CreateGRPCClient(ctx, deviceName, namespace, logger) 531 531 if err != nil { 532 532 return nil, nil, fmt.Errorf("failed to create gRPC client: %w", err) 533 533 } ··· 559 559 560 560 // writeSecretWithMetadata writes both secret data and metadata to a device 561 561 func (mm *MirrorManager) writeSecretWithMetadata(ctx context.Context, deviceName, secretPath string, data hsm.SecretData, metadata *hsm.SecretMetadata, namespace string, logger logr.Logger) error { 562 - grpcClient, err := mm.agentManager.CreateSingleGRPCClient(ctx, deviceName, namespace, logger) 562 + grpcClient, err := mm.agentManager.CreateGRPCClient(ctx, deviceName, namespace, logger) 563 563 if err != nil { 564 564 return fmt.Errorf("failed to create gRPC client: %w", err) 565 565 } ··· 583 583 584 584 // writeMetadataOnly updates only the metadata for an existing secret 585 585 func (mm *MirrorManager) writeMetadataOnly(ctx context.Context, deviceName, secretPath string, metadata *hsm.SecretMetadata, namespace string, logger logr.Logger) error { 586 - grpcClient, err := mm.agentManager.CreateSingleGRPCClient(ctx, deviceName, namespace, logger) 586 + grpcClient, err := mm.agentManager.CreateGRPCClient(ctx, deviceName, namespace, logger) 587 587 if err != nil { 588 588 return fmt.Errorf("failed to create gRPC client: %w", err) 589 589 } ··· 688 688 for _, deviceName := range devices { 689 689 deviceLogger := logger.WithValues("device", deviceName) 690 690 691 - hsmClient, err := mm.agentManager.CreateSingleGRPCClient(ctx, deviceName, operatorNamespace, deviceLogger) 691 + hsmClient, err := mm.agentManager.CreateGRPCClient(ctx, deviceName, operatorNamespace, deviceLogger) 692 692 if err != nil { 693 693 deviceLogger.Info("Failed to connect to device for discovery, skipping", "error", err) 694 694 continue ··· 767 767 if len(devices) > 0 { 768 768 // Try to connect to at least one device to verify agents are actually ready 769 769 for _, deviceName := range devices { 770 - grpcClient, err := mm.agentManager.CreateSingleGRPCClient(ctx, deviceName, mm.operatorNamespace, logger) 770 + grpcClient, err := mm.agentManager.CreateGRPCClient(ctx, deviceName, mm.operatorNamespace, logger) 771 771 if err != nil { 772 772 logger.V(1).Info("Agent not ready yet", "device", deviceName, "error", err) 773 773 continue
+1 -1
internal/mirror/manager_test.go
··· 32 32 // MockAgentManager is a mock implementation of AgentManagerInterface for testing 33 33 type MockAgentManager struct{} 34 34 35 - func (m *MockAgentManager) CreateSingleGRPCClient(ctx context.Context, deviceName, namespace string, logger logr.Logger) (hsm.Client, error) { 35 + func (m *MockAgentManager) CreateGRPCClient(ctx context.Context, deviceName, namespace string, logger logr.Logger) (hsm.Client, error) { 36 36 // Return a mock client for testing 37 37 return hsm.NewMockClient(), nil 38 38 }
+1 -1
internal/modes/manager/manager.go
··· 331 331 332 332 // Start API server if enabled 333 333 if enableAPI { 334 - apiServer := api.NewServer(mgr.GetClient(), agentManager, ctrl.Log.WithName("api")) 334 + apiServer := api.NewServer(mgr.GetClient(), agentManager, operatorNamespace, ctrl.Log.WithName("api")) 335 335 336 336 // Start API server in a separate goroutine 337 337 go func() {