From e7db66f91a638759d9d95ef34fa22f59a8a37f9d Mon Sep 17 00:00:00 2001 From: Prakriti Mandal <98270250+prakrit55@users.noreply.github.com> Date: Thu, 2 Nov 2023 20:29:24 +0530 Subject: [PATCH] feat: add configurable service account to KeptnTasks (#2254) Signed-off-by: Griffin Signed-off-by: Prakriti Mandal <98270250+prakrit55@users.noreply.github.com> --- .../scripts/.helm-tests/default/result.yaml | 19 ++++ .../.helm-tests/lifecycle-only/result.yaml | 19 ++++ .../lifecycle-with-certs/result.yaml | 19 ++++ .../docs/crd-ref/lifecycle/v1alpha3/_index.md | 30 +++++++ .../v1alpha3/keptntaskdefinition_types.go | 26 ++++++ .../keptntaskdefinition_types_test.go | 40 +++++++++ .../v1alpha3/zz_generated.deepcopy.go | 45 ++++++++++ .../templates/keptntaskdefinition-crd.yaml | 21 ++++- ...fecycle.keptn.sh_keptntaskdefinitions.yaml | 20 +++++ .../lifecycle/keptntask/job_utils.go | 4 +- .../lifecycle/keptntask/job_utils_test.go | 86 +++++++++++++++++++ .../serviceaccount-in-jobs/00-assert.yaml | 7 ++ .../serviceaccount-in-jobs/00-install.yaml | 36 ++++++++ 13 files changed, 370 insertions(+), 2 deletions(-) create mode 100644 lifecycle-operator/apis/lifecycle/v1alpha3/keptntaskdefinition_types_test.go create mode 100644 test/integration/serviceaccount-in-jobs/00-assert.yaml create mode 100644 test/integration/serviceaccount-in-jobs/00-install.yaml diff --git a/.github/scripts/.helm-tests/default/result.yaml b/.github/scripts/.helm-tests/default/result.yaml index 0ec4b1b65e..1c7eb3051f 100644 --- a/.github/scripts/.helm-tests/default/result.yaml +++ b/.github/scripts/.helm-tests/default/result.yaml @@ -2720,6 +2720,16 @@ spec: spec: description: Spec describes the desired state of the KeptnTaskDefinition. properties: + automountServiceAccountToken: + description: automountServiceAccountToken allows to enable K8s to + assign cluster API credentials to a pod, if set to false the pod + will decline the serviceAccount + properties: + type: + type: boolean + required: + - type + type: object container: description: Container contains the definition for the container that is to be used in Job based on the KeptnTaskDefinitions. @@ -4212,6 +4222,15 @@ spec: attempt. format: int32 type: integer + serviceAccount: + description: Service Account to be used in jobs to authenticate with + the Kubernetes API and access cluster resources. + properties: + name: + type: string + required: + - name + type: object timeout: default: 5m description: Timeout specifies the maximum time to wait for the task diff --git a/.github/scripts/.helm-tests/lifecycle-only/result.yaml b/.github/scripts/.helm-tests/lifecycle-only/result.yaml index 08018cbc72..6a8c18d2a4 100644 --- a/.github/scripts/.helm-tests/lifecycle-only/result.yaml +++ b/.github/scripts/.helm-tests/lifecycle-only/result.yaml @@ -2666,6 +2666,16 @@ spec: spec: description: Spec describes the desired state of the KeptnTaskDefinition. properties: + automountServiceAccountToken: + description: automountServiceAccountToken allows to enable K8s to + assign cluster API credentials to a pod, if set to false the pod + will decline the serviceAccount + properties: + type: + type: boolean + required: + - type + type: object container: description: Container contains the definition for the container that is to be used in Job based on the KeptnTaskDefinitions. @@ -4158,6 +4168,15 @@ spec: attempt. format: int32 type: integer + serviceAccount: + description: Service Account to be used in jobs to authenticate with + the Kubernetes API and access cluster resources. + properties: + name: + type: string + required: + - name + type: object timeout: default: 5m description: Timeout specifies the maximum time to wait for the task diff --git a/.github/scripts/.helm-tests/lifecycle-with-certs/result.yaml b/.github/scripts/.helm-tests/lifecycle-with-certs/result.yaml index a358662401..c0f1a3ef6d 100644 --- a/.github/scripts/.helm-tests/lifecycle-with-certs/result.yaml +++ b/.github/scripts/.helm-tests/lifecycle-with-certs/result.yaml @@ -2681,6 +2681,16 @@ spec: spec: description: Spec describes the desired state of the KeptnTaskDefinition. properties: + automountServiceAccountToken: + description: automountServiceAccountToken allows to enable K8s to + assign cluster API credentials to a pod, if set to false the pod + will decline the serviceAccount + properties: + type: + type: boolean + required: + - type + type: object container: description: Container contains the definition for the container that is to be used in Job based on the KeptnTaskDefinitions. @@ -4173,6 +4183,15 @@ spec: attempt. format: int32 type: integer + serviceAccount: + description: Service Account to be used in jobs to authenticate with + the Kubernetes API and access cluster resources. + properties: + name: + type: string + required: + - name + type: object timeout: default: 5m description: Timeout specifies the maximum time to wait for the task diff --git a/docs/content/en/docs/crd-ref/lifecycle/v1alpha3/_index.md b/docs/content/en/docs/crd-ref/lifecycle/v1alpha3/_index.md index db4326caf1..acf804f2d2 100644 --- a/docs/content/en/docs/crd-ref/lifecycle/v1alpha3/_index.md +++ b/docs/content/en/docs/crd-ref/lifecycle/v1alpha3/_index.md @@ -34,6 +34,20 @@ Package v1alpha3 contains API Schema definitions for the lifecycle v1alpha3 API +#### AutomountServiceAccountTokenSpec + + + + + +_Appears in:_ +- [KeptnTaskDefinitionSpec](#keptntaskdefinitionspec) + +| Field | Description | +| --- | --- | +| `type` _boolean_ | | + + #### ConfigMapReference @@ -586,6 +600,8 @@ _Appears in:_ | `container` _[ContainerSpec](#containerspec)_ | Container contains the definition for the container that is to be used in Job based on the KeptnTaskDefinitions. | | `retries` _integer_ | Retries specifies how many times a job executing the KeptnTaskDefinition should be restarted in the case of an unsuccessful attempt. | | `timeout` _[Duration](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#duration-v1-meta)_ | Timeout specifies the maximum time to wait for the task to be completed successfully. If the task does not complete successfully within this time frame, it will be considered to be failed. | +| `serviceAccount` _[ServiceAccountSpec](#serviceaccountspec)_ | ServiceAccount specifies the service account to be used in jobs to authenticate with the Kubernetes API and access cluster resources. | +| `automountServiceAccountToken` _[AutomountServiceAccountTokenSpec](#automountserviceaccounttokenspec)_ | AutomountServiceAccountToken allows to enable K8s to assign cluster API credentials to a pod, if set to false the pod will decline the service account | #### KeptnTaskDefinitionStatus @@ -894,6 +910,20 @@ _Appears in:_ | `secret` _string_ | Secret contains the parameters that will be made available to the job executing the KeptnTask via the 'SECRET_DATA' environment variable. The 'SECRET_DATA' environment variable's content will the same as value of the 'SECRET_DATA' key of the referenced secret. | +#### ServiceAccountSpec + + + + + +_Appears in:_ +- [KeptnTaskDefinitionSpec](#keptntaskdefinitionspec) + +| Field | Description | +| --- | --- | +| `name` _string_ | | + + #### TaskContext diff --git a/lifecycle-operator/apis/lifecycle/v1alpha3/keptntaskdefinition_types.go b/lifecycle-operator/apis/lifecycle/v1alpha3/keptntaskdefinition_types.go index 70f130b2cd..e605a080fc 100644 --- a/lifecycle-operator/apis/lifecycle/v1alpha3/keptntaskdefinition_types.go +++ b/lifecycle-operator/apis/lifecycle/v1alpha3/keptntaskdefinition_types.go @@ -55,6 +55,11 @@ type KeptnTaskDefinitionSpec struct { // +kubebuilder:validation:Type:=string // +optional Timeout metav1.Duration `json:"timeout,omitempty"` + // ServiceAccount specifies the service account to be used in jobs to authenticate with the Kubernetes API and access cluster resources. + ServiceAccount *ServiceAccountSpec `json:"serviceAccount,omitempty"` + // AutomountServiceAccountToken allows to enable K8s to assign cluster API credentials to a pod, if set to false + // the pod will decline the service account + AutomountServiceAccountToken *AutomountServiceAccountTokenSpec `json:"automountServiceAccountToken,omitempty"` } type RuntimeSpec struct { @@ -104,6 +109,13 @@ type ContainerSpec struct { *v1.Container `json:",inline"` } +type AutomountServiceAccountTokenSpec struct { + Type *bool `json:"type"` +} +type ServiceAccountSpec struct { + Name string `json:"name"` +} + // KeptnTaskDefinitionStatus defines the observed state of KeptnTaskDefinition type KeptnTaskDefinitionStatus struct { // Function contains status information of the function definition for the task. @@ -142,3 +154,17 @@ type KeptnTaskDefinitionList struct { func init() { SchemeBuilder.Register(&KeptnTaskDefinition{}, &KeptnTaskDefinitionList{}) } + +func (d *KeptnTaskDefinition) GetServiceAccount() string { + if d.Spec.ServiceAccount == nil { + return "" + } + return d.Spec.ServiceAccount.Name +} + +func (d *KeptnTaskDefinition) GetAutomountServiceAccountToken() *bool { + if d.Spec.AutomountServiceAccountToken == nil { + return nil + } + return d.Spec.AutomountServiceAccountToken.Type +} diff --git a/lifecycle-operator/apis/lifecycle/v1alpha3/keptntaskdefinition_types_test.go b/lifecycle-operator/apis/lifecycle/v1alpha3/keptntaskdefinition_types_test.go new file mode 100644 index 0000000000..4d92cbad95 --- /dev/null +++ b/lifecycle-operator/apis/lifecycle/v1alpha3/keptntaskdefinition_types_test.go @@ -0,0 +1,40 @@ +package v1alpha3 + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestTaskDefinition_GetServiceAccountNoName(t *testing.T) { + d := &KeptnTaskDefinition{ + Spec: KeptnTaskDefinitionSpec{}, + } + svcAccname := d.GetServiceAccount() + require.Equal(t, svcAccname, "") +} + +func TestTaskDefinition_GetServiceAccountName(t *testing.T) { + sAName := "sva" + d := &KeptnTaskDefinition{ + Spec: KeptnTaskDefinitionSpec{ + ServiceAccount: &ServiceAccountSpec{ + Name: sAName, + }, + }, + } + svcAccname := d.GetServiceAccount() + require.Equal(t, svcAccname, sAName) +} + +func TestTaskDefinition_GetAutomountServiceAccountToken(t *testing.T) { + token := true + d := &KeptnTaskDefinition{ + Spec: KeptnTaskDefinitionSpec{ + AutomountServiceAccountToken: &AutomountServiceAccountTokenSpec{ + Type: &token, + }, + }, + } + require.True(t, *d.GetAutomountServiceAccountToken()) +} diff --git a/lifecycle-operator/apis/lifecycle/v1alpha3/zz_generated.deepcopy.go b/lifecycle-operator/apis/lifecycle/v1alpha3/zz_generated.deepcopy.go index 71736eecb0..45124e034e 100644 --- a/lifecycle-operator/apis/lifecycle/v1alpha3/zz_generated.deepcopy.go +++ b/lifecycle-operator/apis/lifecycle/v1alpha3/zz_generated.deepcopy.go @@ -27,6 +27,26 @@ import ( "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AutomountServiceAccountTokenSpec) DeepCopyInto(out *AutomountServiceAccountTokenSpec) { + *out = *in + if in.Type != nil { + in, out := &in.Type, &out.Type + *out = new(bool) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AutomountServiceAccountTokenSpec. +func (in *AutomountServiceAccountTokenSpec) DeepCopy() *AutomountServiceAccountTokenSpec { + if in == nil { + return nil + } + out := new(AutomountServiceAccountTokenSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ConfigMapReference) DeepCopyInto(out *ConfigMapReference) { *out = *in @@ -872,6 +892,16 @@ func (in *KeptnTaskDefinitionSpec) DeepCopyInto(out *KeptnTaskDefinitionSpec) { **out = **in } out.Timeout = in.Timeout + if in.ServiceAccount != nil { + in, out := &in.ServiceAccount, &out.ServiceAccount + *out = new(ServiceAccountSpec) + **out = **in + } + if in.AutomountServiceAccountToken != nil { + in, out := &in.AutomountServiceAccountToken, &out.AutomountServiceAccountToken + *out = new(AutomountServiceAccountTokenSpec) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KeptnTaskDefinitionSpec. @@ -1310,6 +1340,21 @@ func (in *SecureParameters) DeepCopy() *SecureParameters { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServiceAccountSpec) DeepCopyInto(out *ServiceAccountSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceAccountSpec. +func (in *ServiceAccountSpec) DeepCopy() *ServiceAccountSpec { + if in == nil { + return nil + } + out := new(ServiceAccountSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TaskContext) DeepCopyInto(out *TaskContext) { *out = *in diff --git a/lifecycle-operator/chart/templates/keptntaskdefinition-crd.yaml b/lifecycle-operator/chart/templates/keptntaskdefinition-crd.yaml index 6d50296004..49b7f900f5 100644 --- a/lifecycle-operator/chart/templates/keptntaskdefinition-crd.yaml +++ b/lifecycle-operator/chart/templates/keptntaskdefinition-crd.yaml @@ -195,6 +195,16 @@ spec: spec: description: Spec describes the desired state of the KeptnTaskDefinition. properties: + automountServiceAccountToken: + description: automountServiceAccountToken allows to enable K8s to + assign cluster API credentials to a pod, if set to false the pod + will decline the serviceAccount + properties: + type: + type: boolean + required: + - type + type: object container: description: Container contains the definition for the container that is to be used in Job based on the KeptnTaskDefinitions. @@ -1687,6 +1697,15 @@ spec: attempt. format: int32 type: integer + serviceAccount: + description: Service Account to be used in jobs to authenticate with + the Kubernetes API and access cluster resources. + properties: + name: + type: string + required: + - name + type: object timeout: default: 5m description: Timeout specifies the maximum time to wait for the task @@ -1712,4 +1731,4 @@ spec: served: true storage: true subresources: - status: {} \ No newline at end of file + status: {} diff --git a/lifecycle-operator/config/crd/bases/lifecycle.keptn.sh_keptntaskdefinitions.yaml b/lifecycle-operator/config/crd/bases/lifecycle.keptn.sh_keptntaskdefinitions.yaml index 188c53434e..8e4f0083c3 100644 --- a/lifecycle-operator/config/crd/bases/lifecycle.keptn.sh_keptntaskdefinitions.yaml +++ b/lifecycle-operator/config/crd/bases/lifecycle.keptn.sh_keptntaskdefinitions.yaml @@ -189,6 +189,16 @@ spec: spec: description: Spec describes the desired state of the KeptnTaskDefinition. properties: + automountServiceAccountToken: + description: AutomountServiceAccountToken allows to enable K8s to + assign cluster API credentials to a pod, if set to false the pod + will decline the service account + properties: + type: + type: boolean + required: + - type + type: object container: description: Container contains the definition for the container that is to be used in Job based on the KeptnTaskDefinitions. @@ -1681,6 +1691,16 @@ spec: attempt. format: int32 type: integer + serviceAccount: + description: ServiceAccount specifies the service account to be used + in jobs to authenticate with the Kubernetes API and access cluster + resources. + properties: + name: + type: string + required: + - name + type: object timeout: default: 5m description: Timeout specifies the maximum time to wait for the task diff --git a/lifecycle-operator/controllers/lifecycle/keptntask/job_utils.go b/lifecycle-operator/controllers/lifecycle/keptntask/job_utils.go index 4338119290..96bbc7b2d3 100644 --- a/lifecycle-operator/controllers/lifecycle/keptntask/job_utils.go +++ b/lifecycle-operator/controllers/lifecycle/keptntask/job_utils.go @@ -105,7 +105,9 @@ func (r *KeptnTaskReconciler) generateJob(ctx context.Context, task *klcv1alpha3 Annotations: task.Annotations, }, Spec: corev1.PodSpec{ - RestartPolicy: "OnFailure", + RestartPolicy: "OnFailure", + ServiceAccountName: definition.GetServiceAccount(), + AutomountServiceAccountToken: definition.GetAutomountServiceAccountToken(), }, }, BackoffLimit: task.Spec.Retries, diff --git a/lifecycle-operator/controllers/lifecycle/keptntask/job_utils_test.go b/lifecycle-operator/controllers/lifecycle/keptntask/job_utils_test.go index 791e003537..c5bdc1cab0 100644 --- a/lifecycle-operator/controllers/lifecycle/keptntask/job_utils_test.go +++ b/lifecycle-operator/controllers/lifecycle/keptntask/job_utils_test.go @@ -201,6 +201,66 @@ func TestKeptnTaskReconciler_updateTaskStatus(t *testing.T) { require.Equal(t, apicommon.StateSucceeded, task.Status.Status) } +func TestKeptnTaskReconciler_generateJob(t *testing.T) { + namespace := "default" + taskName := "my-task" + svcAccname := "svcAccname" + taskDefinitionName := "my-task-definition" + token := true + + taskDefinition := makeTaskDefinitionWithServiceAccount(taskDefinitionName, namespace, svcAccname, &token) + taskDefinition.Spec.ServiceAccount.Name = svcAccname + fakeClient := fakeclient.NewClient(taskDefinition) + task := makeTask(taskName, namespace, taskDefinitionName) + + r := &KeptnTaskReconciler{ + Client: fakeClient, + EventSender: controllercommon.NewK8sSender(record.NewFakeRecorder(100)), + Log: ctrl.Log.WithName("task-controller"), + Scheme: fakeClient.Scheme(), + } + + err := fakeClient.Create(context.TODO(), task) + require.Nil(t, err) + + ctx := context.TODO() + request := ctrl.Request{ + NamespacedName: types.NamespacedName{ + Namespace: namespace, + }, + } + + errTask := fakeClient.Get(context.TODO(), types.NamespacedName{ + Namespace: namespace, + Name: task.Name, + }, task) + require.Nil(t, errTask) + + errTaskDefinition := fakeClient.Get(context.TODO(), types.NamespacedName{ + Namespace: namespace, + Name: taskDefinition.Name, + }, taskDefinition) + require.Nil(t, errTaskDefinition) + + resultingJob, err := r.generateJob(ctx, task, taskDefinition, request) + require.Nil(t, err) + require.NotNil(t, resultingJob, "generateJob function return a valid Job") + + require.NotNil(t, resultingJob.Spec.Template.Spec.Containers) + require.Equal(t, resultingJob.Spec.Template.Spec.ServiceAccountName, svcAccname) + require.Equal(t, resultingJob.Spec.Template.Spec.AutomountServiceAccountToken, &token) + require.Equal(t, map[string]string{ + "label1": "label2", + }, resultingJob.Labels) + require.Equal(t, map[string]string{ + "annotation1": "annotation2", + "keptn.sh/app": "my-app", + "keptn.sh/task-name": "my-task", + "keptn.sh/version": "", + "keptn.sh/workload": "my-workload", + }, resultingJob.Annotations) +} + func makeJob(name, namespace string, status batchv1.JobStatus) *batchv1.Job { return &batchv1.Job{ ObjectMeta: metav1.ObjectMeta{ @@ -273,3 +333,29 @@ func makeConfigMap(name, namespace string) *v1.ConfigMap { }, } } + +func makeTaskDefinitionWithServiceAccount(name, namespace, serviceAccountName string, token *bool) *klcv1alpha3.KeptnTaskDefinition { + return &klcv1alpha3.KeptnTaskDefinition{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Labels: map[string]string{ + "label1": "label2", + }, + Annotations: map[string]string{ + "annotation1": "annotation2", + }, + }, + Spec: klcv1alpha3.KeptnTaskDefinitionSpec{ + Container: &klcv1alpha3.ContainerSpec{ + Container: &v1.Container{}, + }, + ServiceAccount: &klcv1alpha3.ServiceAccountSpec{ + Name: serviceAccountName, + }, + AutomountServiceAccountToken: &klcv1alpha3.AutomountServiceAccountTokenSpec{ + Type: token, + }, + }, + } +} diff --git a/test/integration/serviceaccount-in-jobs/00-assert.yaml b/test/integration/serviceaccount-in-jobs/00-assert.yaml new file mode 100644 index 0000000000..b38b50aba3 --- /dev/null +++ b/test/integration/serviceaccount-in-jobs/00-assert.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: Pod +status: + phase: Succeeded +spec: + serviceAccountName: klt-serviceaccount + automountServiceAccountToken: false diff --git a/test/integration/serviceaccount-in-jobs/00-install.yaml b/test/integration/serviceaccount-in-jobs/00-install.yaml new file mode 100644 index 0000000000..b300663f47 --- /dev/null +++ b/test/integration/serviceaccount-in-jobs/00-install.yaml @@ -0,0 +1,36 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: klt-serviceaccount +automountServiceAccountToken: false +--- +apiVersion: lifecycle.keptn.sh/v1alpha3 +kind: KeptnTaskDefinition +metadata: + name: task-serviceaccount +spec: + serviceAccount: + name: klt-serviceaccount + automountServiceAccountToken: + type: false + retries: 0 + timeout: 30s + container: + name: cowsay + image: rancher/cowsay:latest + args: + - 'hello world' +--- +apiVersion: lifecycle.keptn.sh/v1alpha3 +kind: KeptnTask +metadata: + name: sendserviceaccount +spec: + taskDefinition: task-serviceaccount + context: + appName: "my-app" + appVersion: "1.0.0" + objectType: "" + taskType: "" + workloadName: "my-workload" + workloadVersion: "1.0.0"