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.

implement image utils to fetch first from envvar and then from the manager's image before defaulting to a hardcoded

+132 -83
+11 -6
.claude/settings.json
··· 3 3 "allow": [ 4 4 "Write(*)", 5 5 "Edit(*)", 6 + "Bash(curl:*)", 7 + "Bash(grep:*)", 8 + "Bash(gofmt:*)", 9 + "Bash(golangci-lint run:*)", 10 + "Bash(go run:*)", 11 + "Bash(go install:*)", 12 + "Bash(go test:*)", 13 + "Bash(go build:*)", 14 + "Bash(go vet:*)", 6 15 "Bash(helm lint:*)", 16 + "Bash(helm template:*)", 7 17 "Bash(kubectl get:*)", 8 18 "Bash(kubectl describe:*)", 9 19 "Bash(kubectl exec:*)", 10 - "Bash(grep:*)", 11 20 "Bash(kubectl logs:*)", 12 - "Bash(make:*)", 13 - "Bash(gofmt:*)", 14 - "Bash(golangci-lint run:*)", 15 - "Bash(go run:*)", 16 - "Bash(go install:*)" 21 + "Bash(make:*)" 17 22 ], 18 23 "deny": [], 19 24 "ask": []
+5 -8
cmd/manager/main.go
··· 219 219 220 220 // HSM client registration removed - now handled by agent architecture 221 221 222 - // Create agent manager 223 - agentImage := os.Getenv("AGENT_IMAGE") 224 - if agentImage == "" { 225 - agentImage = "hsm-secrets-operator:latest" // Default to same image as manager 226 - } 227 222 228 223 // Agent manager will detect the current namespace automatically 229 - agentManager := agent.NewManager(mgr.GetClient(), agentImage, "") 224 + imageResolver := controller.NewImageResolver(mgr.GetClient()) 225 + agentManager := agent.NewManager(mgr.GetClient(), "", imageResolver) 230 226 231 227 // Set up HSMPool controller to aggregate discovery reports from pod annotations 232 228 if err := (&controller.HSMPoolReconciler{ ··· 260 256 261 257 // Set up discovery DaemonSet controller (manager-owned) 262 258 if err := (&controller.DiscoveryDaemonSetReconciler{ 263 - Client: mgr.GetClient(), 264 - Scheme: mgr.GetScheme(), 259 + Client: mgr.GetClient(), 260 + Scheme: mgr.GetScheme(), 261 + ImageResolver: imageResolver, 265 262 }).SetupWithManager(mgr); err != nil { 266 263 setupLog.Error(err, "unable to create controller", "controller", "DiscoveryDaemonSet") 267 264 os.Exit(1)
+12 -9
internal/agent/deployment.go
··· 45 45 // AgentNamePrefix is the prefix for HSM agent deployment names 46 46 AgentNamePrefix = "hsm-agent" 47 47 48 - // AgentImage is the container image for HSM agents 49 - AgentImage = "hsm-secrets-operator:latest" 50 - 51 48 // AgentPort is the port the HSM agent serves on 52 49 AgentPort = 8092 53 50 ··· 60 57 client.Client 61 58 AgentImage string 62 59 AgentNamespace string 60 + ImageResolver ImageResolver 61 + } 62 + 63 + // ImageResolver interface for dependency injection 64 + type ImageResolver interface { 65 + GetImage(ctx context.Context, defaultImage string) string 63 66 } 64 67 65 68 // NewManager creates a new agent manager 66 - func NewManager(k8sClient client.Client, agentImage, namespace string) *Manager { 67 - if agentImage == "" { 68 - agentImage = AgentImage 69 - } 69 + func NewManager(k8sClient client.Client, namespace string, imageResolver ImageResolver) *Manager { 70 70 71 71 m := &Manager{ 72 72 Client: k8sClient, 73 - AgentImage: agentImage, 74 73 AgentNamespace: namespace, 74 + ImageResolver: imageResolver, 75 75 } 76 76 77 77 // If no namespace provided, agents will be deployed in the same namespace as their HSMDevice ··· 209 209 return fmt.Errorf("no target node found for HSM device %s", hsmDevice.Name) 210 210 } 211 211 212 + // Get discovery image from environment, manager image, or use default 213 + agentImage := m.ImageResolver.GetImage(ctx, "AGENT_IMAGE") 214 + 212 215 deployment := &appsv1.Deployment{ 213 216 ObjectMeta: metav1.ObjectMeta{ 214 217 Name: agentName, ··· 274 277 Containers: []corev1.Container{ 275 278 { 276 279 Name: "agent", 277 - Image: m.AgentImage, 280 + Image: agentImage, 278 281 Command: []string{ 279 282 "/entrypoint.sh", 280 283 "agent",
+3 -51
internal/controller/discovery_daemonset_controller.go
··· 19 19 import ( 20 20 "context" 21 21 "fmt" 22 - "os" 23 22 "time" 24 23 25 24 appsv1 "k8s.io/api/apps/v1" ··· 42 41 // DiscoveryDaemonSetReconciler manages discovery DaemonSets for HSMDevice resources 43 42 type DiscoveryDaemonSetReconciler struct { 44 43 client.Client 45 - Scheme *runtime.Scheme 44 + Scheme *runtime.Scheme 45 + ImageResolver *ImageResolver 46 46 } 47 47 48 48 // +kubebuilder:rbac:groups=apps,resources=daemonsets,verbs=get;list;watch;create;update;patch;delete ··· 167 167 daemonSetName := fmt.Sprintf("%s-discovery", hsmDevice.Name) 168 168 169 169 // Get discovery image from environment, manager image, or use default 170 - discoveryImage := r.getDiscoveryImage(ctx) 170 + discoveryImage := r.ImageResolver.GetImage(ctx, "DISCOVERY_IMAGE") 171 171 172 172 // Define the desired DaemonSet 173 173 desired := &appsv1.DaemonSet{ ··· 354 354 } 355 355 356 356 return ctrl.Result{}, nil 357 - } 358 - 359 - // getDiscoveryImage determines the discovery image to use 360 - func (r *DiscoveryDaemonSetReconciler) getDiscoveryImage(ctx context.Context) string { 361 - // Try environment variable first 362 - if discoveryImage := os.Getenv("DISCOVERY_IMAGE"); discoveryImage != "" { 363 - return discoveryImage 364 - } 365 - 366 - // Try to detect the manager's running image as fallback 367 - if managerImage := r.getManagerImage(ctx); managerImage != "" { 368 - return managerImage 369 - } 370 - 371 - // Last resort: use default 372 - return "hsm-discovery:latest" 373 - } 374 - 375 - // getManagerImage attempts to detect the manager's running image 376 - func (r *DiscoveryDaemonSetReconciler) getManagerImage(ctx context.Context) string { 377 - logger := log.FromContext(ctx) 378 - 379 - // Get manager deployment by looking for deployments with manager labels 380 - deployments := &appsv1.DeploymentList{} 381 - listOpts := []client.ListOption{ 382 - client.MatchingLabels{ 383 - "app.kubernetes.io/name": "hsm-secrets-operator", 384 - "app.kubernetes.io/component": "manager", 385 - }, 386 - } 387 - 388 - if err := r.List(ctx, deployments, listOpts...); err != nil { 389 - logger.V(1).Info("Failed to list manager deployments for image detection", "error", err) 390 - return "" 391 - } 392 - 393 - // Find the manager container and extract its image 394 - for _, deployment := range deployments.Items { 395 - for _, container := range deployment.Spec.Template.Spec.Containers { 396 - if container.Name == "manager" { 397 - logger.V(1).Info("Detected manager image for discovery DaemonSet", "image", container.Image) 398 - return container.Image 399 - } 400 - } 401 - } 402 - 403 - logger.V(1).Info("Could not detect manager image") 404 - return "" 405 357 } 406 358 407 359 // findDevicesForDaemonSet maps discovery DaemonSets back to HSMDevices for reconciliation
+12 -8
internal/controller/discovery_daemonset_controller_test.go
··· 93 93 94 94 By("Reconciling the HSMDevice") 95 95 reconciler := &DiscoveryDaemonSetReconciler{ 96 - Client: k8sClient, 97 - Scheme: k8sClient.Scheme(), 96 + Client: k8sClient, 97 + Scheme: k8sClient.Scheme(), 98 + ImageResolver: NewImageResolver(k8sClient), 98 99 } 99 100 100 101 _, err := reconciler.Reconcile(ctx, ctrl.Request{ ··· 191 192 192 193 By("Reconciling to create initial DaemonSet") 193 194 reconciler := &DiscoveryDaemonSetReconciler{ 194 - Client: k8sClient, 195 - Scheme: k8sClient.Scheme(), 195 + Client: k8sClient, 196 + Scheme: k8sClient.Scheme(), 197 + ImageResolver: NewImageResolver(k8sClient), 196 198 } 197 199 198 200 _, err := reconciler.Reconcile(ctx, ctrl.Request{ ··· 250 252 251 253 By("Reconciling to create DaemonSet") 252 254 reconciler := &DiscoveryDaemonSetReconciler{ 253 - Client: k8sClient, 254 - Scheme: k8sClient.Scheme(), 255 + Client: k8sClient, 256 + Scheme: k8sClient.Scheme(), 257 + ImageResolver: NewImageResolver(k8sClient), 255 258 } 256 259 257 260 _, err := reconciler.Reconcile(ctx, ctrl.Request{ ··· 305 308 306 309 By("Reconciling the HSMDevice") 307 310 reconciler := &DiscoveryDaemonSetReconciler{ 308 - Client: k8sClient, 309 - Scheme: k8sClient.Scheme(), 311 + Client: k8sClient, 312 + Scheme: k8sClient.Scheme(), 313 + ImageResolver: NewImageResolver(k8sClient), 310 314 } 311 315 312 316 _, err := reconciler.Reconcile(ctx, ctrl.Request{
+2 -1
internal/controller/hsmpool_agent_controller_test.go
··· 129 129 Expect(k8sClient.Status().Update(ctx, hsmPool)).To(Succeed()) 130 130 131 131 // Create agent manager 132 - agentManager = agent.NewManager(k8sClient, "test-agent:latest", hsmPoolNamespace) 132 + imageResolver := NewImageResolver(k8sClient) 133 + agentManager = agent.NewManager(k8sClient, hsmPoolNamespace, imageResolver) 133 134 }) 134 135 135 136 AfterEach(func() {
+87
internal/controller/image_utils.go
··· 1 + /* 2 + Copyright 2025. 3 + 4 + Licensed under the Apache License, Version 2.0 (the "License"); 5 + you may not use this file except in compliance with the License. 6 + You may obtain a copy of the License at 7 + 8 + http://www.apache.org/licenses/LICENSE-2.0 9 + 10 + Unless required by applicable law or agreed to in writing, software 11 + distributed under the License is distributed on an "AS IS" BASIS, 12 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 + See the License for the specific language governing permissions and 14 + limitations under the License. 15 + */ 16 + 17 + package controller 18 + 19 + import ( 20 + "context" 21 + "os" 22 + 23 + appsv1 "k8s.io/api/apps/v1" 24 + "sigs.k8s.io/controller-runtime/pkg/client" 25 + "sigs.k8s.io/controller-runtime/pkg/log" 26 + ) 27 + 28 + const defaultImage = "ghcr.io/evanjarrett/hsm-secrets-operator:latest" 29 + 30 + // ImageResolver provides functionality to resolve container images 31 + type ImageResolver struct { 32 + Client client.Client 33 + } 34 + 35 + // NewImageResolver creates a new ImageResolver 36 + func NewImageResolver(k8sClient client.Client) *ImageResolver { 37 + return &ImageResolver{ 38 + Client: k8sClient, 39 + } 40 + } 41 + 42 + // GetManagerImage attempts to detect the manager's running image by looking for manager deployments 43 + func (r *ImageResolver) GetManagerImage(ctx context.Context) string { 44 + logger := log.FromContext(ctx) 45 + 46 + // Get manager deployment by looking for deployments with manager labels 47 + deployments := &appsv1.DeploymentList{} 48 + listOpts := []client.ListOption{ 49 + client.MatchingLabels{ 50 + "app.kubernetes.io/name": "hsm-secrets-operator", 51 + "app.kubernetes.io/component": "manager", 52 + }, 53 + } 54 + 55 + if err := r.Client.List(ctx, deployments, listOpts...); err != nil { 56 + logger.V(1).Info("Failed to list manager deployments for image detection", "error", err) 57 + return "" 58 + } 59 + 60 + // Find the manager container and extract its image 61 + for _, deployment := range deployments.Items { 62 + for _, container := range deployment.Spec.Template.Spec.Containers { 63 + if container.Name == "manager" { 64 + logger.V(1).Info("Detected manager image", "image", container.Image) 65 + return container.Image 66 + } 67 + } 68 + } 69 + 70 + logger.V(1).Info("Could not detect manager image") 71 + return "" 72 + } 73 + 74 + func (r *ImageResolver) GetImage(ctx context.Context, env string) string { 75 + // Try environment variable first 76 + if discoveryImage := os.Getenv(env); discoveryImage != "" { 77 + return discoveryImage 78 + } 79 + 80 + // Try to detect the manager's running image as fallback 81 + if managerImage := r.GetManagerImage(ctx); managerImage != "" { 82 + return managerImage 83 + } 84 + 85 + // last resort 86 + return defaultImage 87 + }