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.

at main 624 lines 21 kB view raw
1/* 2Copyright 2025. 3 4Licensed under the Apache License, Version 2.0 (the "License"); 5you may not use this file except in compliance with the License. 6You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10Unless required by applicable law or agreed to in writing, software 11distributed under the License is distributed on an "AS IS" BASIS, 12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13See the License for the specific language governing permissions and 14limitations under the License. 15*/ 16 17package controller 18 19import ( 20 "context" 21 "fmt" 22 "maps" 23 "slices" 24 "strings" 25 "time" 26 27 corev1 "k8s.io/api/core/v1" 28 "k8s.io/apimachinery/pkg/api/errors" 29 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 30 "k8s.io/apimachinery/pkg/runtime" 31 "k8s.io/apimachinery/pkg/types" 32 ctrl "sigs.k8s.io/controller-runtime" 33 "sigs.k8s.io/controller-runtime/pkg/client" 34 "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 35 "sigs.k8s.io/controller-runtime/pkg/log" 36 37 hsmv1alpha1 "github.com/evanjarrett/hsm-secrets-operator/api/v1alpha1" 38 "github.com/evanjarrett/hsm-secrets-operator/internal/agent" 39 "github.com/evanjarrett/hsm-secrets-operator/internal/hsm" 40) 41 42const ( 43 // HSMSecretFinalizer is the finalizer used by the HSMSecret controller 44 HSMSecretFinalizer = "hsmsecret.hsm.j5t.io/finalizer" 45 46 // DefaultSyncInterval is the default sync interval in seconds 47 DefaultSyncInterval = 30 48 49 // StartupGracePeriod is the duration during which "no agents available" is logged at Info level 50 StartupGracePeriod = 2 * time.Minute 51) 52 53// HSMSecretReconciler reconciles a HSMSecret object 54type HSMSecretReconciler struct { 55 client.Client 56 Scheme *runtime.Scheme 57 AgentManager *agent.Manager 58 OperatorNamespace string 59 OperatorName string 60 StartupTime time.Time 61} 62 63// HSMDeviceClients holds multiple HSM devices and their clients 64type HSMDeviceClients struct { 65 Devices []*hsmv1alpha1.HSMDevice 66 Clients []hsm.Client 67} 68 69// Close closes all clients 70func (hdc *HSMDeviceClients) Close() error { 71 var errs []error 72 for _, hsmClient := range hdc.Clients { 73 if hsmClient != nil { 74 if err := hsmClient.Close(); err != nil { 75 errs = append(errs, err) 76 } 77 } 78 } 79 if len(errs) > 0 { 80 return fmt.Errorf("failed to close %d clients: %v", len(errs), errs) 81 } 82 return nil 83} 84 85// DeviceInfo holds device data and metadata for version-based conflict resolution 86type DeviceInfo struct { 87 Data hsm.SecretData 88 Metadata *hsm.SecretMetadata 89 Version int64 90 Checksum string 91 Timestamp time.Time 92} 93 94// +kubebuilder:rbac:groups=hsm.j5t.io,resources=hsmsecrets,verbs=get;list;watch;create;update;patch;delete 95// +kubebuilder:rbac:groups=hsm.j5t.io,resources=hsmsecrets/status,verbs=get;update;patch 96// +kubebuilder:rbac:groups=hsm.j5t.io,resources=hsmsecrets/finalizers,verbs=update 97// +kubebuilder:rbac:groups=hsm.j5t.io,resources=hsmdevices,verbs=get;list;watch 98// +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;create;update;patch;delete 99// +kubebuilder:rbac:groups="",resources=events,verbs=create;patch 100// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete 101// +kubebuilder:rbac:groups="",resources=services,verbs=get;list;watch;create;update;patch;delete 102// +kubebuilder:rbac:groups=authentication.k8s.io,resources=tokenreviews,verbs=create 103 104// Reconcile handles HSMSecret reconciliation 105func (r *HSMSecretReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { 106 logger := log.FromContext(ctx) 107 108 // Fetch the HSMSecret instance 109 var hsmSecret hsmv1alpha1.HSMSecret 110 if err := r.Get(ctx, req.NamespacedName, &hsmSecret); err != nil { 111 if errors.IsNotFound(err) { 112 logger.Info("HSMSecret resource not found, ignoring since object must be deleted") 113 return ctrl.Result{}, nil 114 } 115 logger.Error(err, "Failed to get HSMSecret") 116 return ctrl.Result{}, err 117 } 118 119 // Check if this HSMSecret should be handled by this operator instance 120 if !r.shouldHandleSecret(&hsmSecret) { 121 logger.V(1).Info("HSMSecret not assigned to this operator instance, skipping", 122 "secret", hsmSecret.Name, 123 "namespace", hsmSecret.Namespace, 124 "operatorName", r.OperatorName, 125 "operatorNamespace", r.OperatorNamespace) 126 return ctrl.Result{}, nil 127 } 128 129 // Handle deletion 130 if hsmSecret.DeletionTimestamp != nil { 131 return ctrl.Result{}, r.reconcileDelete(ctx, &hsmSecret) 132 } 133 134 // Add finalizer if not present 135 if !controllerutil.ContainsFinalizer(&hsmSecret, HSMSecretFinalizer) { 136 controllerutil.AddFinalizer(&hsmSecret, HSMSecretFinalizer) 137 if err := r.updateWithRetry(ctx, &hsmSecret); err != nil { 138 logger.Error(err, "Failed to add finalizer") 139 return ctrl.Result{}, err 140 } 141 return ctrl.Result{Requeue: true}, nil 142 } 143 144 // Find available devices via AgentManager 145 devices, err := r.AgentManager.GetAvailableDevices(ctx, r.OperatorNamespace) 146 if err != nil { 147 // During startup grace period, log at Info level to reduce noise 148 if time.Since(r.StartupTime) < StartupGracePeriod { 149 logger.Info("No HSM agents available yet (startup grace period)", "error", err.Error(), "elapsed", time.Since(r.StartupTime).Round(time.Second)) 150 } else { 151 logger.Error(err, "Failed to get available devices") 152 } 153 return ctrl.Result{RequeueAfter: time.Minute * 2}, nil 154 } 155 if len(devices) == 0 { 156 // During startup grace period, log at Info level to reduce noise 157 if time.Since(r.StartupTime) < StartupGracePeriod { 158 logger.Info("No HSM devices available yet (startup grace period)", "elapsed", time.Since(r.StartupTime).Round(time.Second)) 159 } else { 160 logger.Info("No HSM devices available") 161 } 162 return ctrl.Result{RequeueAfter: time.Minute * 2}, nil 163 } 164 165 device := devices[0] 166 grpcClient, err := r.AgentManager.CreateGRPCClient(ctx, device, logger) 167 if err != nil { 168 logger.Error(err, "Failed to create gRPC client", "node", device.NodeName) 169 return ctrl.Result{RequeueAfter: time.Minute * 2}, nil 170 } 171 defer func() { 172 if err := grpcClient.Close(); err != nil { 173 logger.Error(err, "Failed to close gRPC client") 174 } 175 }() 176 177 // Check if secret exists 178 secrets, err := grpcClient.ListSecrets(ctx, "") 179 if err != nil { 180 logger.Error(err, "Failed to list secrets") 181 return ctrl.Result{RequeueAfter: time.Minute * 2}, nil 182 } 183 184 if !slices.Contains(secrets, hsmSecret.Name) { 185 logger.Info("Secret not found in HSM", "secret", hsmSecret.Name) 186 return ctrl.Result{RequeueAfter: time.Minute * 5}, nil 187 } 188 189 // Read secret data and metadata 190 hsmData, err := grpcClient.ReadSecret(ctx, hsmSecret.Name) 191 if err != nil { 192 logger.Error(err, "Failed to read secret from HSM") 193 return ctrl.Result{RequeueAfter: time.Minute * 2}, nil 194 } 195 196 hsmMetadata, err := grpcClient.ReadMetadata(ctx, hsmSecret.Name) 197 if err != nil { 198 logger.V(1).Info("Failed to read metadata", "error", err) 199 hsmMetadata = nil // Continue without metadata 200 } 201 202 // Reconcile the HSMSecret with the data from HSM 203 result, err := r.reconcileSecret(ctx, &hsmSecret, hsmData, hsmMetadata) 204 if err != nil { 205 logger.Error(err, "Failed to reconcile HSMSecret") 206 r.updateStatus(ctx, &hsmSecret, hsmv1alpha1.SyncStatusError, err.Error()) 207 } 208 209 return result, err 210} 211 212// reconcileSecret handles HSM secret reconciliation with data from HSM 213func (r *HSMSecretReconciler) reconcileSecret(ctx context.Context, hsmSecret *hsmv1alpha1.HSMSecret, hsmData hsm.SecretData, hsmMetadata *hsm.SecretMetadata) (ctrl.Result, error) { 214 // Set default values 215 secretName := hsmSecret.Spec.SecretName 216 if secretName == "" { 217 secretName = hsmSecret.Name 218 } 219 220 syncInterval := hsmSecret.Spec.SyncInterval 221 if syncInterval == 0 { 222 syncInterval = DefaultSyncInterval 223 } 224 225 return r.updateKubernetesSecret(ctx, hsmSecret, secretName, hsmData, hsmMetadata, syncInterval) 226} 227 228// updateKubernetesSecret updates the Kubernetes Secret with the given data 229func (r *HSMSecretReconciler) updateKubernetesSecret(ctx context.Context, hsmSecret *hsmv1alpha1.HSMSecret, secretName string, hsmData hsm.SecretData, hsmMetadata *hsm.SecretMetadata, syncInterval int32) (ctrl.Result, error) { 230 logger := log.FromContext(ctx) 231 232 // Calculate HSM checksum 233 hsmChecksum := hsm.CalculateChecksum(hsmData) 234 235 // Get or create Kubernetes Secret 236 var k8sSecret corev1.Secret 237 secretKey := types.NamespacedName{ 238 Namespace: hsmSecret.Namespace, 239 Name: secretName, 240 } 241 242 err := r.Get(ctx, secretKey, &k8sSecret) 243 if err != nil { 244 if errors.IsNotFound(err) { 245 // Create new secret 246 k8sSecret = r.buildSecret(hsmSecret, secretName, hsmData, hsmMetadata) 247 if err := r.Create(ctx, &k8sSecret); err != nil { 248 logger.Error(err, "Failed to create Secret") 249 return ctrl.Result{}, err 250 } 251 logger.Info("Created new Secret", "secret", secretKey) 252 } else { 253 logger.Error(err, "Failed to get Secret") 254 return ctrl.Result{}, err 255 } 256 } else { 257 // Update existing secret if needed 258 r.updateSecretWithMetadata(&k8sSecret, hsmSecret, hsmData, hsmMetadata) 259 if err := r.Update(ctx, &k8sSecret); err != nil { 260 logger.Error(err, "Failed to update Secret") 261 return ctrl.Result{}, err 262 } 263 logger.V(1).Info("Updated existing Secret", "secret", secretKey) 264 } 265 266 // Calculate K8s Secret checksum 267 secretChecksum := hsm.CalculateChecksum(r.convertSecretDataToHSMData(k8sSecret.Data)) 268 269 // Update status 270 syncStatus := hsmv1alpha1.SyncStatusInSync 271 if hsmChecksum != secretChecksum { 272 syncStatus = hsmv1alpha1.SyncStatusOutOfSync 273 } 274 275 r.updateStatus(ctx, hsmSecret, syncStatus, "") 276 hsmSecret.Status.HSMChecksum = hsmChecksum 277 hsmSecret.Status.SecretChecksum = secretChecksum 278 hsmSecret.Status.SecretRef = &corev1.ObjectReference{ 279 APIVersion: "v1", 280 Kind: "Secret", 281 Name: k8sSecret.Name, 282 Namespace: k8sSecret.Namespace, 283 UID: k8sSecret.UID, 284 } 285 286 if err := r.updateStatusWithRetry(ctx, hsmSecret); err != nil { 287 logger.Error(err, "Failed to update HSMSecret status") 288 return ctrl.Result{}, err 289 } 290 291 // Schedule next sync if AutoSync is enabled 292 if hsmSecret.Spec.AutoSync { 293 return ctrl.Result{RequeueAfter: time.Second * time.Duration(syncInterval)}, nil 294 } 295 296 return ctrl.Result{}, nil 297} 298 299// reconcileDelete handles HSMSecret deletion 300func (r *HSMSecretReconciler) reconcileDelete(ctx context.Context, hsmSecret *hsmv1alpha1.HSMSecret) error { 301 logger := log.FromContext(ctx) 302 303 if controllerutil.ContainsFinalizer(hsmSecret, HSMSecretFinalizer) { 304 logger.Info("Cleaning up HSMSecret resources") 305 306 // Optionally delete the Kubernetes Secret 307 secretName := hsmSecret.Spec.SecretName 308 if secretName == "" { 309 secretName = hsmSecret.Name 310 } 311 312 secretKey := types.NamespacedName{ 313 Namespace: hsmSecret.Namespace, 314 Name: secretName, 315 } 316 317 var k8sSecret corev1.Secret 318 if err := r.Get(ctx, secretKey, &k8sSecret); err == nil { 319 if err := r.Delete(ctx, &k8sSecret); err != nil { 320 logger.Error(err, "Failed to delete associated Secret") 321 return err 322 } 323 logger.Info("Deleted associated Secret", "secret", secretKey) 324 } 325 326 // Remove finalizer 327 controllerutil.RemoveFinalizer(hsmSecret, HSMSecretFinalizer) 328 if err := r.updateWithRetry(ctx, hsmSecret); err != nil { 329 logger.Error(err, "Failed to remove finalizer") 330 return err 331 } 332 } 333 334 return nil 335} 336 337// buildSecret creates a new Kubernetes Secret from HSM data and metadata 338func (r *HSMSecretReconciler) buildSecret(hsmSecret *hsmv1alpha1.HSMSecret, secretName string, hsmData hsm.SecretData, hsmMetadata *hsm.SecretMetadata) corev1.Secret { 339 secretType := hsmSecret.Spec.SecretType 340 if secretType == "" { 341 secretType = corev1.SecretTypeOpaque 342 } 343 344 // Build labels starting with default operator labels 345 labels := map[string]string{ 346 "managed-by": "hsm-secrets-operator", 347 "hsm-path": strings.ReplaceAll(hsmSecret.Name, "/", "_"), 348 } 349 350 // Build annotations starting with empty map 351 annotations := make(map[string]string) 352 353 // Add metadata labels and annotations if metadata exists 354 r.applyMetadataToLabelsAndAnnotations(labels, annotations, hsmMetadata) 355 356 secret := corev1.Secret{ 357 ObjectMeta: metav1.ObjectMeta{ 358 Name: secretName, 359 Namespace: hsmSecret.Namespace, 360 Labels: labels, 361 Annotations: annotations, 362 }, 363 Type: secretType, 364 Data: r.convertHSMDataToSecretData(hsmData), 365 } 366 367 // Set owner reference 368 if err := ctrl.SetControllerReference(hsmSecret, &secret, r.Scheme); err != nil { 369 ctrl.Log.Error(err, "Failed to set owner reference") 370 } 371 372 return secret 373} 374 375// updateSecretWithMetadata updates an existing Kubernetes Secret with HSM data and metadata 376func (r *HSMSecretReconciler) updateSecretWithMetadata(secret *corev1.Secret, hsmSecret *hsmv1alpha1.HSMSecret, hsmData hsm.SecretData, hsmMetadata *hsm.SecretMetadata) { 377 // Update data 378 secret.Data = r.convertHSMDataToSecretData(hsmData) 379 380 // Initialize labels if nil 381 if secret.Labels == nil { 382 secret.Labels = make(map[string]string) 383 } 384 385 // Initialize annotations if nil 386 if secret.Annotations == nil { 387 secret.Annotations = make(map[string]string) 388 } 389 390 // Ensure essential operator labels are present 391 secret.Labels["managed-by"] = "hsm-secrets-operator" 392 secret.Labels["hsm-path"] = strings.ReplaceAll(hsmSecret.Name, "/", "_") 393 394 // Apply metadata to labels and annotations 395 r.applyMetadataToLabelsAndAnnotations(secret.Labels, secret.Annotations, hsmMetadata) 396} 397 398// applyMetadataToLabelsAndAnnotations applies HSM metadata to Kubernetes labels and annotations 399func (r *HSMSecretReconciler) applyMetadataToLabelsAndAnnotations(labels map[string]string, annotations map[string]string, hsmMetadata *hsm.SecretMetadata) { 400 if hsmMetadata == nil { 401 return 402 } 403 404 // Apply metadata labels directly to Kubernetes labels 405 if hsmMetadata.Labels != nil { 406 for key, value := range hsmMetadata.Labels { 407 // Validate Kubernetes label format 408 if r.isValidKubernetesLabelKey(key) && r.isValidKubernetesLabelValue(value) { 409 labels[key] = value 410 } 411 } 412 } 413 414 // Apply other metadata fields as annotations with hsm.j5t.io prefix 415 if hsmMetadata.Description != "" { 416 annotations["hsm.j5t.io/description"] = hsmMetadata.Description 417 } 418 if hsmMetadata.Format != "" { 419 annotations["hsm.j5t.io/format"] = hsmMetadata.Format 420 } 421 if hsmMetadata.DataType != "" { 422 annotations["hsm.j5t.io/data-type"] = hsmMetadata.DataType 423 } 424 if hsmMetadata.Source != "" { 425 annotations["hsm.j5t.io/source"] = hsmMetadata.Source 426 } 427 if hsmMetadata.CreatedAt != "" { 428 annotations["hsm.j5t.io/created-at"] = hsmMetadata.CreatedAt 429 } 430} 431 432// isValidKubernetesLabelKey validates a Kubernetes label key 433func (r *HSMSecretReconciler) isValidKubernetesLabelKey(key string) bool { 434 // Basic validation - more comprehensive validation could be added 435 if len(key) == 0 || len(key) > 63 { 436 return false 437 } 438 // Should start and end with alphanumeric, can contain alphanumeric, dash, underscore, and dot 439 for i, char := range key { 440 isAlphaNumeric := (char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') || (char >= '0' && char <= '9') 441 isAllowedSymbol := char == '-' || char == '_' || char == '.' 442 443 if !isAlphaNumeric && !isAllowedSymbol { 444 return false 445 } 446 if (i == 0 || i == len(key)-1) && !isAlphaNumeric { 447 return false 448 } 449 } 450 return true 451} 452 453// isValidKubernetesLabelValue validates a Kubernetes label value 454func (r *HSMSecretReconciler) isValidKubernetesLabelValue(value string) bool { 455 // Basic validation 456 if len(value) > 63 { 457 return false 458 } 459 if len(value) == 0 { 460 return true // Empty values are allowed 461 } 462 // Should start and end with alphanumeric, can contain alphanumeric, dash, underscore, and dot 463 for i, char := range value { 464 isAlphaNumeric := (char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') || (char >= '0' && char <= '9') 465 isAllowedSymbol := char == '-' || char == '_' || char == '.' 466 467 if !isAlphaNumeric && !isAllowedSymbol { 468 return false 469 } 470 if (i == 0 || i == len(value)-1) && !isAlphaNumeric { 471 return false 472 } 473 } 474 return true 475} 476 477// convertHSMDataToSecretData converts HSM data format to Kubernetes Secret data format 478func (r *HSMSecretReconciler) convertHSMDataToSecretData(hsmData hsm.SecretData) map[string][]byte { 479 result := make(map[string][]byte) 480 maps.Copy(result, hsmData) 481 return result 482} 483 484// convertSecretDataToHSMData converts Kubernetes Secret data format to HSM data format 485func (r *HSMSecretReconciler) convertSecretDataToHSMData(secretData map[string][]byte) hsm.SecretData { 486 result := make(hsm.SecretData) 487 maps.Copy(result, secretData) 488 return result 489} 490 491// updateWithRetry updates the HSMSecret with conflict retry logic 492func (r *HSMSecretReconciler) updateWithRetry(ctx context.Context, hsmSecret *hsmv1alpha1.HSMSecret) error { 493 logger := log.FromContext(ctx) 494 maxRetries := 3 495 496 for attempt := range maxRetries { 497 if err := r.Update(ctx, hsmSecret); err != nil { 498 if errors.IsConflict(err) && attempt < maxRetries-1 { 499 logger.V(1).Info("Update conflict, retrying", "attempt", attempt+1) 500 // Fresh read of the HSMSecret to get latest resourceVersion 501 var freshSecret hsmv1alpha1.HSMSecret 502 if err := r.Get(ctx, types.NamespacedName{Name: hsmSecret.Name, Namespace: hsmSecret.Namespace}, &freshSecret); err != nil { 503 if errors.IsNotFound(err) { 504 return nil // Resource was deleted 505 } 506 return fmt.Errorf("failed to get fresh HSMSecret: %w", err) 507 } 508 // Copy our changes to the fresh copy 509 freshSecret.Finalizers = hsmSecret.Finalizers 510 *hsmSecret = freshSecret 511 continue 512 } 513 return err 514 } 515 516 // Success 517 return nil 518 } 519 520 return fmt.Errorf("failed to update after %d attempts", maxRetries) 521} 522 523// updateStatusWithRetry updates the HSMSecret status with conflict retry logic 524func (r *HSMSecretReconciler) updateStatusWithRetry(ctx context.Context, hsmSecret *hsmv1alpha1.HSMSecret) error { 525 logger := log.FromContext(ctx) 526 maxRetries := 3 527 528 for attempt := range maxRetries { 529 // Fresh read of the HSMSecret to get latest resourceVersion 530 var freshSecret hsmv1alpha1.HSMSecret 531 if err := r.Get(ctx, types.NamespacedName{Name: hsmSecret.Name, Namespace: hsmSecret.Namespace}, &freshSecret); err != nil { 532 if errors.IsNotFound(err) { 533 return nil // Resource was deleted, no need to update status 534 } 535 return fmt.Errorf("failed to get fresh HSMSecret: %w", err) 536 } 537 538 // Copy the status from our working copy to the fresh copy 539 freshSecret.Status = hsmSecret.Status 540 541 // Attempt to update status 542 if err := r.Status().Update(ctx, &freshSecret); err != nil { 543 if errors.IsConflict(err) && attempt < maxRetries-1 { 544 logger.V(1).Info("Status update conflict, retrying", "attempt", attempt+1) 545 continue 546 } 547 return err 548 } 549 550 // Success 551 return nil 552 } 553 554 return fmt.Errorf("failed to update status after %d attempts", maxRetries) 555} 556 557// updateStatus updates the HSMSecret status 558func (r *HSMSecretReconciler) updateStatus(_ context.Context, hsmSecret *hsmv1alpha1.HSMSecret, status hsmv1alpha1.SyncStatus, errorMsg string) { 559 now := metav1.Now() 560 hsmSecret.Status.SyncStatus = status 561 hsmSecret.Status.LastError = errorMsg 562 563 if status == hsmv1alpha1.SyncStatusInSync { 564 hsmSecret.Status.LastSyncTime = &now 565 } 566 567 // Update conditions 568 condition := metav1.Condition{ 569 Type: "Ready", 570 Status: metav1.ConditionTrue, 571 LastTransitionTime: now, 572 Reason: string(status), 573 Message: errorMsg, 574 } 575 576 if status == hsmv1alpha1.SyncStatusError { 577 condition.Status = metav1.ConditionFalse 578 } 579 580 // Update or add condition 581 found := false 582 for i, cond := range hsmSecret.Status.Conditions { 583 if cond.Type == condition.Type { 584 hsmSecret.Status.Conditions[i] = condition 585 found = true 586 break 587 } 588 } 589 if !found { 590 hsmSecret.Status.Conditions = append(hsmSecret.Status.Conditions, condition) 591 } 592} 593 594// shouldHandleSecret determines if this operator instance should handle the given HSMSecret 595func (r *HSMSecretReconciler) shouldHandleSecret(hsmSecret *hsmv1alpha1.HSMSecret) bool { 596 // If no parentRef is present, ignore the secret (explicit association required) 597 if hsmSecret.Spec.ParentRef == nil { 598 return false 599 } 600 601 parentRef := hsmSecret.Spec.ParentRef 602 603 // Check if the parent name matches this operator 604 if parentRef.Name != r.OperatorName { 605 return false 606 } 607 608 // Check namespace match - if parentRef.Namespace is nil, assume same namespace as HSMSecret 609 expectedNamespace := r.OperatorNamespace 610 if parentRef.Namespace != nil { 611 expectedNamespace = *parentRef.Namespace 612 } 613 614 return expectedNamespace == r.OperatorNamespace 615} 616 617// SetupWithManager sets up the controller with the Manager. 618func (r *HSMSecretReconciler) SetupWithManager(mgr ctrl.Manager) error { 619 return ctrl.NewControllerManagedBy(mgr). 620 For(&hsmv1alpha1.HSMSecret{}). 621 Owns(&corev1.Secret{}). 622 Named("hsmsecret"). 623 Complete(r) 624}