diff --git a/README.md b/README.md index ac7ff966..8384ef31 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ - [Send start/finished event if the job config.yaml can't be found](#send-startfinished-event-if-the-job-configyaml-cant-be-found) - [Additional Event Data](#additional-event-data) - [Remote Control Plane](#remote-control-plane) + - [Job clean-up](#job-clean-up) - [How to validate a job configuration](#how-to-validate-a-job-configuration) - [Endless Possibilities](#endless-possibilities) - [Credits](#credits) @@ -567,6 +568,38 @@ If you are using the service in a remote control plane setup make sure the distr events used in the `job/config.yaml`. Just edit the `PUBSUB_TOPIC` environment variable in the distributor deployment configuration to fit your needs. +### Job clean-up + +Jobs objects are kept in kubernetes after completion to allow checking for status or logs inspections/retrieval. +This is not always desirable so kubernetes allows for [automatic clean-up of finished jobs](https://kubernetes.io/docs/concepts/workloads/controllers/ttlafterfinished/) +using `ttlSecondsAfterFinished` property in the job spec. + +Jobs created by the executor service will still be available for a time after (successful or failed) completion. +The default value of the time-to-live (TTL) for completed jobs is `21600` seconds (6 hours). + +In order to set a different TTL for jobs add the `ttlSecondsAfterFinished` property in the task definition, for example: + +```yaml +tasks: + - name: "Run locust tests" + files: + - locust/basic.py + - locust/import.py + - locust/locust.conf + image: "locustio/locust" + cmd: + - locust + args: + - '--config' + - /keptn/locust/locust.conf + - '-f' + - /keptn/locust/basic.py + - '--host' + - $(HOST) + # the corresponding job for this task will be cleaned up 10 minutes (600 seconds) after completion + ttlSecondsAfterFinished: 600 +``` + ## How to validate a job configuration `job-lint` is a simple cli tool that validates any given job configuration file and shows possible errors. diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index da640f1f..ed29609a 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -346,4 +346,50 @@ actions: assert.Check(t, cmp.Equal(config.Actions[0].Tasks[1].ImagePullPolicy, "Always")) assert.Check(t, cmp.Equal(config.Actions[0].Tasks[2].ImagePullPolicy, "Never")) assert.Check(t, cmp.Equal(config.Actions[0].Tasks[3].ImagePullPolicy, "IfNotPresent")) +} + +func TestTTLSecondsAfterFinished(t *testing.T) { + configYaml := ` +apiVersion: v2 +actions: + - name: "Run some fancy job with various ttl after job completes" + events: + - name: "sh.keptn.event.test.triggered" + jsonpath: + property: "$.test.teststrategy" + match: "health" + tasks: + - name: "task1-noinputttl" + workingDir: "/bin" + image: "somefancyimage" + cmd: + - cmd + args: + - arg1 + - arg2 + - name: "task2-ttl10mins" + workingDir: "/bin" + image: "somefancyimage" + cmd: + - cmd + ttlSecondsAfterFinished: 600 + - name: "task3-zerosecondsttl" + workingDir: "/bin" + image: "somefancyimage" + cmd: + - cmd + ttlSecondsAfterFinished: 0 + ` + + config, err := NewConfig([]byte(configYaml)) + + assert.NilError(t, err) + + assert.Equal(t, len(config.Actions), 1) + assert.Equal(t, len(config.Actions[0].Tasks), 3) + assert.Assert(t, config.Actions[0].Tasks[0].TTLSecondsAfterFinished == nil) + assert.Assert(t, config.Actions[0].Tasks[1].TTLSecondsAfterFinished != nil) + assert.Check(t, cmp.Equal(*config.Actions[0].Tasks[1].TTLSecondsAfterFinished, int32(600))) + assert.Assert(t, config.Actions[0].Tasks[2].TTLSecondsAfterFinished != nil) + assert.Check(t, cmp.Equal(*config.Actions[0].Tasks[2].TTLSecondsAfterFinished, int32(0))) } \ No newline at end of file diff --git a/pkg/k8sutils/job_test.go b/pkg/k8sutils/job_test.go index 0c563107..1d51f646 100644 --- a/pkg/k8sutils/job_test.go +++ b/pkg/k8sutils/job_test.go @@ -520,6 +520,79 @@ func TestImagePullPolicy(t *testing.T) { } +func TestTTLSecondsAfterFinished(t *testing.T) { + + var DefaultTTLAfterFinished int32 = 21600 + var TenMinutesTTLAfterFinished int32 = 600 + var ImmediatedlyDeletableTTLAfterFinished int32 = 0 + + tests := []struct { + name string + ttlSecondsAfterFinished *int32 + expectedTTLSecondsAfterFinished int32 + }{ + { + name: "No ttl specified in input - we should have the default job executor of 21600", + ttlSecondsAfterFinished: nil, + expectedTTLSecondsAfterFinished: DefaultTTLAfterFinished, + }, + { + name: "10 mins ttl specified in input", + ttlSecondsAfterFinished: &TenMinutesTTLAfterFinished, + expectedTTLSecondsAfterFinished: TenMinutesTTLAfterFinished, + }, + { + name: "0 seconds ttl specified in input - job eligible for deletion immediately", + ttlSecondsAfterFinished: &ImmediatedlyDeletableTTLAfterFinished, + expectedTTLSecondsAfterFinished: ImmediatedlyDeletableTTLAfterFinished, + }, + } + for i, test := range tests { + t.Run( + test.name, func(t *testing.T) { + + k8sClientSet := k8sfake.NewSimpleClientset() + k8s := k8sImpl{clientset: k8sClientSet} + + jobName:= fmt.Sprintf("ipp-job-%d", i) + + task := config.Task{ + Name: fmt.Sprintf("noIppTask-%d", i), + Image: "someImage:someversion", + Cmd: []string{"someCmd"}, + TTLSecondsAfterFinished: test.ttlSecondsAfterFinished, + } + + eventData := keptnv2.EventData{ + Project: "keptnproject", + Stage: "dev", + Service: "keptnservice", + } + + namespace := "test-namespace" + + err := k8s.CreateK8sJob( + jobName, &config.Action{Name: jobName}, task, &eventData, JobSettings{ + JobNamespace: namespace, + DefaultResourceRequirements: &corev1.ResourceRequirements{ + Limits: make(corev1.ResourceList), + Requests: make(corev1.ResourceList), + }, + }, "", namespace, + ) + assert.NilError(t, err) + + job, err := k8sClientSet.BatchV1().Jobs(namespace).Get(context.TODO(), jobName, metav1.GetOptions{}) + assert.NilError(t, err) + + assert.Assert(t, job.Spec.TTLSecondsAfterFinished != nil) + assert.Equal(t, *job.Spec.TTLSecondsAfterFinished, test.expectedTTLSecondsAfterFinished) + }, + ) + } + +} + func createK8sSecretObj(name string, namespace string, data map[string][]byte) *corev1.Secret { return &corev1.Secret{ TypeMeta: metav1.TypeMeta{