package shared import ( "context" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" appsv1 "k8s.io/api/apps/v1" batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" ) // K8sTestSuite tests Kubernetes-related utility functions type K8sTestSuite struct { suite.Suite } func TestK8sTestSuite(t *testing.T) { suite.Run(t, new(K8sTestSuite)) } // ProcessContainerNameTestCase represents a test case for ProcessContainerName type ProcessContainerNameTestCase struct { name string scenario TestScenario input string expectedImage string expectedTag string expectedSha string expectError bool errorContains string } // TestProcessContainerName tests the ProcessContainerName function with comprehensive scenarios func (s *K8sTestSuite) TestProcessContainerName() { testCases := []ProcessContainerNameTestCase{ // ========== GOOD SCENARIOS ========== // Standard Docker Hub images { name: "simple image name defaults to latest", scenario: ScenarioGood, input: "nginx", expectedImage: "nginx", expectedTag: "latest", expectedSha: "", expectError: false, }, { name: "image with latest tag", scenario: ScenarioGood, input: "nginx:latest", expectedImage: "nginx", expectedTag: "latest", expectedSha: "", expectError: false, }, { name: "image with version tag", scenario: ScenarioGood, input: "nginx:1.21.0", expectedImage: "nginx", expectedTag: "1.21.0", expectedSha: "", expectError: false, }, { name: "image with semver tag", scenario: ScenarioGood, input: "redis:6.2.6", expectedImage: "redis", expectedTag: "6.2.6", expectedSha: "", expectError: false, }, // Images with registry prefix { name: "gcr.io registry", scenario: ScenarioGood, input: "gcr.io/google-containers/pause:3.2", expectedImage: "gcr.io/google-containers/pause", expectedTag: "3.2", expectedSha: "", expectError: false, }, { name: "ghcr.io registry", scenario: ScenarioGood, input: "ghcr.io/owner/repo:v1.0.0", expectedImage: "ghcr.io/owner/repo", expectedTag: "v1.0.0", expectedSha: "", expectError: false, }, { name: "quay.io registry", scenario: ScenarioGood, input: "quay.io/coreos/etcd:v3.5.0", expectedImage: "quay.io/coreos/etcd", expectedTag: "v3.5.0", expectedSha: "", expectError: false, }, { name: "registry.k8s.io", scenario: ScenarioGood, input: "registry.k8s.io/pause:3.9", expectedImage: "registry.k8s.io/pause", expectedTag: "3.9", expectedSha: "", expectError: false, }, { name: "docker.io explicit registry", scenario: ScenarioGood, input: "docker.io/library/nginx:latest", expectedImage: "docker.io/library/nginx", expectedTag: "latest", expectedSha: "", expectError: false, }, // Private registry with port { name: "private registry with port", scenario: ScenarioGood, input: "myregistry.local:5000/myimage:v1", expectedImage: "myregistry.local", expectedTag: "5000/myimage:v1", // This is the current behavior expectedSha: "", expectError: false, }, // ========== NOT GOOD SCENARIOS ========== // SHA-only references (no tag) { name: "image with sha256 digest only", scenario: ScenarioNotGood, input: "nginx@sha256:abc123def456789012345678901234567890123456789012345678901234", expectedImage: "nginx", expectedTag: "", expectedSha: "sha256:abc123def456789012345678901234567890123456789012345678901234", expectError: false, }, { name: "registry image with sha only", scenario: ScenarioNotGood, input: "gcr.io/distroless/static@sha256:abcdef1234567890", expectedImage: "gcr.io/distroless/static", expectedTag: "", expectedSha: "sha256:abcdef1234567890", expectError: false, }, // Tag + SHA references (pinned images) { name: "cilium with tag and sha - real world example", scenario: ScenarioNotGood, input: "quay.io/cilium/cilium:v1.18.4@sha256:49d87af187eeeb9e9e3ec2bc6bd372261a0b5cb2d845659463ba7cc10fe9e45f", expectedImage: "quay.io/cilium/cilium", expectedTag: "v1.18.4", expectedSha: "sha256:49d87af187eeeb9e9e3ec2bc6bd372261a0b5cb2d845659463ba7cc10fe9e45f", expectError: false, }, { name: "distroless with tag and sha", scenario: ScenarioNotGood, input: "gcr.io/distroless/static:nonroot@sha256:abc123", expectedImage: "gcr.io/distroless/static", expectedTag: "nonroot", expectedSha: "sha256:abc123", expectError: false, }, // Complex nested paths { name: "deeply nested registry path", scenario: ScenarioNotGood, input: "us-docker.pkg.dev/project/repo/subdir/image:tag", expectedImage: "us-docker.pkg.dev/project/repo/subdir/image", expectedTag: "tag", expectedSha: "", expectError: false, }, // ========== REALLY BAD SCENARIOS ========== // Empty and invalid inputs { name: "empty string", scenario: ScenarioReallyBad, input: "", expectedImage: "", expectedTag: "", expectedSha: "", expectError: true, errorContains: "image name is required", }, { name: "only colon", scenario: ScenarioReallyBad, input: ":", expectedImage: "", expectedTag: "", expectedSha: "", expectError: true, errorContains: "image name is required", }, { name: "only at sign", scenario: ScenarioReallyBad, input: "@", expectedImage: "", expectedTag: "", expectedSha: "", expectError: true, errorContains: "invalid SHA format", }, // Invalid SHA formats { name: "invalid sha format - missing colon", scenario: ScenarioReallyBad, input: "nginx@sha256abc123", expectedImage: "", expectedTag: "", expectedSha: "", expectError: true, errorContains: "invalid SHA format", }, { name: "invalid sha format - wrong algorithm", scenario: ScenarioReallyBad, input: "nginx@md5:abc123", expectedImage: "", expectedTag: "", expectedSha: "", expectError: true, errorContains: "invalid SHA format", }, { name: "multiple @ symbols", scenario: ScenarioReallyBad, input: "nginx@sha256:abc@sha256:def", expectedImage: "", expectedTag: "", expectedSha: "", expectError: true, errorContains: "invalid container name format", }, // Edge cases for Kubernetes volatility { name: "k8s pause image", scenario: ScenarioGood, input: "registry.k8s.io/pause:3.9", expectedImage: "registry.k8s.io/pause", expectedTag: "3.9", expectedSha: "", expectError: false, }, { name: "coredns image", scenario: ScenarioGood, input: "registry.k8s.io/coredns/coredns:v1.11.1", expectedImage: "registry.k8s.io/coredns/coredns", expectedTag: "v1.11.1", expectedSha: "", expectError: false, }, { name: "etcd with sha pinning", scenario: ScenarioNotGood, input: "registry.k8s.io/etcd:3.5.12-0@sha256:abc123", expectedImage: "registry.k8s.io/etcd", expectedTag: "3.5.12-0", expectedSha: "sha256:abc123", expectError: false, }, } for _, tc := range testCases { s.Run(tc.name, func() { result, err := ProcessContainerName(tc.input) if tc.expectError { require.Error(s.T(), err, "Scenario: %s - Expected error for input: %s", tc.scenario, tc.input) if tc.errorContains != "" { assert.Contains(s.T(), err.Error(), tc.errorContains) } } else { require.NoError(s.T(), err, "Scenario: %s - Unexpected error for input: %s", tc.scenario, tc.input) assert.Equal(s.T(), tc.expectedImage, result.Image, "Image mismatch") assert.Equal(s.T(), tc.expectedTag, result.Tag, "Tag mismatch") assert.Equal(s.T(), tc.expectedSha, result.Sha, "SHA mismatch") // Verify FullName is preserved if tc.input != "" { assert.Equal(s.T(), tc.input, result.FullName, "FullName should match input") } } }) } } // TestProcessContainerNameCaching tests that the container cache works correctly func (s *K8sTestSuite) TestProcessContainerNameCaching() { // Clear the cache first by processing a unique image uniqueImage := "test-cache-" + time.Now().Format("20060102150405") // First call - should not be cached result1, err := ProcessContainerName(uniqueImage) require.NoError(s.T(), err) assert.Equal(s.T(), uniqueImage, result1.Image) assert.Equal(s.T(), "latest", result1.Tag) // Defaults to latest // Second call - should be cached result2, err := ProcessContainerName(uniqueImage) require.NoError(s.T(), err) assert.Equal(s.T(), result1, result2, "Cached result should match original") } // TestProcessContainerNameConcurrency tests thread safety of ProcessContainerName func (s *K8sTestSuite) TestProcessContainerNameConcurrency() { images := []string{ "nginx:latest", "redis:6", "postgres:14", "mysql:8", "mongo:5", } done := make(chan bool, len(images)*10) errors := make(chan error, len(images)*10) // Run 10 goroutines per image for i := 0; i < 10; i++ { for _, img := range images { go func(image string) { _, err := ProcessContainerName(image) if err != nil { errors <- err } done <- true }(img) } } // Wait for all goroutines for i := 0; i < len(images)*10; i++ { <-done } // Check for errors close(errors) for err := range errors { s.T().Errorf("Concurrent access error: %v", err) } } // TestContainerCacheOperations tests the ContainerCache directly func (s *K8sTestSuite) TestContainerCacheOperations() { cache := &ContainerCache{ cache: make(map[string]Container), } // Test Set and Get testContainer := Container{ Image: "test-image", Tag: "v1.0.0", FullName: "test-image:v1.0.0", } cache.Set("test-key", testContainer) retrieved, ok := cache.Get("test-key") assert.True(s.T(), ok, "Should find cached container") assert.Equal(s.T(), testContainer, retrieved) // Test Get non-existent key _, ok = cache.Get("non-existent") assert.False(s.T(), ok, "Should not find non-existent key") } // TestProcessContainerWithContext tests context cancellation func (s *K8sTestSuite) TestProcessContainerWithContext() { ctx, cancel := context.WithCancel(context.Background()) // Test with active context containersList := &ContainersList{} err := processContainer(ctx, "nginx:latest", "default", containersList) require.NoError(s.T(), err) assert.Len(s.T(), containersList.Containers, 1) // Test with cancelled context cancel() err = processContainer(ctx, "redis:6", "default", containersList) assert.Error(s.T(), err) assert.Equal(s.T(), context.Canceled, err) } // TestKubernetesVolatilityScenarios tests scenarios specific to Kubernetes environment volatility func (s *K8sTestSuite) TestKubernetesVolatilityScenarios() { testCases := []struct { name string description string input string expectError bool }{ // Pod restart scenarios - same image, different instance { name: "pod_restart_same_image", description: "When a pod restarts, the same image reference should parse identically", input: "nginx:1.21", expectError: false, }, // Rolling update scenarios { name: "rolling_update_new_tag", description: "During rolling update, new tag version", input: "myapp:v2.0.0", expectError: false, }, // Image pull scenarios { name: "image_pull_backoff_recovery", description: "Image that might have failed to pull initially", input: "private-registry.io/secure/image:latest", expectError: false, }, // Helm chart common patterns { name: "helm_chart_image_pattern", description: "Common Helm chart image reference pattern", input: "bitnami/postgresql:14.5.0-debian-11-r14", expectError: false, }, // Operator-managed images { name: "operator_managed_image", description: "Image managed by an operator with specific versioning", input: "quay.io/prometheus/prometheus:v2.45.0", expectError: false, }, // Init container images { name: "init_container_image", description: "Common init container image", input: "busybox:1.36", expectError: false, }, // Sidecar images { name: "sidecar_envoy_proxy", description: "Envoy sidecar proxy image", input: "envoyproxy/envoy:v1.28.0", expectError: false, }, // CSI driver images { name: "csi_driver_image", description: "CSI driver image with complex path", input: "registry.k8s.io/sig-storage/csi-provisioner:v3.6.0", expectError: false, }, // Admission webhook images { name: "admission_webhook_image", description: "Admission webhook controller image", input: "k8s.gcr.io/ingress-nginx/kube-webhook-certgen:v1.3.0", expectError: false, }, } for _, tc := range testCases { s.Run(tc.name, func() { result, err := ProcessContainerName(tc.input) if tc.expectError { assert.Error(s.T(), err, "Description: %s", tc.description) } else { assert.NoError(s.T(), err, "Description: %s", tc.description) assert.NotEmpty(s.T(), result.Image, "Image should not be empty") assert.Equal(s.T(), tc.input, result.FullName, "FullName should be preserved") } }) } } // TestImageParsingEdgeCases tests edge cases that might occur in real Kubernetes clusters func (s *K8sTestSuite) TestImageParsingEdgeCases() { s.Run("image with plus sign in tag", func() { // Some images use + in tags (e.g., build metadata) result, err := ProcessContainerName("myimage:v1.0.0+build123") require.NoError(s.T(), err) assert.Equal(s.T(), "myimage", result.Image) assert.Equal(s.T(), "v1.0.0+build123", result.Tag) }) s.Run("image with underscore in name", func() { result, err := ProcessContainerName("my_custom_image:latest") require.NoError(s.T(), err) assert.Equal(s.T(), "my_custom_image", result.Image) }) s.Run("image with dash in tag", func() { result, err := ProcessContainerName("nginx:1.21-alpine") require.NoError(s.T(), err) assert.Equal(s.T(), "1.21-alpine", result.Tag) }) s.Run("image with rc/beta tag", func() { result, err := ProcessContainerName("kubernetes/kube-apiserver:v1.29.0-rc.0") require.NoError(s.T(), err) assert.Equal(s.T(), "v1.29.0-rc.0", result.Tag) }) s.Run("image with only sha256 digest (no tag)", func() { sha := "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" result, err := ProcessContainerName("alpine@" + sha) require.NoError(s.T(), err) assert.Equal(s.T(), "alpine", result.Image) assert.Equal(s.T(), "", result.Tag) assert.Equal(s.T(), sha, result.Sha) }) s.Run("aws ecr image", func() { result, err := ProcessContainerName("123456789.dkr.ecr.us-east-1.amazonaws.com/my-repo:latest") require.NoError(s.T(), err) assert.Equal(s.T(), "123456789.dkr.ecr.us-east-1.amazonaws.com/my-repo", result.Image) assert.Equal(s.T(), "latest", result.Tag) }) s.Run("azure acr image", func() { result, err := ProcessContainerName("myregistry.azurecr.io/samples/nginx:v1") require.NoError(s.T(), err) assert.Equal(s.T(), "myregistry.azurecr.io/samples/nginx", result.Image) assert.Equal(s.T(), "v1", result.Tag) }) s.Run("google artifact registry", func() { result, err := ProcessContainerName("us-central1-docker.pkg.dev/my-project/my-repo/my-image:tag") require.NoError(s.T(), err) assert.Equal(s.T(), "us-central1-docker.pkg.dev/my-project/my-repo/my-image", result.Image) assert.Equal(s.T(), "tag", result.Tag) }) } // BenchmarkProcessContainerName benchmarks the ProcessContainerName function func BenchmarkProcessContainerName(b *testing.B) { images := []string{ "nginx", "nginx:latest", "quay.io/cilium/cilium:v1.18.4@sha256:49d87af187eeeb9e9e3ec2bc6bd372261a0b5cb2d845659463ba7cc10fe9e45f", "gcr.io/google-containers/pause:3.2", } b.ResetTimer() for i := 0; i < b.N; i++ { for _, img := range images { _, _ = ProcessContainerName(img) } } } // BenchmarkNormalizeImageName benchmarks the NormalizeImageName function func BenchmarkNormalizeImageName(b *testing.B) { images := []string{ "nginx", "nginx:latest", "quay.io/cilium/cilium:v1.18.4@sha256:49d87af187eeeb9e9e3ec2bc6bd372261a0b5cb2d845659463ba7cc10fe9e45f", } b.ResetTimer() for i := 0; i < b.N; i++ { for _, img := range images { _ = NormalizeImageName(img) } } } // ========== K8s Resource Wrapper Tests ========== // TestDeploymentWrapper tests the DeploymentWrapper GetPodSpec method func (s *K8sTestSuite) TestDeploymentWrapper() { deployment := appsv1.Deployment{ Spec: appsv1.DeploymentSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ {Name: "main", Image: "nginx:latest"}, }, InitContainers: []corev1.Container{ {Name: "init", Image: "busybox:1.36"}, }, }, }, }, } wrapper := (*DeploymentWrapper)(&deployment) podSpec := wrapper.GetPodSpec() require.NotNil(s.T(), podSpec) assert.Len(s.T(), podSpec.Containers, 1) assert.Equal(s.T(), "nginx:latest", podSpec.Containers[0].Image) assert.Len(s.T(), podSpec.InitContainers, 1) assert.Equal(s.T(), "busybox:1.36", podSpec.InitContainers[0].Image) } // TestJobWrapper tests the JobWrapper GetPodSpec method func (s *K8sTestSuite) TestJobWrapper() { job := batchv1.Job{ Spec: batchv1.JobSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ {Name: "worker", Image: "worker:v1.0"}, }, RestartPolicy: corev1.RestartPolicyNever, }, }, }, } wrapper := (*JobWrapper)(&job) podSpec := wrapper.GetPodSpec() require.NotNil(s.T(), podSpec) assert.Len(s.T(), podSpec.Containers, 1) assert.Equal(s.T(), "worker:v1.0", podSpec.Containers[0].Image) } // TestDaemonSetWrapper tests the DaemonSetWrapper GetPodSpec method func (s *K8sTestSuite) TestDaemonSetWrapper() { daemonSet := appsv1.DaemonSet{ Spec: appsv1.DaemonSetSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ {Name: "agent", Image: "prometheus/node-exporter:v1.6.0"}, }, }, }, }, } wrapper := (*DaemonSetWrapper)(&daemonSet) podSpec := wrapper.GetPodSpec() require.NotNil(s.T(), podSpec) assert.Len(s.T(), podSpec.Containers, 1) assert.Equal(s.T(), "prometheus/node-exporter:v1.6.0", podSpec.Containers[0].Image) } // TestCronJobWrapper tests the CronJobWrapper GetPodSpec method func (s *K8sTestSuite) TestCronJobWrapper() { cronJob := batchv1.CronJob{ Spec: batchv1.CronJobSpec{ Schedule: "*/5 * * * *", JobTemplate: batchv1.JobTemplateSpec{ Spec: batchv1.JobSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ {Name: "cron-worker", Image: "myapp/cron:latest"}, }, RestartPolicy: corev1.RestartPolicyOnFailure, }, }, }, }, }, } wrapper := (*CronJobWrapper)(&cronJob) podSpec := wrapper.GetPodSpec() require.NotNil(s.T(), podSpec) assert.Len(s.T(), podSpec.Containers, 1) assert.Equal(s.T(), "myapp/cron:latest", podSpec.Containers[0].Image) } // ========== ProcessContainers Tests ========== // TestProcessContainers tests the processContainers function func (s *K8sTestSuite) TestProcessContainers() { ctx := context.Background() s.Run("deployment with containers and init containers", func() { deployment := appsv1.Deployment{ Spec: appsv1.DeploymentSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ {Name: "main", Image: "nginx:1.21"}, {Name: "sidecar", Image: "envoyproxy/envoy:v1.28"}, }, InitContainers: []corev1.Container{ {Name: "init-db", Image: "postgres:15"}, }, }, }, }, } wrapper := (*DeploymentWrapper)(&deployment) containersList := &ContainersList{} err := processContainers(ctx, wrapper, "default", containersList) require.NoError(s.T(), err) assert.Len(s.T(), containersList.Containers, 3) // Verify all images are processed images := make([]string, len(containersList.Containers)) for i, c := range containersList.Containers { images[i] = c.FullName } assert.Contains(s.T(), images, "nginx:1.21") assert.Contains(s.T(), images, "envoyproxy/envoy:v1.28") assert.Contains(s.T(), images, "postgres:15") // Verify namespace is set for _, c := range containersList.Containers { assert.Equal(s.T(), "default", c.ImageNamespace) } }) s.Run("job with single container", func() { job := batchv1.Job{ Spec: batchv1.JobSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ {Name: "worker", Image: "myapp/worker:v2.0"}, }, RestartPolicy: corev1.RestartPolicyNever, }, }, }, } wrapper := (*JobWrapper)(&job) containersList := &ContainersList{} err := processContainers(ctx, wrapper, "production", containersList) require.NoError(s.T(), err) assert.Len(s.T(), containersList.Containers, 1) assert.Equal(s.T(), "myapp/worker", containersList.Containers[0].Image) assert.Equal(s.T(), "v2.0", containersList.Containers[0].Tag) assert.Equal(s.T(), "production", containersList.Containers[0].ImageNamespace) }) s.Run("daemonset with multiple containers", func() { daemonSet := appsv1.DaemonSet{ Spec: appsv1.DaemonSetSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ {Name: "exporter", Image: "prom/node-exporter:v1.6.0"}, {Name: "collector", Image: "otel/opentelemetry-collector:0.88.0"}, }, }, }, }, } wrapper := (*DaemonSetWrapper)(&daemonSet) containersList := &ContainersList{} err := processContainers(ctx, wrapper, "monitoring", containersList) require.NoError(s.T(), err) assert.Len(s.T(), containersList.Containers, 2) }) s.Run("cronjob with container", func() { cronJob := batchv1.CronJob{ Spec: batchv1.CronJobSpec{ Schedule: "0 * * * *", JobTemplate: batchv1.JobTemplateSpec{ Spec: batchv1.JobSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ {Name: "backup", Image: "restic/restic:0.16.2"}, }, RestartPolicy: corev1.RestartPolicyNever, }, }, }, }, }, } wrapper := (*CronJobWrapper)(&cronJob) containersList := &ContainersList{} err := processContainers(ctx, wrapper, "backup", containersList) require.NoError(s.T(), err) assert.Len(s.T(), containersList.Containers, 1) assert.Equal(s.T(), "restic/restic", containersList.Containers[0].Image) }) s.Run("deployment with ephemeral containers", func() { deployment := appsv1.Deployment{ Spec: appsv1.DeploymentSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ {Name: "main", Image: "myapp:v1"}, }, EphemeralContainers: []corev1.EphemeralContainer{ { EphemeralContainerCommon: corev1.EphemeralContainerCommon{ Name: "debugger", Image: "busybox:1.36", }, }, }, }, }, }, } wrapper := (*DeploymentWrapper)(&deployment) containersList := &ContainersList{} err := processContainers(ctx, wrapper, "default", containersList) require.NoError(s.T(), err) // Should include both main container and ephemeral container assert.Len(s.T(), containersList.Containers, 2) }) s.Run("context cancellation", func() { ctx, cancel := context.WithCancel(context.Background()) cancel() // Cancel immediately deployment := appsv1.Deployment{ Spec: appsv1.DeploymentSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ {Name: "main", Image: "nginx:latest"}, }, }, }, }, } wrapper := (*DeploymentWrapper)(&deployment) containersList := &ContainersList{} err := processContainers(ctx, wrapper, "default", containersList) // Should return context error assert.Error(s.T(), err) assert.ErrorIs(s.T(), err, context.Canceled) }) } // TestProcessContainer tests the processContainer function directly func (s *K8sTestSuite) TestProcessContainer() { ctx := context.Background() s.Run("valid image", func() { containersList := &ContainersList{} err := processContainer(ctx, "nginx:1.21", "default", containersList) require.NoError(s.T(), err) assert.Len(s.T(), containersList.Containers, 1) assert.Equal(s.T(), "nginx", containersList.Containers[0].Image) assert.Equal(s.T(), "1.21", containersList.Containers[0].Tag) assert.Equal(s.T(), "default", containersList.Containers[0].ImageNamespace) }) s.Run("image with sha", func() { sha := "sha256:abc123def456" containersList := &ContainersList{} err := processContainer(ctx, "nginx:1.21@"+sha, "production", containersList) require.NoError(s.T(), err) assert.Len(s.T(), containersList.Containers, 1) assert.Equal(s.T(), "nginx", containersList.Containers[0].Image) assert.Equal(s.T(), "1.21", containersList.Containers[0].Tag) assert.Equal(s.T(), sha, containersList.Containers[0].Sha) }) s.Run("context already cancelled", func() { ctx, cancel := context.WithCancel(context.Background()) cancel() containersList := &ContainersList{} err := processContainer(ctx, "nginx:latest", "default", containersList) assert.Error(s.T(), err) assert.ErrorIs(s.T(), err, context.Canceled) }) s.Run("invalid image name", func() { containersList := &ContainersList{} err := processContainer(ctx, "@@@invalid@@@", "default", containersList) assert.Error(s.T(), err) }) } // TestContainerCache tests the container cache functionality func (s *K8sTestSuite) TestContainerCache() { // Clear cache first containerCache.Lock() containerCache.cache = make(map[string]Container) containerCache.Unlock() s.Run("cache miss then hit", func() { // First call - cache miss result1, err := ProcessContainerName("redis:7.0") require.NoError(s.T(), err) assert.Equal(s.T(), "redis", result1.Image) // Second call - should hit cache result2, err := ProcessContainerName("redis:7.0") require.NoError(s.T(), err) assert.Equal(s.T(), result1, result2) }) s.Run("get from empty cache", func() { _, found := containerCache.Get("nonexistent:tag") assert.False(s.T(), found) }) s.Run("set and get from cache", func() { testContainer := Container{ Image: "test-image", Tag: "test-tag", FullName: "test-image:test-tag", } containerCache.Set("test-key", testContainer) retrieved, found := containerCache.Get("test-key") assert.True(s.T(), found) assert.Equal(s.T(), testContainer, retrieved) }) } // TestConcurrentProcessing tests concurrent container processing func (s *K8sTestSuite) TestConcurrentProcessing() { ctx := context.Background() s.Run("concurrent processContainer calls", func() { images := []string{ "nginx:1.21", "redis:7.0", "postgres:15", "mysql:8.0", "mongodb:6.0", } results := make(chan Container, len(images)) errors := make(chan error, len(images)) for _, img := range images { go func(image string) { containersList := &ContainersList{} err := processContainer(ctx, image, "default", containersList) if err != nil { errors <- err return } if len(containersList.Containers) > 0 { results <- containersList.Containers[0] } }(img) } // Collect results collected := 0 for collected < len(images) { select { case <-results: collected++ case err := <-errors: s.T().Errorf("Unexpected error: %v", err) collected++ case <-time.After(5 * time.Second): s.T().Fatal("Timeout waiting for concurrent processing") } } assert.Equal(s.T(), len(images), collected) }) } // Helper to create a fake client func (s *K8sTestSuite) newFakeClient(objs ...client.Object) client.Client { scheme := runtime.NewScheme() _ = appsv1.AddToScheme(scheme) _ = batchv1.AddToScheme(scheme) _ = corev1.AddToScheme(scheme) return fake.NewClientBuilder(). WithScheme(scheme). WithObjects(objs...). Build() } // TestListAndProcessResources tests the ListAndProcessResources function func (s *K8sTestSuite) TestListAndProcessResources() { ctx := context.Background() s.Run("process deployments", func() { deployment := &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ Name: "test-deployment", Namespace: "default", }, Spec: appsv1.DeploymentSpec{ Selector: &metav1.LabelSelector{ MatchLabels: map[string]string{"app": "test"}, }, Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"app": "test"}}, Spec: corev1.PodSpec{ Containers: []corev1.Container{ {Name: "main", Image: "nginx:1.21"}, {Name: "sidecar", Image: "envoy:v1.28"}, }, }, }, }, } fakeClient := s.newFakeClient(deployment) containersList := &ContainersList{} err := ListAndProcessResources[*DeploymentWrapper](ctx, fakeClient, &appsv1.DeploymentList{}, containersList) require.NoError(s.T(), err) assert.Len(s.T(), containersList.Containers, 2) // Verify images images := make(map[string]bool) for _, c := range containersList.Containers { images[c.FullName] = true } assert.True(s.T(), images["nginx:1.21"]) assert.True(s.T(), images["envoy:v1.28"]) }) s.Run("process jobs", func() { job := &batchv1.Job{ ObjectMeta: metav1.ObjectMeta{ Name: "test-job", Namespace: "default", }, Spec: batchv1.JobSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ {Name: "worker", Image: "busybox:1.36"}, }, RestartPolicy: corev1.RestartPolicyNever, }, }, }, } fakeClient := s.newFakeClient(job) containersList := &ContainersList{} err := ListAndProcessResources[*JobWrapper](ctx, fakeClient, &batchv1.JobList{}, containersList) require.NoError(s.T(), err) assert.Len(s.T(), containersList.Containers, 1) assert.Equal(s.T(), "busybox", containersList.Containers[0].Image) }) s.Run("process daemonsets", func() { daemonSet := &appsv1.DaemonSet{ ObjectMeta: metav1.ObjectMeta{ Name: "test-ds", Namespace: "monitoring", }, Spec: appsv1.DaemonSetSpec{ Selector: &metav1.LabelSelector{ MatchLabels: map[string]string{"app": "exporter"}, }, Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"app": "exporter"}}, Spec: corev1.PodSpec{ Containers: []corev1.Container{ {Name: "exporter", Image: "prom/node-exporter:v1.6.0"}, }, }, }, }, } fakeClient := s.newFakeClient(daemonSet) containersList := &ContainersList{} err := ListAndProcessResources[*DaemonSetWrapper](ctx, fakeClient, &appsv1.DaemonSetList{}, containersList) require.NoError(s.T(), err) assert.Len(s.T(), containersList.Containers, 1) assert.Equal(s.T(), "monitoring", containersList.Containers[0].ImageNamespace) }) s.Run("process cronjobs", func() { cronJob := &batchv1.CronJob{ ObjectMeta: metav1.ObjectMeta{ Name: "test-cron", Namespace: "batch", }, Spec: batchv1.CronJobSpec{ Schedule: "0 * * * *", JobTemplate: batchv1.JobTemplateSpec{ Spec: batchv1.JobSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ {Name: "cron", Image: "alpine:3.18"}, }, RestartPolicy: corev1.RestartPolicyNever, }, }, }, }, }, } fakeClient := s.newFakeClient(cronJob) containersList := &ContainersList{} err := ListAndProcessResources[*CronJobWrapper](ctx, fakeClient, &batchv1.CronJobList{}, containersList) require.NoError(s.T(), err) assert.Len(s.T(), containersList.Containers, 1) assert.Equal(s.T(), "alpine", containersList.Containers[0].Image) }) s.Run("process multiple resources", func() { deployment1 := &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{Name: "deploy-1", Namespace: "ns1"}, Spec: appsv1.DeploymentSpec{ Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"app": "app1"}}, Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"app": "app1"}}, Spec: corev1.PodSpec{ Containers: []corev1.Container{{Name: "main", Image: "app1:v1"}}, }, }, }, } deployment2 := &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{Name: "deploy-2", Namespace: "ns2"}, Spec: appsv1.DeploymentSpec{ Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"app": "app2"}}, Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"app": "app2"}}, Spec: corev1.PodSpec{ Containers: []corev1.Container{{Name: "main", Image: "app2:v1"}}, }, }, }, } fakeClient := s.newFakeClient(deployment1, deployment2) containersList := &ContainersList{} err := ListAndProcessResources[*DeploymentWrapper](ctx, fakeClient, &appsv1.DeploymentList{}, containersList) require.NoError(s.T(), err) assert.Len(s.T(), containersList.Containers, 2) }) s.Run("empty list", func() { fakeClient := s.newFakeClient() containersList := &ContainersList{} err := ListAndProcessResources[*DeploymentWrapper](ctx, fakeClient, &appsv1.DeploymentList{}, containersList) require.NoError(s.T(), err) assert.Len(s.T(), containersList.Containers, 0) }) s.Run("context cancellation", func() { ctx, cancel := context.WithCancel(context.Background()) cancel() deployment := &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"}, Spec: appsv1.DeploymentSpec{ Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"app": "test"}}, Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"app": "test"}}, Spec: corev1.PodSpec{ Containers: []corev1.Container{{Name: "main", Image: "test:v1"}}, }, }, }, } fakeClient := s.newFakeClient(deployment) containersList := &ContainersList{} err := ListAndProcessResources[*DeploymentWrapper](ctx, fakeClient, &appsv1.DeploymentList{}, containersList) // Context cancellation should result in an error assert.Error(s.T(), err) }) s.Run("unsupported list type", func() { fakeClient := s.newFakeClient() containersList := &ContainersList{} // Pass an unsupported list type (corev1.PodList is not handled) err := ListAndProcessResources[*DeploymentWrapper](ctx, fakeClient, &corev1.PodList{}, containersList) assert.Error(s.T(), err) assert.Contains(s.T(), err.Error(), "unsupported list type") }) }