A Kubernetes operator that bridges Hardware Security Module (HSM) data storage with Kubernetes Secrets, providing true secret portability th
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}