mirror of
https://github.com/lukaszraczylo/kubernetes-images-sync-operator.git
synced 2026-06-10 23:29:11 +00:00
More fixes, moving from python to golang worker.
This commit is contained in:
@@ -0,0 +1,643 @@
|
||||
package shared
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
// TestScenario represents a test scenario classification
|
||||
type TestScenario string
|
||||
|
||||
const (
|
||||
ScenarioGood TestScenario = "good"
|
||||
ScenarioNotGood TestScenario = "not_good"
|
||||
ScenarioReallyBad TestScenario = "really_bad"
|
||||
)
|
||||
|
||||
// DefinitionsTestSuite tests the definitions and utility functions
|
||||
type DefinitionsTestSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func TestDefinitionsTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(DefinitionsTestSuite))
|
||||
}
|
||||
|
||||
// TestNormalizeImageName tests the NormalizeImageName function with matrix strategy
|
||||
func (s *DefinitionsTestSuite) TestNormalizeImageName() {
|
||||
testCases := []struct {
|
||||
name string
|
||||
scenario TestScenario
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
// Good scenarios - standard image names
|
||||
{
|
||||
name: "simple image name",
|
||||
scenario: ScenarioGood,
|
||||
input: "nginx",
|
||||
expected: "nginx",
|
||||
},
|
||||
{
|
||||
name: "image with tag",
|
||||
scenario: ScenarioGood,
|
||||
input: "nginx:latest",
|
||||
expected: "nginx-latest",
|
||||
},
|
||||
{
|
||||
name: "image with version tag",
|
||||
scenario: ScenarioGood,
|
||||
input: "nginx:1.21.0",
|
||||
expected: "nginx-1.21.0",
|
||||
},
|
||||
{
|
||||
name: "full registry path",
|
||||
scenario: ScenarioGood,
|
||||
input: "quay.io/cilium/cilium:v1.18.4",
|
||||
expected: "quay.io-cilium-cilium-v1.18.4",
|
||||
},
|
||||
{
|
||||
name: "ghcr registry",
|
||||
scenario: ScenarioGood,
|
||||
input: "ghcr.io/owner/repo:v1.0.0",
|
||||
expected: "ghcr.io-owner-repo-v1.0.0",
|
||||
},
|
||||
|
||||
// Not good scenarios - unusual but valid formats
|
||||
{
|
||||
name: "image with SHA digest",
|
||||
scenario: ScenarioNotGood,
|
||||
input: "nginx@sha256:abc123def456",
|
||||
expected: "nginx-sha256-abc123def456",
|
||||
},
|
||||
{
|
||||
name: "image with tag and SHA",
|
||||
scenario: ScenarioNotGood,
|
||||
input: "quay.io/cilium/cilium:v1.18.4@sha256:49d87af187eeeb9e9e3ec2bc6bd372261a0b5cb2d845659463ba7cc10fe9e45f",
|
||||
expected: "quay.io-cilium-cilium-v1.18.4-sha256-49d87af187eeeb9e9e3ec2bc6bd372261a0b5cb2d845659463ba7cc10fe9e45f",
|
||||
},
|
||||
{
|
||||
name: "multiple colons in path",
|
||||
scenario: ScenarioNotGood,
|
||||
input: "registry:5000/image:tag",
|
||||
expected: "registry-5000-image-tag",
|
||||
},
|
||||
|
||||
// Really bad scenarios - edge cases and potential problems
|
||||
{
|
||||
name: "empty string",
|
||||
scenario: ScenarioReallyBad,
|
||||
input: "",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "only special characters",
|
||||
scenario: ScenarioReallyBad,
|
||||
input: ":///@",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "multiple consecutive special chars",
|
||||
scenario: ScenarioReallyBad,
|
||||
input: "image:::tag",
|
||||
expected: "image-tag",
|
||||
},
|
||||
{
|
||||
name: "leading special characters",
|
||||
scenario: ScenarioReallyBad,
|
||||
input: "//image:tag",
|
||||
expected: "image-tag",
|
||||
},
|
||||
{
|
||||
name: "trailing special characters",
|
||||
scenario: ScenarioReallyBad,
|
||||
input: "image:tag//",
|
||||
expected: "image-tag",
|
||||
},
|
||||
{
|
||||
name: "spaces in name",
|
||||
scenario: ScenarioReallyBad,
|
||||
input: "image name:tag",
|
||||
expected: "image-name-tag",
|
||||
},
|
||||
{
|
||||
name: "unicode characters",
|
||||
scenario: ScenarioReallyBad,
|
||||
input: "image:tag-日本語",
|
||||
expected: "image-tag-日本語",
|
||||
},
|
||||
{
|
||||
name: "very long image name",
|
||||
scenario: ScenarioReallyBad,
|
||||
input: "registry.example.com/very/long/path/to/image:tag@sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
|
||||
expected: "registry.example.com-very-long-path-to-image-tag-sha256-abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
|
||||
},
|
||||
{
|
||||
name: "query string characters",
|
||||
scenario: ScenarioReallyBad,
|
||||
input: "image?foo=bar&baz=qux",
|
||||
expected: "image-foo-bar-baz-qux",
|
||||
},
|
||||
{
|
||||
name: "brackets and special chars",
|
||||
scenario: ScenarioReallyBad,
|
||||
input: "image[tag]{version}",
|
||||
expected: "image-tag-version",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
s.Run(tc.name, func() {
|
||||
result := NormalizeImageName(tc.input)
|
||||
assert.Equal(s.T(), tc.expected, result, "Scenario: %s", tc.scenario)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestRemoveDuplicates tests duplicate removal functionality
|
||||
func (s *DefinitionsTestSuite) TestRemoveDuplicates() {
|
||||
testCases := []struct {
|
||||
name string
|
||||
scenario TestScenario
|
||||
input ContainersList
|
||||
expected int
|
||||
}{
|
||||
// Good scenarios
|
||||
{
|
||||
name: "no duplicates",
|
||||
scenario: ScenarioGood,
|
||||
input: ContainersList{
|
||||
Containers: []Container{
|
||||
{Image: "nginx", Tag: "latest", FullName: "nginx:latest"},
|
||||
{Image: "redis", Tag: "6", FullName: "redis:6"},
|
||||
},
|
||||
},
|
||||
expected: 2,
|
||||
},
|
||||
{
|
||||
name: "with duplicates",
|
||||
scenario: ScenarioGood,
|
||||
input: ContainersList{
|
||||
Containers: []Container{
|
||||
{Image: "nginx", Tag: "latest", FullName: "nginx:latest"},
|
||||
{Image: "nginx", Tag: "latest", FullName: "nginx:latest"},
|
||||
{Image: "redis", Tag: "6", FullName: "redis:6"},
|
||||
},
|
||||
},
|
||||
expected: 2,
|
||||
},
|
||||
|
||||
// Not good scenarios
|
||||
{
|
||||
name: "same image different tags",
|
||||
scenario: ScenarioNotGood,
|
||||
input: ContainersList{
|
||||
Containers: []Container{
|
||||
{Image: "nginx", Tag: "latest", FullName: "nginx:latest"},
|
||||
{Image: "nginx", Tag: "1.21", FullName: "nginx:1.21"},
|
||||
},
|
||||
},
|
||||
expected: 2, // Should keep both
|
||||
},
|
||||
{
|
||||
name: "same image with and without SHA",
|
||||
scenario: ScenarioNotGood,
|
||||
input: ContainersList{
|
||||
Containers: []Container{
|
||||
{Image: "nginx", Tag: "latest", FullName: "nginx:latest"},
|
||||
{Image: "nginx", Tag: "latest", Sha: "sha256:abc", FullName: "nginx:latest@sha256:abc"},
|
||||
},
|
||||
},
|
||||
expected: 2, // Different because of SHA
|
||||
},
|
||||
|
||||
// Really bad scenarios
|
||||
{
|
||||
name: "empty list",
|
||||
scenario: ScenarioReallyBad,
|
||||
input: ContainersList{Containers: []Container{}},
|
||||
expected: 0,
|
||||
},
|
||||
{
|
||||
name: "all duplicates",
|
||||
scenario: ScenarioReallyBad,
|
||||
input: ContainersList{
|
||||
Containers: []Container{
|
||||
{Image: "nginx", Tag: "latest", FullName: "nginx:latest"},
|
||||
{Image: "nginx", Tag: "latest", FullName: "nginx:latest"},
|
||||
{Image: "nginx", Tag: "latest", FullName: "nginx:latest"},
|
||||
},
|
||||
},
|
||||
expected: 1,
|
||||
},
|
||||
{
|
||||
name: "nil containers",
|
||||
scenario: ScenarioReallyBad,
|
||||
input: ContainersList{Containers: nil},
|
||||
expected: 0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
s.Run(tc.name, func() {
|
||||
result := RemoveDuplicates(tc.input)
|
||||
assert.Len(s.T(), result.Containers, tc.expected, "Scenario: %s", tc.scenario)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestRemoveExcludedImages tests image exclusion functionality
|
||||
func (s *DefinitionsTestSuite) TestRemoveExcludedImages() {
|
||||
testCases := []struct {
|
||||
name string
|
||||
scenario TestScenario
|
||||
input ContainersList
|
||||
excludes []string
|
||||
expected int
|
||||
}{
|
||||
// Good scenarios
|
||||
{
|
||||
name: "no exclusions",
|
||||
scenario: ScenarioGood,
|
||||
input: ContainersList{
|
||||
Containers: []Container{
|
||||
{Image: "nginx", Tag: "latest"},
|
||||
{Image: "redis", Tag: "6"},
|
||||
},
|
||||
},
|
||||
excludes: []string{},
|
||||
expected: 2,
|
||||
},
|
||||
{
|
||||
name: "exclude one image",
|
||||
scenario: ScenarioGood,
|
||||
input: ContainersList{
|
||||
Containers: []Container{
|
||||
{Image: "nginx", Tag: "latest"},
|
||||
{Image: "redis", Tag: "6"},
|
||||
},
|
||||
},
|
||||
excludes: []string{"nginx"},
|
||||
expected: 1,
|
||||
},
|
||||
{
|
||||
name: "exclude by registry",
|
||||
scenario: ScenarioGood,
|
||||
input: ContainersList{
|
||||
Containers: []Container{
|
||||
{Image: "gcr.io/google/nginx", Tag: "latest"},
|
||||
{Image: "docker.io/library/redis", Tag: "6"},
|
||||
},
|
||||
},
|
||||
excludes: []string{"gcr.io"},
|
||||
expected: 1,
|
||||
},
|
||||
|
||||
// Not good scenarios
|
||||
{
|
||||
name: "case insensitive exclusion",
|
||||
scenario: ScenarioNotGood,
|
||||
input: ContainersList{
|
||||
Containers: []Container{
|
||||
{Image: "NGINX", Tag: "latest"},
|
||||
{Image: "Redis", Tag: "6"},
|
||||
},
|
||||
},
|
||||
excludes: []string{"nginx"},
|
||||
expected: 1, // Should exclude NGINX
|
||||
},
|
||||
{
|
||||
name: "partial match exclusion",
|
||||
scenario: ScenarioNotGood,
|
||||
input: ContainersList{
|
||||
Containers: []Container{
|
||||
{Image: "my-nginx-custom", Tag: "latest"},
|
||||
{Image: "redis", Tag: "6"},
|
||||
},
|
||||
},
|
||||
excludes: []string{"nginx"},
|
||||
expected: 1, // Should exclude my-nginx-custom
|
||||
},
|
||||
|
||||
// Really bad scenarios
|
||||
{
|
||||
name: "exclude all images",
|
||||
scenario: ScenarioReallyBad,
|
||||
input: ContainersList{
|
||||
Containers: []Container{
|
||||
{Image: "nginx", Tag: "latest"},
|
||||
{Image: "redis", Tag: "6"},
|
||||
},
|
||||
},
|
||||
excludes: []string{"nginx", "redis"},
|
||||
expected: 0,
|
||||
},
|
||||
{
|
||||
name: "empty exclude list on empty containers",
|
||||
scenario: ScenarioReallyBad,
|
||||
input: ContainersList{Containers: []Container{}},
|
||||
excludes: []string{},
|
||||
expected: 0,
|
||||
},
|
||||
{
|
||||
name: "exclude with empty string",
|
||||
scenario: ScenarioReallyBad,
|
||||
input: ContainersList{
|
||||
Containers: []Container{
|
||||
{Image: "nginx", Tag: "latest"},
|
||||
},
|
||||
},
|
||||
excludes: []string{""},
|
||||
expected: 0, // Empty string matches all
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
s.Run(tc.name, func() {
|
||||
result := RemoveExcludedImages(tc.input, tc.excludes)
|
||||
assert.Len(s.T(), result.Containers, tc.expected, "Scenario: %s", tc.scenario)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestIncludeOnlyImages tests image inclusion filtering
|
||||
func (s *DefinitionsTestSuite) TestIncludeOnlyImages() {
|
||||
testCases := []struct {
|
||||
name string
|
||||
scenario TestScenario
|
||||
input ContainersList
|
||||
includes []string
|
||||
expected int
|
||||
}{
|
||||
// Good scenarios
|
||||
{
|
||||
name: "include specific image",
|
||||
scenario: ScenarioGood,
|
||||
input: ContainersList{
|
||||
Containers: []Container{
|
||||
{Image: "nginx", Tag: "latest"},
|
||||
{Image: "redis", Tag: "6"},
|
||||
{Image: "postgres", Tag: "14"},
|
||||
},
|
||||
},
|
||||
includes: []string{"nginx"},
|
||||
expected: 1,
|
||||
},
|
||||
{
|
||||
name: "include multiple images",
|
||||
scenario: ScenarioGood,
|
||||
input: ContainersList{
|
||||
Containers: []Container{
|
||||
{Image: "nginx", Tag: "latest"},
|
||||
{Image: "redis", Tag: "6"},
|
||||
{Image: "postgres", Tag: "14"},
|
||||
},
|
||||
},
|
||||
includes: []string{"nginx", "redis"},
|
||||
expected: 2,
|
||||
},
|
||||
|
||||
// Not good scenarios
|
||||
{
|
||||
name: "include by partial match",
|
||||
scenario: ScenarioNotGood,
|
||||
input: ContainersList{
|
||||
Containers: []Container{
|
||||
{Image: "my-nginx-app", Tag: "latest"},
|
||||
{Image: "nginx-proxy", Tag: "v1"},
|
||||
{Image: "redis", Tag: "6"},
|
||||
},
|
||||
},
|
||||
includes: []string{"nginx"},
|
||||
expected: 2,
|
||||
},
|
||||
|
||||
// Really bad scenarios
|
||||
{
|
||||
name: "include non-existent image",
|
||||
scenario: ScenarioReallyBad,
|
||||
input: ContainersList{
|
||||
Containers: []Container{
|
||||
{Image: "nginx", Tag: "latest"},
|
||||
},
|
||||
},
|
||||
includes: []string{"nonexistent"},
|
||||
expected: 0,
|
||||
},
|
||||
{
|
||||
name: "empty includes list",
|
||||
scenario: ScenarioReallyBad,
|
||||
input: ContainersList{
|
||||
Containers: []Container{
|
||||
{Image: "nginx", Tag: "latest"},
|
||||
},
|
||||
},
|
||||
includes: []string{},
|
||||
expected: 0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
s.Run(tc.name, func() {
|
||||
result := IncludeOnlyImages(tc.input, tc.includes)
|
||||
assert.Len(s.T(), result.Containers, tc.expected, "Scenario: %s", tc.scenario)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestFilterOnlyFromNamespaces tests namespace filtering
|
||||
func (s *DefinitionsTestSuite) TestFilterOnlyFromNamespaces() {
|
||||
testCases := []struct {
|
||||
name string
|
||||
scenario TestScenario
|
||||
input ContainersList
|
||||
namespaces []string
|
||||
expected int
|
||||
}{
|
||||
// Good scenarios
|
||||
{
|
||||
name: "filter single namespace",
|
||||
scenario: ScenarioGood,
|
||||
input: ContainersList{
|
||||
Containers: []Container{
|
||||
{Image: "nginx", ImageNamespace: "default"},
|
||||
{Image: "redis", ImageNamespace: "kube-system"},
|
||||
},
|
||||
},
|
||||
namespaces: []string{"default"},
|
||||
expected: 1,
|
||||
},
|
||||
|
||||
// Not good scenarios
|
||||
{
|
||||
name: "filter multiple namespaces",
|
||||
scenario: ScenarioNotGood,
|
||||
input: ContainersList{
|
||||
Containers: []Container{
|
||||
{Image: "nginx", ImageNamespace: "default"},
|
||||
{Image: "redis", ImageNamespace: "kube-system"},
|
||||
{Image: "postgres", ImageNamespace: "database"},
|
||||
},
|
||||
},
|
||||
namespaces: []string{"default", "database"},
|
||||
expected: 2,
|
||||
},
|
||||
|
||||
// Really bad scenarios
|
||||
{
|
||||
name: "filter non-existent namespace",
|
||||
scenario: ScenarioReallyBad,
|
||||
input: ContainersList{
|
||||
Containers: []Container{
|
||||
{Image: "nginx", ImageNamespace: "default"},
|
||||
},
|
||||
},
|
||||
namespaces: []string{"nonexistent"},
|
||||
expected: 0,
|
||||
},
|
||||
{
|
||||
name: "empty namespace filter",
|
||||
scenario: ScenarioReallyBad,
|
||||
input: ContainersList{
|
||||
Containers: []Container{
|
||||
{Image: "nginx", ImageNamespace: "default"},
|
||||
},
|
||||
},
|
||||
namespaces: []string{},
|
||||
expected: 0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
s.Run(tc.name, func() {
|
||||
result := FilterOnlyFromNamespaces(tc.input, tc.namespaces)
|
||||
assert.Len(s.T(), result.Containers, tc.expected, "Scenario: %s", tc.scenario)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestFilterOutWholeNamespaces tests namespace exclusion
|
||||
func (s *DefinitionsTestSuite) TestFilterOutWholeNamespaces() {
|
||||
testCases := []struct {
|
||||
name string
|
||||
scenario TestScenario
|
||||
input ContainersList
|
||||
namespaces []string
|
||||
expected int
|
||||
}{
|
||||
// Good scenarios
|
||||
{
|
||||
name: "exclude kube-system",
|
||||
scenario: ScenarioGood,
|
||||
input: ContainersList{
|
||||
Containers: []Container{
|
||||
{Image: "nginx", ImageNamespace: "default"},
|
||||
{Image: "coredns", ImageNamespace: "kube-system"},
|
||||
{Image: "redis", ImageNamespace: "apps"},
|
||||
},
|
||||
},
|
||||
namespaces: []string{"kube-system"},
|
||||
expected: 2,
|
||||
},
|
||||
|
||||
// Not good scenarios
|
||||
{
|
||||
name: "exclude multiple system namespaces",
|
||||
scenario: ScenarioNotGood,
|
||||
input: ContainersList{
|
||||
Containers: []Container{
|
||||
{Image: "nginx", ImageNamespace: "default"},
|
||||
{Image: "coredns", ImageNamespace: "kube-system"},
|
||||
{Image: "cilium", ImageNamespace: "kube-system"},
|
||||
{Image: "local-path", ImageNamespace: "local-path-storage"},
|
||||
},
|
||||
},
|
||||
namespaces: []string{"kube-system", "local-path-storage"},
|
||||
expected: 1,
|
||||
},
|
||||
|
||||
// Really bad scenarios
|
||||
{
|
||||
name: "exclude all namespaces",
|
||||
scenario: ScenarioReallyBad,
|
||||
input: ContainersList{
|
||||
Containers: []Container{
|
||||
{Image: "nginx", ImageNamespace: "default"},
|
||||
{Image: "redis", ImageNamespace: "apps"},
|
||||
},
|
||||
},
|
||||
namespaces: []string{"default", "apps"},
|
||||
expected: 0,
|
||||
},
|
||||
{
|
||||
name: "empty exclusion list",
|
||||
scenario: ScenarioReallyBad,
|
||||
input: ContainersList{
|
||||
Containers: []Container{
|
||||
{Image: "nginx", ImageNamespace: "default"},
|
||||
},
|
||||
},
|
||||
namespaces: []string{},
|
||||
expected: 1, // No exclusions = keep all
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
s.Run(tc.name, func() {
|
||||
result := FilterOutWholeNamespaces(tc.input, tc.namespaces)
|
||||
assert.Len(s.T(), result.Containers, tc.expected, "Scenario: %s", tc.scenario)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestContainerStruct tests Container struct behavior
|
||||
func (s *DefinitionsTestSuite) TestContainerStruct() {
|
||||
s.Run("full container with all fields", func() {
|
||||
c := Container{
|
||||
Image: "quay.io/cilium/cilium",
|
||||
Tag: "v1.18.4",
|
||||
Sha: "sha256:49d87af187eeeb9e9e3ec2bc6bd372261a0b5cb2d845659463ba7cc10fe9e45f",
|
||||
FullName: "quay.io/cilium/cilium:v1.18.4@sha256:49d87af187eeeb9e9e3ec2bc6bd372261a0b5cb2d845659463ba7cc10fe9e45f",
|
||||
ImageNamespace: "kube-system",
|
||||
}
|
||||
|
||||
assert.Equal(s.T(), "quay.io/cilium/cilium", c.Image)
|
||||
assert.Equal(s.T(), "v1.18.4", c.Tag)
|
||||
assert.Contains(s.T(), c.Sha, "sha256:")
|
||||
assert.Contains(s.T(), c.FullName, "@")
|
||||
})
|
||||
|
||||
s.Run("container equality for deduplication", func() {
|
||||
c1 := Container{Image: "nginx", Tag: "latest", FullName: "nginx:latest"}
|
||||
c2 := Container{Image: "nginx", Tag: "latest", FullName: "nginx:latest"}
|
||||
c3 := Container{Image: "nginx", Tag: "1.21", FullName: "nginx:1.21"}
|
||||
|
||||
assert.Equal(s.T(), c1, c2)
|
||||
assert.NotEqual(s.T(), c1, c3)
|
||||
})
|
||||
}
|
||||
|
||||
// TestConstants tests that constants are defined correctly
|
||||
func (s *DefinitionsTestSuite) TestConstants() {
|
||||
// Status constants
|
||||
assert.Equal(s.T(), "PENDING", STATUS_PENDING)
|
||||
assert.Equal(s.T(), "STARTING", STATUS_STARTING)
|
||||
assert.Equal(s.T(), "RETRYING", STATUS_RETRYING)
|
||||
assert.Equal(s.T(), "RUNNING", STATUS_RUNNING)
|
||||
assert.Equal(s.T(), "FAILED", STATUS_FAILED)
|
||||
assert.Equal(s.T(), "COMPLETED", STATUS_SUCCESS)
|
||||
assert.Equal(s.T(), "PRESENT", STATUS_PRESENT)
|
||||
|
||||
// Storage constants
|
||||
assert.Equal(s.T(), "S3", STORAGE_S3)
|
||||
assert.Equal(s.T(), "FILE", STORAGE_FILE)
|
||||
}
|
||||
|
||||
// TestBackupJobImage tests the BACKUP_JOB_IMAGE initialization
|
||||
func (s *DefinitionsTestSuite) TestBackupJobImage() {
|
||||
require.NotEmpty(s.T(), BACKUP_JOB_IMAGE)
|
||||
assert.Contains(s.T(), BACKUP_JOB_IMAGE, "kubernetes-images-sync-worker")
|
||||
}
|
||||
@@ -0,0 +1,547 @@
|
||||
package shared
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
raczylocomv1 "github.com/lukaszraczylo/kubernetes-images-sync-operator/api/raczylo.com/v1"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// JobsTestSuite tests job creation and related functionality
|
||||
type JobsTestSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func TestJobsTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(JobsTestSuite))
|
||||
}
|
||||
|
||||
// TestCreateJob tests the CreateJob function with various scenarios
|
||||
func (s *JobsTestSuite) TestCreateJob() {
|
||||
testCases := []struct {
|
||||
name string
|
||||
scenario TestScenario
|
||||
params JobParams
|
||||
expectJobName string
|
||||
expectNamespace string
|
||||
expectImage string
|
||||
expectAnnotations map[string]string
|
||||
expectSecrets int
|
||||
expectEnvVars int
|
||||
}{
|
||||
// Good scenarios
|
||||
{
|
||||
name: "basic job creation",
|
||||
scenario: ScenarioGood,
|
||||
params: JobParams{
|
||||
Name: "test-job",
|
||||
Namespace: "default",
|
||||
Image: "worker:latest",
|
||||
Commands: []string{"echo hello"},
|
||||
},
|
||||
expectJobName: "test-job",
|
||||
expectNamespace: "default",
|
||||
expectImage: "worker:latest",
|
||||
expectSecrets: 0,
|
||||
expectEnvVars: 0,
|
||||
},
|
||||
{
|
||||
name: "job with annotations",
|
||||
scenario: ScenarioGood,
|
||||
params: JobParams{
|
||||
Name: "annotated-job",
|
||||
Namespace: "default",
|
||||
Image: "worker:latest",
|
||||
Commands: []string{"echo hello"},
|
||||
Annotations: map[string]string{
|
||||
"key1": "value1",
|
||||
"key2": "value2",
|
||||
},
|
||||
},
|
||||
expectJobName: "annotated-job",
|
||||
expectNamespace: "default",
|
||||
expectImage: "worker:latest",
|
||||
expectAnnotations: map[string]string{
|
||||
"key1": "value1",
|
||||
"key2": "value2",
|
||||
},
|
||||
expectSecrets: 0,
|
||||
expectEnvVars: 0,
|
||||
},
|
||||
{
|
||||
name: "job with image pull secrets",
|
||||
scenario: ScenarioGood,
|
||||
params: JobParams{
|
||||
Name: "secret-job",
|
||||
Namespace: "default",
|
||||
Image: "private-registry/image:latest",
|
||||
Commands: []string{"echo hello"},
|
||||
ImagePullSecrets: []corev1.LocalObjectReference{
|
||||
{Name: "my-registry-secret"},
|
||||
},
|
||||
},
|
||||
expectJobName: "secret-job",
|
||||
expectNamespace: "default",
|
||||
expectImage: "private-registry/image:latest",
|
||||
expectSecrets: 1,
|
||||
expectEnvVars: 0,
|
||||
},
|
||||
{
|
||||
name: "job with multiple secrets",
|
||||
scenario: ScenarioGood,
|
||||
params: JobParams{
|
||||
Name: "multi-secret-job",
|
||||
Namespace: "default",
|
||||
Image: "private-registry/image:latest",
|
||||
Commands: []string{"echo hello"},
|
||||
ImagePullSecrets: []corev1.LocalObjectReference{
|
||||
{Name: "secret1"},
|
||||
{Name: "secret2"},
|
||||
{Name: "secret3"},
|
||||
},
|
||||
},
|
||||
expectJobName: "multi-secret-job",
|
||||
expectNamespace: "default",
|
||||
expectImage: "private-registry/image:latest",
|
||||
expectSecrets: 3,
|
||||
expectEnvVars: 0,
|
||||
},
|
||||
{
|
||||
name: "job with environment variables",
|
||||
scenario: ScenarioGood,
|
||||
params: JobParams{
|
||||
Name: "env-job",
|
||||
Namespace: "default",
|
||||
Image: "worker:latest",
|
||||
Commands: []string{"echo $MY_VAR"},
|
||||
EnvVars: []corev1.EnvVar{
|
||||
{Name: "MY_VAR", Value: "my-value"},
|
||||
{Name: "AWS_REGION", Value: "us-east-1"},
|
||||
},
|
||||
},
|
||||
expectJobName: "env-job",
|
||||
expectNamespace: "default",
|
||||
expectImage: "worker:latest",
|
||||
expectSecrets: 0,
|
||||
expectEnvVars: 2,
|
||||
},
|
||||
|
||||
// Not good scenarios
|
||||
{
|
||||
name: "job with backoff limit",
|
||||
scenario: ScenarioNotGood,
|
||||
params: JobParams{
|
||||
Name: "retry-job",
|
||||
Namespace: "default",
|
||||
Image: "worker:latest",
|
||||
Commands: []string{"might-fail"},
|
||||
BackoffLimit: int32Ptr(3),
|
||||
},
|
||||
expectJobName: "retry-job",
|
||||
expectNamespace: "default",
|
||||
expectImage: "worker:latest",
|
||||
expectSecrets: 0,
|
||||
expectEnvVars: 0,
|
||||
},
|
||||
{
|
||||
name: "job with TTL after finished",
|
||||
scenario: ScenarioNotGood,
|
||||
params: JobParams{
|
||||
Name: "ttl-job",
|
||||
Namespace: "default",
|
||||
Image: "worker:latest",
|
||||
Commands: []string{"echo done"},
|
||||
TTLSecondsAfterFinished: int32Ptr(300),
|
||||
},
|
||||
expectJobName: "ttl-job",
|
||||
expectNamespace: "default",
|
||||
expectImage: "worker:latest",
|
||||
expectSecrets: 0,
|
||||
expectEnvVars: 0,
|
||||
},
|
||||
|
||||
// Really bad scenarios - edge cases
|
||||
{
|
||||
name: "job with empty commands",
|
||||
scenario: ScenarioReallyBad,
|
||||
params: JobParams{
|
||||
Name: "empty-cmd-job",
|
||||
Namespace: "default",
|
||||
Image: "worker:latest",
|
||||
Commands: []string{},
|
||||
},
|
||||
expectJobName: "empty-cmd-job",
|
||||
expectNamespace: "default",
|
||||
expectImage: "worker:latest",
|
||||
expectSecrets: 0,
|
||||
expectEnvVars: 0,
|
||||
},
|
||||
{
|
||||
name: "job with very long name",
|
||||
scenario: ScenarioReallyBad,
|
||||
params: JobParams{
|
||||
Name: "this-is-a-very-long-job-name-that-might-cause-issues-in-kubernetes-because-names-have-limits",
|
||||
Namespace: "default",
|
||||
Image: "worker:latest",
|
||||
Commands: []string{"echo hello"},
|
||||
},
|
||||
expectJobName: "this-is-a-very-long-job-name-that-might-cause-issues-in-kubernetes-because-names-have-limits",
|
||||
expectNamespace: "default",
|
||||
expectImage: "worker:latest",
|
||||
expectSecrets: 0,
|
||||
expectEnvVars: 0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
s.Run(tc.name, func() {
|
||||
job := CreateJob(tc.params, func(raczylocomv1.ClusterImageExport) []string { return nil })
|
||||
|
||||
require.NotNil(s.T(), job, "Job should not be nil")
|
||||
|
||||
// Verify job metadata
|
||||
assert.Equal(s.T(), tc.expectJobName, job.ObjectMeta.Name)
|
||||
assert.Equal(s.T(), tc.expectNamespace, job.ObjectMeta.Namespace)
|
||||
|
||||
// Verify labels
|
||||
assert.Equal(s.T(), "image-export", job.ObjectMeta.Labels["app"])
|
||||
assert.Equal(s.T(), "image-export", job.Spec.Template.ObjectMeta.Labels["app"])
|
||||
|
||||
// Verify annotations if expected
|
||||
if tc.expectAnnotations != nil {
|
||||
for k, v := range tc.expectAnnotations {
|
||||
assert.Equal(s.T(), v, job.ObjectMeta.Annotations[k])
|
||||
assert.Equal(s.T(), v, job.Spec.Template.ObjectMeta.Annotations[k])
|
||||
}
|
||||
}
|
||||
|
||||
// Verify pod template
|
||||
podSpec := job.Spec.Template.Spec
|
||||
require.Len(s.T(), podSpec.Containers, 1, "Should have exactly one container")
|
||||
|
||||
container := podSpec.Containers[0]
|
||||
assert.Equal(s.T(), "exporter", container.Name)
|
||||
assert.Equal(s.T(), tc.expectImage, container.Image)
|
||||
assert.True(s.T(), container.TTY)
|
||||
|
||||
// Verify restart policy
|
||||
assert.Equal(s.T(), corev1.RestartPolicyOnFailure, podSpec.RestartPolicy)
|
||||
|
||||
// Verify secrets
|
||||
assert.Len(s.T(), podSpec.ImagePullSecrets, tc.expectSecrets)
|
||||
assert.Len(s.T(), podSpec.Volumes, tc.expectSecrets)
|
||||
assert.Len(s.T(), container.VolumeMounts, tc.expectSecrets)
|
||||
|
||||
// Verify environment variables
|
||||
assert.Len(s.T(), container.Env, tc.expectEnvVars)
|
||||
|
||||
// Verify security context (privileged for podman)
|
||||
require.NotNil(s.T(), container.SecurityContext)
|
||||
require.NotNil(s.T(), container.SecurityContext.Privileged)
|
||||
assert.True(s.T(), *container.SecurityContext.Privileged)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestCreateJobWithOwnerReferences tests owner reference handling
|
||||
func (s *JobsTestSuite) TestCreateJobWithOwnerReferences() {
|
||||
ownerRefs := []metav1.OwnerReference{
|
||||
{
|
||||
APIVersion: "raczylo.com/v1",
|
||||
Kind: "ClusterImage",
|
||||
Name: "test-image",
|
||||
UID: "test-uid-12345",
|
||||
},
|
||||
}
|
||||
|
||||
params := JobParams{
|
||||
Name: "owned-job",
|
||||
Namespace: "default",
|
||||
Image: "worker:latest",
|
||||
Commands: []string{"echo hello"},
|
||||
OwnerReferences: ownerRefs,
|
||||
}
|
||||
|
||||
job := CreateJob(params, func(raczylocomv1.ClusterImageExport) []string { return nil })
|
||||
|
||||
require.Len(s.T(), job.ObjectMeta.OwnerReferences, 1)
|
||||
assert.Equal(s.T(), "ClusterImage", job.ObjectMeta.OwnerReferences[0].Kind)
|
||||
assert.Equal(s.T(), "test-image", job.ObjectMeta.OwnerReferences[0].Name)
|
||||
}
|
||||
|
||||
// TestCreateJobCommands tests command concatenation
|
||||
func (s *JobsTestSuite) TestCreateJobCommands() {
|
||||
testCases := []struct {
|
||||
name string
|
||||
commands []string
|
||||
expectedArgsLen int
|
||||
expectedJoined string
|
||||
}{
|
||||
{
|
||||
name: "single command",
|
||||
commands: []string{"echo hello"},
|
||||
expectedArgsLen: 3, // /bin/bash, -c, "command"
|
||||
expectedJoined: "echo hello",
|
||||
},
|
||||
{
|
||||
name: "multiple commands",
|
||||
commands: []string{"echo hello", "echo world"},
|
||||
expectedArgsLen: 3,
|
||||
expectedJoined: "echo hello && echo world",
|
||||
},
|
||||
{
|
||||
name: "complex podman commands",
|
||||
commands: []string{
|
||||
"podman pull nginx:latest",
|
||||
"podman save --quiet -o /tmp/nginx.tar nginx:latest",
|
||||
"./worker export /tmp/nginx.tar s3://bucket/path",
|
||||
},
|
||||
expectedArgsLen: 3,
|
||||
expectedJoined: "podman pull nginx:latest && podman save --quiet -o /tmp/nginx.tar nginx:latest && ./worker export /tmp/nginx.tar s3://bucket/path",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
s.Run(tc.name, func() {
|
||||
params := JobParams{
|
||||
Name: "cmd-test",
|
||||
Namespace: "default",
|
||||
Image: "worker:latest",
|
||||
Commands: tc.commands,
|
||||
}
|
||||
|
||||
job := CreateJob(params, func(raczylocomv1.ClusterImageExport) []string { return nil })
|
||||
|
||||
container := job.Spec.Template.Spec.Containers[0]
|
||||
assert.Len(s.T(), container.Args, tc.expectedArgsLen)
|
||||
assert.Equal(s.T(), "/bin/bash", container.Args[0])
|
||||
assert.Equal(s.T(), "-c", container.Args[1])
|
||||
assert.Equal(s.T(), tc.expectedJoined, container.Args[2])
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestCreateJobBackoffAndTTL tests backoff limit and TTL settings
|
||||
func (s *JobsTestSuite) TestCreateJobBackoffAndTTL() {
|
||||
backoff := int32(5)
|
||||
ttl := int32(600)
|
||||
|
||||
params := JobParams{
|
||||
Name: "backoff-ttl-job",
|
||||
Namespace: "default",
|
||||
Image: "worker:latest",
|
||||
Commands: []string{"echo hello"},
|
||||
BackoffLimit: &backoff,
|
||||
TTLSecondsAfterFinished: &ttl,
|
||||
}
|
||||
|
||||
job := CreateJob(params, func(raczylocomv1.ClusterImageExport) []string { return nil })
|
||||
|
||||
require.NotNil(s.T(), job.Spec.BackoffLimit)
|
||||
assert.Equal(s.T(), int32(5), *job.Spec.BackoffLimit)
|
||||
|
||||
require.NotNil(s.T(), job.Spec.TTLSecondsAfterFinished)
|
||||
assert.Equal(s.T(), int32(600), *job.Spec.TTLSecondsAfterFinished)
|
||||
}
|
||||
|
||||
// TestSetupS3Params tests S3 parameter generation
|
||||
func (s *JobsTestSuite) TestSetupS3Params() {
|
||||
testCases := []struct {
|
||||
name string
|
||||
scenario TestScenario
|
||||
config raczylocomv1.ClusterImageStorageS3
|
||||
expectContains []string
|
||||
expectNotIn []string
|
||||
}{
|
||||
// Good scenarios
|
||||
{
|
||||
name: "basic credentials",
|
||||
scenario: ScenarioGood,
|
||||
config: raczylocomv1.ClusterImageStorageS3{
|
||||
Bucket: "my-bucket",
|
||||
Region: "us-east-1",
|
||||
AccessKey: "AKIAIOSFODNN7EXAMPLE",
|
||||
SecretKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
|
||||
},
|
||||
expectContains: []string{
|
||||
"--aws_access_key_id='AKIAIOSFODNN7EXAMPLE'",
|
||||
"--aws_secret_access_key='wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY'",
|
||||
},
|
||||
expectNotIn: []string{"--use_role", "--use_current_role"},
|
||||
},
|
||||
{
|
||||
name: "use current role (IRSA)",
|
||||
scenario: ScenarioGood,
|
||||
config: raczylocomv1.ClusterImageStorageS3{
|
||||
Bucket: "my-bucket",
|
||||
Region: "us-east-1",
|
||||
UseRole: true,
|
||||
},
|
||||
expectContains: []string{"--use_current_role"},
|
||||
expectNotIn: []string{"--aws_access_key_id", "--aws_secret_access_key"},
|
||||
},
|
||||
{
|
||||
name: "use specific role ARN",
|
||||
scenario: ScenarioGood,
|
||||
config: raczylocomv1.ClusterImageStorageS3{
|
||||
Bucket: "my-bucket",
|
||||
Region: "us-east-1",
|
||||
UseRole: true,
|
||||
RoleARN: "arn:aws:iam::123456789:role/MyRole",
|
||||
},
|
||||
expectContains: []string{
|
||||
"--use_role",
|
||||
"--role_name='arn:aws:iam::123456789:role/MyRole'",
|
||||
},
|
||||
expectNotIn: []string{"--aws_access_key_id", "--use_current_role"},
|
||||
},
|
||||
|
||||
// Not good scenarios
|
||||
{
|
||||
name: "with custom endpoint (MinIO)",
|
||||
scenario: ScenarioNotGood,
|
||||
config: raczylocomv1.ClusterImageStorageS3{
|
||||
Bucket: "my-bucket",
|
||||
Region: "us-east-1",
|
||||
AccessKey: "minioadmin",
|
||||
SecretKey: "minioadmin",
|
||||
Endpoint: "http://minio.local:9000",
|
||||
},
|
||||
expectContains: []string{
|
||||
"--endpoint_url='http://minio.local:9000'",
|
||||
"--aws_access_key_id='minioadmin'",
|
||||
},
|
||||
},
|
||||
|
||||
// Really bad scenarios
|
||||
{
|
||||
name: "empty credentials with UseRole false",
|
||||
scenario: ScenarioReallyBad,
|
||||
config: raczylocomv1.ClusterImageStorageS3{
|
||||
Bucket: "my-bucket",
|
||||
Region: "us-east-1",
|
||||
},
|
||||
expectContains: []string{
|
||||
"--aws_access_key_id=''",
|
||||
"--aws_secret_access_key=''",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
s.Run(tc.name, func() {
|
||||
params := SetupS3Params(tc.config)
|
||||
|
||||
// Check expected parameters are present
|
||||
for _, expected := range tc.expectContains {
|
||||
found := false
|
||||
for _, param := range params {
|
||||
if param == expected {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.True(s.T(), found, "Expected parameter not found: %s", expected)
|
||||
}
|
||||
|
||||
// Check unexpected parameters are not present
|
||||
for _, notExpected := range tc.expectNotIn {
|
||||
for _, param := range params {
|
||||
assert.NotContains(s.T(), param, notExpected, "Unexpected parameter found: %s", notExpected)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestJobParamsDefaults tests JobParams default handling
|
||||
func (s *JobsTestSuite) TestJobParamsDefaults() {
|
||||
s.Run("nil backoff limit", func() {
|
||||
params := JobParams{
|
||||
Name: "test",
|
||||
Namespace: "default",
|
||||
Image: "worker:latest",
|
||||
Commands: []string{"echo"},
|
||||
}
|
||||
|
||||
job := CreateJob(params, func(raczylocomv1.ClusterImageExport) []string { return nil })
|
||||
assert.Nil(s.T(), job.Spec.BackoffLimit)
|
||||
})
|
||||
|
||||
s.Run("nil TTL", func() {
|
||||
params := JobParams{
|
||||
Name: "test",
|
||||
Namespace: "default",
|
||||
Image: "worker:latest",
|
||||
Commands: []string{"echo"},
|
||||
}
|
||||
|
||||
job := CreateJob(params, func(raczylocomv1.ClusterImageExport) []string { return nil })
|
||||
assert.Nil(s.T(), job.Spec.TTLSecondsAfterFinished)
|
||||
})
|
||||
|
||||
s.Run("empty service account uses env var", func() {
|
||||
// This test verifies that when ServiceAccount is empty,
|
||||
// the job will use POD_SERVICE_ACCOUNT env var
|
||||
params := JobParams{
|
||||
Name: "test",
|
||||
Namespace: "default",
|
||||
Image: "worker:latest",
|
||||
Commands: []string{"echo"},
|
||||
ServiceAccount: "", // Empty
|
||||
}
|
||||
|
||||
job := CreateJob(params, func(raczylocomv1.ClusterImageExport) []string { return nil })
|
||||
// Service account will be set from environment variable POD_SERVICE_ACCOUNT
|
||||
// In tests, this will be empty, so we just verify the job is created
|
||||
require.NotNil(s.T(), job)
|
||||
})
|
||||
}
|
||||
|
||||
// TestSecretVolumeMounting tests that secrets are properly mounted
|
||||
func (s *JobsTestSuite) TestSecretVolumeMounting() {
|
||||
secrets := []corev1.LocalObjectReference{
|
||||
{Name: "docker-registry"},
|
||||
{Name: "gcr-json-key"},
|
||||
{Name: "ecr-credentials"},
|
||||
}
|
||||
|
||||
params := JobParams{
|
||||
Name: "secret-mount-test",
|
||||
Namespace: "default",
|
||||
Image: "worker:latest",
|
||||
Commands: []string{"echo"},
|
||||
ImagePullSecrets: secrets,
|
||||
}
|
||||
|
||||
job := CreateJob(params, func(raczylocomv1.ClusterImageExport) []string { return nil })
|
||||
podSpec := job.Spec.Template.Spec
|
||||
container := podSpec.Containers[0]
|
||||
|
||||
// Verify volumes are created
|
||||
require.Len(s.T(), podSpec.Volumes, 3)
|
||||
for i, vol := range podSpec.Volumes {
|
||||
assert.Equal(s.T(), secrets[i].Name, vol.VolumeSource.Secret.SecretName)
|
||||
assert.Contains(s.T(), vol.Name, "secret-")
|
||||
}
|
||||
|
||||
// Verify volume mounts
|
||||
require.Len(s.T(), container.VolumeMounts, 3)
|
||||
for i, mount := range container.VolumeMounts {
|
||||
assert.Contains(s.T(), mount.MountPath, ".docker-secret-")
|
||||
assert.True(s.T(), mount.ReadOnly)
|
||||
assert.Contains(s.T(), mount.Name, "secret-")
|
||||
// Verify index is correct
|
||||
expectedIndex := i
|
||||
assert.Equal(s.T(), "/home/runner/.docker-secret-"+string(rune('0'+expectedIndex)), mount.MountPath)
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function
|
||||
func int32Ptr(i int32) *int32 {
|
||||
return &i
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user