Ctrl K
ComunidadGuías…

internal/actions/cronjob_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package actions

import (
	"context"
	"errors"
	"strings"
	"testing"

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/runtime"
	dynamicfake "k8s.io/client-go/dynamic/fake"
	k8sfake "k8s.io/client-go/kubernetes/fake"
	k8stesting "k8s.io/client-go/testing"

	"github.com/sophotechlabs/spinoza/internal/api"
)

func newCronJob(suspended bool) *unstructured.Unstructured {
	return &unstructured.Unstructured{Object: map[string]any{
		"apiVersion": "batch/v1",
		"kind":       "CronJob",
		"metadata": map[string]any{
			"name":      "nightly",
			"namespace": "shop",
			"uid":       "cron-uid",
		},
		"spec": map[string]any{
			"schedule": "0 2 * * *",
			"suspend":  suspended,
			"jobTemplate": map[string]any{
				"metadata": map[string]any{
					"labels":      map[string]any{"app": "backup"},
					"annotations": map[string]any{"owner": "platform"},
				},
				"spec": map[string]any{
					"backoffLimit": int64(2),
					"template": map[string]any{
						"spec": map[string]any{"restartPolicy": "Never"},
					},
				},
			},
		},
	}}
}

func cronJobRef() api.ObjectRef {
	return api.ObjectRef{
		Group:     "batch",
		Version:   "v1",
		Resource:  "cronjobs",
		Namespace: "shop",
		Name:      "nightly",
	}
}

func readCronJob(t *testing.T, client *dynamicfake.FakeDynamicClient) *unstructured.Unstructured {
	t.Helper()
	got, err := client.Resource(cronJobGVR).
		Namespace("shop").
		Get(context.Background(), "nightly", metav1.GetOptions{})
	if err != nil {
		t.Fatalf("read back: %v", err)
	}
	return got
}

func TestSuspendSetsTheFlag(t *testing.T) {
	client := dynClient(newCronJob(false))
	seen := recordPatches(client)
	service := serviceFor(client, k8sfake.NewClientset())

	result, err := service.Do(context.Background(), Request{Ref: cronJobRef(), Action: Suspend}, stamp)
	if err != nil {
		t.Fatalf("suspend: %v", err)
	}

	if len(*seen) != 1 {
		t.Fatalf("sent %d patches, want 1", len(*seen))
	}
	if (*seen)[0].body != `{"spec":{"suspend":true}}` {
		t.Fatalf("patch = %s", (*seen)[0].body)
	}
	if (*seen)[0].subresource != "" {
		t.Fatalf("subresource = %q, want the object itself", (*seen)[0].subresource)
	}
	if result.Action != string(Suspend) {
		t.Fatalf("action = %q, want suspend", result.Action)
	}
	if !strings.Contains(result.Message, "nightly") {
		t.Fatalf("message = %q, want it to name the cron job", result.Message)
	}
	if suspend, _, _ := unstructured.NestedBool(readCronJob(t, client).Object, "spec", "suspend"); !suspend {
		t.Fatal("the cron job is still running its schedule")
	}
}

func TestResumeClearsTheFlag(t *testing.T) {
	client := dynClient(newCronJob(true))
	seen := recordPatches(client)
	service := serviceFor(client, k8sfake.NewClientset())

	result, err := service.Do(context.Background(), Request{Ref: cronJobRef(), Action: Resume}, stamp)
	if err != nil {
		t.Fatalf("resume: %v", err)
	}

	if (*seen)[0].body != `{"spec":{"suspend":false}}` {
		t.Fatalf("patch = %s", (*seen)[0].body)
	}
	if result.Action != string(Resume) {
		t.Fatalf("action = %q, want resume", result.Action)
	}
	if suspend, _, _ := unstructured.NestedBool(readCronJob(t, client).Object, "spec", "suspend"); suspend {
		t.Fatal("the cron job is still suspended")
	}
}

func TestSuspendReportsARefusal(t *testing.T) {
	client := dynClient()
	service := serviceFor(client, k8sfake.NewClientset())

	_, err := service.Do(context.Background(), Request{Ref: cronJobRef(), Action: Suspend}, stamp)

	if err == nil {
		t.Fatal("a cron job that is not there was suspended anyway")
	}
}

func triggeredJob(t *testing.T, client *dynamicfake.FakeDynamicClient) *unstructured.Unstructured {
	t.Helper()
	list, err := client.Resource(jobGVR).Namespace("shop").List(context.Background(), metav1.ListOptions{})
	if err != nil {
		t.Fatalf("list jobs: %v", err)
	}
	if len(list.Items) != 1 {
		t.Fatalf("made %d jobs, want 1", len(list.Items))
	}
	return &list.Items[0]
}

func TestTriggerMakesAJobFromTheTemplate(t *testing.T) {
	client := dynClient(newCronJob(false))
	service := serviceFor(client, k8sfake.NewClientset())

	result, err := service.Do(context.Background(), Request{Ref: cronJobRef(), Action: Trigger}, stamp)
	if err != nil {
		t.Fatalf("trigger: %v", err)
	}

	job := triggeredJob(t, client)
	if job.GetAPIVersion() != "batch/v1" || job.GetKind() != "Job" {
		t.Fatalf("made a %s %s", job.GetAPIVersion(), job.GetKind())
	}
	limit, _, _ := unstructured.NestedInt64(job.Object, "spec", "backoffLimit")
	if limit != 2 {
		t.Fatalf("backoffLimit = %d, want the template's", limit)
	}
	policy, _, _ := unstructured.NestedString(job.Object, "spec", "template", "spec", "restartPolicy")
	if policy != "Never" {
		t.Fatalf("restartPolicy = %q, want the template's", policy)
	}
	if !strings.Contains(result.Message, job.GetName()) {
		t.Fatalf("message = %q, want it to name the job it started", result.Message)
	}
}

func TestATriggeredJobLeavesAUniqueNameToTheAPIServer(t *testing.T) {
	job, err := jobFrom(newCronJob(false))
	if err != nil {
		t.Fatalf("job from cron job: %v", err)
	}

	if job.GetName() != "" {
		t.Fatalf("job name = %q, want the apiserver to choose it", job.GetName())
	}
	if job.GetGenerateName() != "nightly-" {
		t.Fatalf("generated name prefix = %q, want nightly-", job.GetGenerateName())
	}
}

func TestATriggeredJobCarriesTheTemplatesLabelsAndAnnotations(t *testing.T) {
	client := dynClient(newCronJob(false))
	service := serviceFor(client, k8sfake.NewClientset())

	if _, err := service.Do(context.Background(), Request{Ref: cronJobRef(), Action: Trigger}, stamp); err != nil {
		t.Fatalf("trigger: %v", err)
	}

	job := triggeredJob(t, client)
	if job.GetLabels()["app"] != "backup" {
		t.Fatalf("labels = %v, want the template's", job.GetLabels())
	}
	if job.GetAnnotations()["owner"] != "platform" {
		t.Fatalf("annotations = %v, want the template's", job.GetAnnotations())
	}
	if job.GetAnnotations()[instantiateAnnotation] != "manual" {
		t.Fatalf("annotations = %v, want the run marked as started by hand", job.GetAnnotations())
	}
}

func TestATriggeredJobIsOwnedByTheCronJobButNotControlledByIt(t *testing.T) {
	client := dynClient(newCronJob(false))
	service := serviceFor(client, k8sfake.NewClientset())

	if _, err := service.Do(context.Background(), Request{Ref: cronJobRef(), Action: Trigger}, stamp); err != nil {
		t.Fatalf("trigger: %v", err)
	}

	owners := triggeredJob(t, client).GetOwnerReferences()
	if len(owners) != 1 {
		t.Fatalf("owners = %v, want the cron job alone", owners)
	}
	if owners[0].Kind != "CronJob" || owners[0].Name != "nightly" || string(owners[0].UID) != "cron-uid" {
		t.Fatalf("owner = %+v, want the cron job", owners[0])
	}
	if owners[0].Controller != nil && *owners[0].Controller {
		t.Fatal("the job was marked as controlled by the schedule")
	}
}

func TestATriggeredJobKeepsALabellessTemplateLabelless(t *testing.T) {
	cron := newCronJob(false)
	unstructured.RemoveNestedField(cron.Object, "spec", "jobTemplate", "metadata")
	client := dynClient(cron)
	service := serviceFor(client, k8sfake.NewClientset())

	if _, err := service.Do(context.Background(), Request{Ref: cronJobRef(), Action: Trigger}, stamp); err != nil {
		t.Fatalf("trigger: %v", err)
	}

	job := triggeredJob(t, client)
	if len(job.GetLabels()) != 0 {
		t.Fatalf("labels = %v, want none", job.GetLabels())
	}
	if job.GetAnnotations()[instantiateAnnotation] != "manual" {
		t.Fatalf("annotations = %v, want the run still marked", job.GetAnnotations())
	}
}

func TestACronJobWithNoTemplateCannotBeTriggered(t *testing.T) {
	cron := newCronJob(false)
	unstructured.RemoveNestedField(cron.Object, "spec", "jobTemplate")
	client := dynClient(cron)
	service := serviceFor(client, k8sfake.NewClientset())

	_, err := service.Do(context.Background(), Request{Ref: cronJobRef(), Action: Trigger}, stamp)

	if err == nil {
		t.Fatal("a cron job with no template was triggered anyway")
	}
	list, _ := client.Resource(jobGVR).Namespace("shop").List(context.Background(), metav1.ListOptions{})
	if len(list.Items) != 0 {
		t.Fatalf("made %d jobs anyway", len(list.Items))
	}
}

func TestTriggeringACronJobThatIsNotThereIsReported(t *testing.T) {
	service := serviceFor(dynClient(), k8sfake.NewClientset())

	_, err := service.Do(context.Background(), Request{Ref: cronJobRef(), Action: Trigger}, stamp)

	if err == nil {
		t.Fatal("a cron job that is not there was triggered anyway")
	}
}

func TestTriggerReportsWhenTheJobCannotBeCreated(t *testing.T) {
	client := dynClient(newCronJob(false))
	client.PrependReactor("create", "jobs", func(k8stesting.Action) (bool, runtime.Object, error) {
		return true, nil, errors.New("jobs are forbidden")
	})
	service := serviceFor(client, k8sfake.NewClientset())

	_, err := service.Do(context.Background(), Request{Ref: cronJobRef(), Action: Trigger}, stamp)

	if err == nil || !strings.Contains(err.Error(), "forbidden") {
		t.Fatalf("error = %v, want the create refusal", err)
	}
	list, listErr := client.Resource(jobGVR).Namespace("shop").List(context.Background(), metav1.ListOptions{})
	if listErr != nil {
		t.Fatalf("list jobs: %v", listErr)
	}
	if len(list.Items) != 0 {
		t.Fatalf("made %d jobs after the create was refused", len(list.Items))
	}
}