Files
Reloader/test/e2e/utils/workload_job.go
2026-01-08 11:06:45 +01:00

208 lines
6.2 KiB
Go

package utils
import (
"context"
"time"
batchv1 "k8s.io/api/batch/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/client-go/kubernetes"
)
// JobAdapter implements WorkloadAdapter for Kubernetes Jobs.
// Note: Jobs are handled specially by Reloader - they are recreated rather than updated.
type JobAdapter struct {
client kubernetes.Interface
}
// NewJobAdapter creates a new JobAdapter.
func NewJobAdapter(client kubernetes.Interface) *JobAdapter {
return &JobAdapter{client: client}
}
// Type returns the workload type.
func (a *JobAdapter) Type() WorkloadType {
return WorkloadJob
}
// Create creates a Job with the given config.
func (a *JobAdapter) Create(ctx context.Context, namespace, name string, cfg WorkloadConfig) error {
opts := buildJobOptions(cfg)
_, err := CreateJob(ctx, a.client, namespace, name, opts...)
return err
}
// Delete removes the Job.
func (a *JobAdapter) Delete(ctx context.Context, namespace, name string) error {
return DeleteJob(ctx, a.client, namespace, name)
}
// WaitReady waits for the Job to exist.
func (a *JobAdapter) WaitReady(ctx context.Context, namespace, name string, timeout time.Duration) error {
return WaitForJobExists(ctx, a.client, namespace, name, timeout)
}
// WaitReloaded waits for the Job to be recreated (new UID).
// For Jobs, Reloader recreates the Job rather than updating annotations.
func (a *JobAdapter) WaitReloaded(ctx context.Context, namespace, name, annotationKey string, timeout time.Duration) (bool, error) {
// For Jobs, we check if it was recreated by looking for a new UID
// This requires storing the original UID before the test
// For simplicity, we use the same pattern as other workloads
// The test should verify recreation using WaitForJobRecreated instead
return false, nil
}
// WaitEnvVar is not supported for Jobs as they don't use env var reload strategy.
func (a *JobAdapter) WaitEnvVar(ctx context.Context, namespace, name, prefix string, timeout time.Duration) (bool, error) {
return false, nil
}
// SupportsEnvVarStrategy returns false as Jobs don't support env var reload strategy.
func (a *JobAdapter) SupportsEnvVarStrategy() bool {
return false
}
// RequiresSpecialHandling returns true as Jobs are recreated by Reloader.
func (a *JobAdapter) RequiresSpecialHandling() bool {
return true
}
// GetOriginalUID retrieves the current UID of the Job for recreation verification.
func (a *JobAdapter) GetOriginalUID(ctx context.Context, namespace, name string) (string, error) {
job, err := GetJob(ctx, a.client, namespace, name)
if err != nil {
return "", err
}
return string(job.UID), nil
}
// WaitForRecreation waits for the Job to be recreated with a new UID.
func (a *JobAdapter) WaitForRecreation(ctx context.Context, namespace, name, originalUID string, timeout time.Duration) (string, bool, error) {
return WaitForJobRecreated(ctx, a.client, namespace, name, originalUID, timeout)
}
// buildJobOptions converts WorkloadConfig to JobOption slice.
func buildJobOptions(cfg WorkloadConfig) []JobOption {
var opts []JobOption
// Add annotations
if len(cfg.Annotations) > 0 {
opts = append(opts, WithJobAnnotations(cfg.Annotations))
}
// Add envFrom references
if cfg.UseConfigMapEnvFrom && cfg.ConfigMapName != "" {
opts = append(opts, WithJobConfigMapEnvFrom(cfg.ConfigMapName))
}
if cfg.UseSecretEnvFrom && cfg.SecretName != "" {
opts = append(opts, WithJobSecretEnvFrom(cfg.SecretName))
}
// Add volume mounts
if cfg.UseConfigMapVolume && cfg.ConfigMapName != "" {
opts = append(opts, WithJobConfigMapVolume(cfg.ConfigMapName))
}
if cfg.UseSecretVolume && cfg.SecretName != "" {
opts = append(opts, WithJobSecretVolume(cfg.SecretName))
}
// Add projected volume
if cfg.UseProjectedVolume {
opts = append(opts, WithJobProjectedVolume(cfg.ConfigMapName, cfg.SecretName))
}
return opts
}
// WithJobConfigMapVolume adds a volume mount for a ConfigMap to a Job.
func WithJobConfigMapVolume(name string) JobOption {
return func(j *batchv1.Job) {
volumeName := "cm-" + name
j.Spec.Template.Spec.Volumes = append(
j.Spec.Template.Spec.Volumes,
corev1.Volume{
Name: volumeName,
VolumeSource: corev1.VolumeSource{
ConfigMap: &corev1.ConfigMapVolumeSource{
LocalObjectReference: corev1.LocalObjectReference{Name: name},
},
},
},
)
j.Spec.Template.Spec.Containers[0].VolumeMounts = append(
j.Spec.Template.Spec.Containers[0].VolumeMounts,
corev1.VolumeMount{
Name: volumeName,
MountPath: "/etc/config/" + name,
},
)
}
}
// WithJobSecretVolume adds a volume mount for a Secret to a Job.
func WithJobSecretVolume(name string) JobOption {
return func(j *batchv1.Job) {
volumeName := "secret-" + name
j.Spec.Template.Spec.Volumes = append(
j.Spec.Template.Spec.Volumes,
corev1.Volume{
Name: volumeName,
VolumeSource: corev1.VolumeSource{
Secret: &corev1.SecretVolumeSource{
SecretName: name,
},
},
},
)
j.Spec.Template.Spec.Containers[0].VolumeMounts = append(
j.Spec.Template.Spec.Containers[0].VolumeMounts,
corev1.VolumeMount{
Name: volumeName,
MountPath: "/etc/secrets/" + name,
},
)
}
}
// WithJobProjectedVolume adds a projected volume with ConfigMap and/or Secret sources to a Job.
func WithJobProjectedVolume(cmName, secretName string) JobOption {
return func(j *batchv1.Job) {
volumeName := "projected-config"
sources := []corev1.VolumeProjection{}
if cmName != "" {
sources = append(sources, corev1.VolumeProjection{
ConfigMap: &corev1.ConfigMapProjection{
LocalObjectReference: corev1.LocalObjectReference{Name: cmName},
},
})
}
if secretName != "" {
sources = append(sources, corev1.VolumeProjection{
Secret: &corev1.SecretProjection{
LocalObjectReference: corev1.LocalObjectReference{Name: secretName},
},
})
}
j.Spec.Template.Spec.Volumes = append(
j.Spec.Template.Spec.Volumes,
corev1.Volume{
Name: volumeName,
VolumeSource: corev1.VolumeSource{
Projected: &corev1.ProjectedVolumeSource{
Sources: sources,
},
},
},
)
j.Spec.Template.Spec.Containers[0].VolumeMounts = append(
j.Spec.Template.Spec.Containers[0].VolumeMounts,
corev1.VolumeMount{
Name: volumeName,
MountPath: "/etc/projected",
},
)
}
}