Files
kubernetes-images-sync-oper…/internal/shared/jobs_test.go
T
lukaszraczylo 2fdce1bf21 fix(lint): resolve all golangci-lint findings
Migrate the lint config to the v2 format.
Replace the deprecated scheme.Builder with runtime.NewSchemeBuilder.
Handle Close errors in the worker and replace repeated strings with constants.
Remove unused parameters and use switch statements where the linter asks.
2026-10-02 10:37:06 +01:00

548 lines
15 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.Name)
assert.Equal(s.T(), tc.expectNamespace, job.Namespace)
// Verify labels
assert.Equal(s.T(), "image-export", job.Labels["app"])
assert.Equal(s.T(), "image-export", job.Spec.Template.Labels["app"])
// Verify annotations if expected
if tc.expectAnnotations != nil {
for k, v := range tc.expectAnnotations {
assert.Equal(s.T(), v, job.Annotations[k])
assert.Equal(s.T(), v, job.Spec.Template.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.OwnerReferences, 1)
assert.Equal(s.T(), "ClusterImage", job.OwnerReferences[0].Kind)
assert.Equal(s.T(), "test-image", job.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.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
}