Files
ayatori/internal/adapter/kubernetes/job.go
T
panxiao81 86effb72a8
Verify / test (pull_request) Successful in 7m25s
Verify / lint (pull_request) Successful in 8m9s
feat: execute jobs on Kubernetes
2026-09-18 18:20:10 +00:00

133 lines
4.0 KiB
Go

package kubernetes
import (
"fmt"
executionv1alpha1 "git.ddupan.top/panxiao81/ayatori/api/execution/v1alpha1"
batchv1 "k8s.io/api/batch/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
)
const (
ControllerName = "execution.ayatori.ddupan.top/kubernetes"
ReferenceType = "Job"
JobUIDLabel = "execution.ayatori.ddupan.top/job-uid"
)
var ayatoriJobGVK = schema.GroupVersionKind{
Group: executionv1alpha1.GroupVersion.Group,
Version: executionv1alpha1.GroupVersion.Version,
Kind: "Job",
}
// BuildJob translates the stable execution API into the Kubernetes adapter's
// backend object. It intentionally does not accept or expose a PodSpec.
func BuildJob(
job *executionv1alpha1.Job,
parameters *executionv1alpha1.KubernetesExecutionParameters,
resources executionv1alpha1.ExecutionResourceRequirements,
) *batchv1.Job {
backoffLimit := int32(0)
controller := true
blockOwnerDeletion := true
//nolint:modernize // ObjectMeta is promoted through embedded TypeMeta; embedlit produces invalid Go here.
return &batchv1.Job{
ObjectMeta: metav1.ObjectMeta{
Name: job.Name,
Namespace: job.Namespace,
Labels: map[string]string{
JobUIDLabel: string(job.UID),
},
OwnerReferences: []metav1.OwnerReference{{
APIVersion: ayatoriJobGVK.GroupVersion().String(),
Kind: ayatoriJobGVK.Kind,
Name: job.Name,
UID: job.UID,
Controller: &controller,
BlockOwnerDeletion: &blockOwnerDeletion,
}},
},
Spec: batchv1.JobSpec{
BackoffLimit: &backoffLimit,
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{JobUIDLabel: string(job.UID)}},
Spec: corev1.PodSpec{
RestartPolicy: corev1.RestartPolicyNever,
ServiceAccountName: parameters.Spec.ServiceAccountName,
RuntimeClassName: optionalString(parameters.Spec.RuntimeClassName),
NodeSelector: parameters.Spec.Scheduling.NodeSelector,
Tolerations: parameters.Spec.Scheduling.Tolerations,
SecurityContext: parameters.Spec.PodSecurityContext,
ImagePullSecrets: job.Spec.Task.ImagePullSecrets,
Containers: []corev1.Container{{
Name: "task",
Image: job.Spec.Task.Image,
ImagePullPolicy: parameters.Spec.ImagePullPolicy,
Command: job.Spec.Task.Command,
Args: job.Spec.Task.Args,
WorkingDir: job.Spec.Task.WorkingDir,
Env: environment(job.Spec.Task.Env),
Resources: resourceRequirements(resources),
}},
},
},
},
}
}
func ValidateOwnership(owner *executionv1alpha1.Job, backend *batchv1.Job) error {
if backend.Labels[JobUIDLabel] != string(owner.UID) {
return fmt.Errorf("backend Job %s/%s is not owned by Ayatori Job UID %s", backend.Namespace, backend.Name, owner.UID)
}
return nil
}
func environment(values []executionv1alpha1.EnvVar) []corev1.EnvVar {
result := make([]corev1.EnvVar, 0, len(values))
for _, value := range values {
env := corev1.EnvVar{Name: value.Name}
if value.Value != nil {
env.Value = *value.Value
}
if value.ValueFrom != nil {
env.ValueFrom = &corev1.EnvVarSource{
SecretKeyRef: value.ValueFrom.SecretKeyRef,
ConfigMapKeyRef: value.ValueFrom.ConfigMapKeyRef,
}
}
result = append(result, env)
}
return result
}
func resourceRequirements(resources executionv1alpha1.ExecutionResourceRequirements) corev1.ResourceRequirements {
return corev1.ResourceRequirements{
Requests: resourceList(resources.Requests),
Limits: resourceList(resources.Limits),
}
}
func resourceList(values executionv1alpha1.ResourceValues) corev1.ResourceList {
result := corev1.ResourceList{}
if values.CPU != nil {
result[corev1.ResourceCPU] = values.CPU.DeepCopy()
}
if values.Memory != nil {
result[corev1.ResourceMemory] = values.Memory.DeepCopy()
}
if len(result) == 0 {
return nil
}
return result
}
func optionalString(value string) *string {
if value == "" {
return nil
}
return &value
}