mirror of
https://github.com/lukaszraczylo/kubernetes-images-sync-operator.git
synced 2026-10-12 00:45:27 +00:00
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.
548 lines
15 KiB
Go
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
|
|
}
|