diff --git a/go.mod b/go.mod index b7dcffc..9514f6c 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.22 require ( github.com/goccy/go-yaml v1.11.3 + github.com/google/go-cmp v0.6.0 github.com/mattn/go-tty v0.0.5 k8s.io/api v0.29.3 k8s.io/apimachinery v0.29.3 diff --git a/main.go b/main.go index 98edbda..f624385 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,7 @@ package main import ( + "bytes" "context" "crypto/rand" "flag" @@ -8,6 +9,7 @@ import ( "os" "os/exec" "path/filepath" + "slices" "strings" "github.com/mattn/go-tty" @@ -126,7 +128,7 @@ func getNamespaceAndName(s []string) (namespace, name string, ok bool) { } func newJob(ctx context.Context, clientset *kubernetes.Clientset, namespace, name string) (*batchv1.Job, error) { - jobSpec, err := newJobTemplate(ctx, clientset, namespace, name) + jobSpec, ownerRef, err := newJobTemplate(ctx, clientset, namespace, name) if err != nil { return nil, err } @@ -141,18 +143,19 @@ func newJob(ctx context.Context, clientset *kubernetes.Clientset, namespace, nam Kind: "Job", }, ObjectMeta: metav1.ObjectMeta{ - Namespace: namespace, - Name: fmt.Sprintf("%s-%s", name, suffix), + Namespace: namespace, + Name: fmt.Sprintf("%s-%s", name, suffix), + OwnerReferences: []metav1.OwnerReference{ownerRef}, }, Spec: jobSpec, } return job, nil } -func newJobTemplate(ctx context.Context, clientset *kubernetes.Clientset, namespace, name string) (batchv1.JobSpec, error) { +func newJobTemplate(ctx context.Context, clientset *kubernetes.Clientset, namespace, name string) (jobSpec batchv1.JobSpec, ownerRef metav1.OwnerReference, err error) { v, err := clientset.ServerVersion() if err != nil { - return batchv1.JobSpec{}, fmt.Errorf("failed to get serverVersion: %w", err) + return jobSpec, ownerRef, fmt.Errorf("failed to get serverVersion: %w", err) } // When kubernetes version is 1.21 or higher, use batchv1.CronJob. @@ -160,16 +163,30 @@ func newJobTemplate(ctx context.Context, clientset *kubernetes.Clientset, namesp if isCronJobGA(v) { cj, err := clientset.BatchV1().CronJobs(namespace).Get(ctx, name, metav1.GetOptions{}) if err != nil { - return batchv1.JobSpec{}, err + return jobSpec, ownerRef, err } - return cj.Spec.JobTemplate.Spec, nil + ownerRef := metav1.OwnerReference{ + APIVersion: "batch/v1", + Kind: "CronJob", + Name: cj.GetName(), + UID: cj.GetUID(), + BlockOwnerDeletion: toPtr(true), + } + return cj.Spec.JobTemplate.Spec, ownerRef, nil } cj, err := clientset.BatchV1beta1().CronJobs(namespace).Get(ctx, name, metav1.GetOptions{}) if err != nil { - return batchv1.JobSpec{}, err + return jobSpec, ownerRef, err + } + ownerRef = metav1.OwnerReference{ + APIVersion: "batch/v1beta1", + Kind: "CronJob", + Name: cj.GetName(), + UID: cj.GetUID(), + BlockOwnerDeletion: toPtr(true), } - return cj.Spec.JobTemplate.Spec, nil + return cj.Spec.JobTemplate.Spec, ownerRef, nil } func isCronJobGA(v *version.Info) bool { @@ -214,7 +231,7 @@ func createJobWithFileName(filename *string, job *batchv1.Job) error { } func createJob(f *os.File, job *batchv1.Job) error { - data, err := yaml.Marshal(job) + data, err := jobToYaml(job) if err != nil { return err } @@ -269,3 +286,30 @@ func createJob(f *os.File, job *batchv1.Job) error { } return nil } + +func jobToYaml(job *batchv1.Job) ([]byte, error) { + // Marshal with ownerReferences commented out + ownerRefs, err := yaml.Marshal(map[string]any{"ownerReferences": job.ObjectMeta.OwnerReferences}) + if err != nil { + return nil, err + } + + job.ObjectMeta.OwnerReferences = nil + data, err := yaml.Marshal(job) + if err != nil { + return nil, err + } + + commentForOwnerRefs := " # " + commentedOwnerRefs := commentForOwnerRefs + strings.ReplaceAll(string(ownerRefs), "\n", "\n"+commentForOwnerRefs) + commentedOwnerRefs = strings.TrimSuffix(commentedOwnerRefs, commentForOwnerRefs) + namespaceInd := bytes.Index(data, []byte("namespace: ")) + namespaceLineInd := bytes.Index(data[namespaceInd:], []byte("\n")) + namespaceInd + + data = slices.Insert(data, namespaceLineInd+1, []byte(commentedOwnerRefs)...) + return data, nil +} + +func toPtr[T any](t T) *T { + return &t +} diff --git a/main_test.go b/main_test.go index ef3b3a0..d539ce1 100644 --- a/main_test.go +++ b/main_test.go @@ -3,6 +3,9 @@ package main import ( "testing" + "github.com/google/go-cmp/cmp" + batchv1 "k8s.io/api/batch/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/version" ) @@ -104,3 +107,64 @@ func TestIsCronJobGA(t *testing.T) { }) } } + +func TestJobToYaml(t *testing.T) { + tests := map[string]struct { + job *batchv1.Job + expect []byte + }{ + "should ownereReferences to commented out": { + job: &batchv1.Job{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "batch/v1", + Kind: "Job", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "default", + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: "batch/v1", + BlockOwnerDeletion: toPtr(true), + Kind: "CronJob", + Name: "test", + UID: "", + }, + }, + }, + }, + expect: []byte(`apiVersion: batch/v1 +kind: Job +metadata: + creationTimestamp: null + name: test + namespace: default + # ownerReferences: + # - apiVersion: batch/v1 + # blockOwnerDeletion: true + # kind: CronJob + # name: test + # uid: "" +spec: + template: + metadata: + creationTimestamp: null + spec: + containers: null +status: {} +`), + }, + } + + for n, tt := range tests { + t.Run(n, func(t *testing.T) { + got, err := jobToYaml(tt.job) + if err != nil { + t.Fatalf("jobToYaml got error: %v", err) + } + if diff := cmp.Diff(tt.expect, got); diff != "" { + t.Errorf("jobToYaml result diff (-expect, +got)\n%s", diff) + } + }) + } +}