Files
kubernetes-images-sync-oper…/internal/shared/jobs_test.go
T
lukaszraczylo 3880af56a7 new year update (#4)
* Bring operator to the brand new world of build and deployments.

* Clean up the code and basic improvements.

* More fixes, moving from python to golang worker.

* fixup! More fixes, moving from python to golang worker.

* fixup! fixup! More fixes, moving from python to golang worker.

* fixup! fixup! fixup! More fixes, moving from python to golang worker.

* fixup! fixup! fixup! fixup! More fixes, moving from python to golang worker.

* fixup! fixup! fixup! fixup! fixup! More fixes, moving from python to golang worker.

* fixup! fixup! fixup! fixup! fixup! fixup! More fixes, moving from python to golang worker.
2025-12-18 14:41:24 +00:00

548 lines
16 KiB
Go

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
}