From d050cfc5c70647097bd136bfca059a9e0da3d1e0 Mon Sep 17 00:00:00 2001 From: Lee Briggs Date: Mon, 5 May 2025 10:16:43 -0700 Subject: [PATCH] cmd/k8s-operator,k8s-operator: recorder custom service account Allows configuring a custom service account for the recorder pods, allowing the use of IRSA and other mechanisms for authing to write to recorder buckets Signed-off-by: Lee Briggs --- .../deploy/crds/tailscale.com_recorders.yaml | 7 +++ .../deploy/manifests/operator.yaml | 7 +++ cmd/k8s-operator/tsrecorder.go | 19 ++++--- cmd/k8s-operator/tsrecorder_specs.go | 51 +++++++++++-------- k8s-operator/api.md | 1 + k8s-operator/apis/v1alpha1/types_recorder.go | 8 +++ 6 files changed, 65 insertions(+), 28 deletions(-) diff --git a/cmd/k8s-operator/deploy/crds/tailscale.com_recorders.yaml b/cmd/k8s-operator/deploy/crds/tailscale.com_recorders.yaml index b07e9f692..542cf1049 100644 --- a/cmd/k8s-operator/deploy/crds/tailscale.com_recorders.yaml +++ b/cmd/k8s-operator/deploy/crds/tailscale.com_recorders.yaml @@ -1557,6 +1557,13 @@ spec: May also be set in PodSecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. type: string + serviceAccountName: + description: |- + The service account to use for the Recorder's StatefulSet. If not set, + the operator will create a service account with the same name as the + Recorder resource. + https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#service-account + type: string tolerations: description: |- Tolerations for Recorder Pods. By default, the operator does not apply diff --git a/cmd/k8s-operator/deploy/manifests/operator.yaml b/cmd/k8s-operator/deploy/manifests/operator.yaml index 9bfbd533f..e9bfa29d0 100644 --- a/cmd/k8s-operator/deploy/manifests/operator.yaml +++ b/cmd/k8s-operator/deploy/manifests/operator.yaml @@ -4552,6 +4552,13 @@ spec: type: string type: object type: object + serviceAccountName: + description: |- + The service account to use for the Recorder's StatefulSet. If not set, + the operator will create a service account with the same name as the + Recorder resource. + https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#service-account + type: string tolerations: description: |- Tolerations for Recorder Pods. By default, the operator does not apply diff --git a/cmd/k8s-operator/tsrecorder.go b/cmd/k8s-operator/tsrecorder.go index e9e6b2c6c..c12964a16 100644 --- a/cmd/k8s-operator/tsrecorder.go +++ b/cmd/k8s-operator/tsrecorder.go @@ -169,14 +169,17 @@ func (r *RecorderReconciler) maybeProvision(ctx context.Context, tsr *tsapi.Reco }); err != nil { return fmt.Errorf("error creating state Secret: %w", err) } - sa := tsrServiceAccount(tsr, r.tsNamespace) - if _, err := createOrUpdate(ctx, r.Client, r.tsNamespace, sa, func(s *corev1.ServiceAccount) { - s.ObjectMeta.Labels = sa.ObjectMeta.Labels - s.ObjectMeta.Annotations = sa.ObjectMeta.Annotations - s.ObjectMeta.OwnerReferences = sa.ObjectMeta.OwnerReferences - }); err != nil { - return fmt.Errorf("error creating ServiceAccount: %w", err) - } + // Create the ServiceAccount only if the user hasn't specified a custom name + if tsr.Spec.StatefulSet.Pod.ServiceAccountName == "" { + sa := tsrServiceAccount(tsr, r.tsNamespace) + if _, err := createOrUpdate(ctx, r.Client, r.tsNamespace, sa, func(s *corev1.ServiceAccount) { + s.ObjectMeta.Labels = sa.ObjectMeta.Labels + s.ObjectMeta.Annotations = sa.ObjectMeta.Annotations + s.ObjectMeta.OwnerReferences = sa.ObjectMeta.OwnerReferences + }); err != nil { + return fmt.Errorf("error creating ServiceAccount: %w", err) + } + } role := tsrRole(tsr, r.tsNamespace) if _, err := createOrUpdate(ctx, r.Client, r.tsNamespace, role, func(r *rbacv1.Role) { r.ObjectMeta.Labels = role.ObjectMeta.Labels diff --git a/cmd/k8s-operator/tsrecorder_specs.go b/cmd/k8s-operator/tsrecorder_specs.go index 4a7bf9887..47086559e 100644 --- a/cmd/k8s-operator/tsrecorder_specs.go +++ b/cmd/k8s-operator/tsrecorder_specs.go @@ -39,7 +39,13 @@ func tsrStatefulSet(tsr *tsapi.Recorder, namespace string) *appsv1.StatefulSet { Annotations: tsr.Spec.StatefulSet.Pod.Annotations, }, Spec: corev1.PodSpec{ - ServiceAccountName: tsr.Name, + ServiceAccountName: func() string { + if tsr.Spec.StatefulSet.Pod.ServiceAccountName != "" { + return tsr.Spec.StatefulSet.Pod.ServiceAccountName + } + + return tsr.Name + }(), Affinity: tsr.Spec.StatefulSet.Pod.Affinity, SecurityContext: tsr.Spec.StatefulSet.Pod.SecurityContext, ImagePullSecrets: tsr.Spec.StatefulSet.Pod.ImagePullSecrets, @@ -144,25 +150,30 @@ func tsrRole(tsr *tsapi.Recorder, namespace string) *rbacv1.Role { } func tsrRoleBinding(tsr *tsapi.Recorder, namespace string) *rbacv1.RoleBinding { - return &rbacv1.RoleBinding{ - ObjectMeta: metav1.ObjectMeta{ - Name: tsr.Name, - Namespace: namespace, - Labels: labels("recorder", tsr.Name, nil), - OwnerReferences: tsrOwnerReference(tsr), - }, - Subjects: []rbacv1.Subject{ - { - Kind: "ServiceAccount", - Name: tsr.Name, - Namespace: namespace, - }, - }, - RoleRef: rbacv1.RoleRef{ - Kind: "Role", - Name: tsr.Name, - }, - } + saName := tsr.Spec.StatefulSet.Pod.ServiceAccountName + if saName == "" { + saName = tsr.Name + } + + return &rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: tsr.Name, + Namespace: namespace, + Labels: labels("recorder", tsr.Name, nil), + OwnerReferences: tsrOwnerReference(tsr), + }, + Subjects: []rbacv1.Subject{ + { + Kind: "ServiceAccount", + Name: saName, + Namespace: namespace, + }, + }, + RoleRef: rbacv1.RoleRef{ + Kind: "Role", + Name: tsr.Name, + }, + } } func tsrAuthSecret(tsr *tsapi.Recorder, namespace string, authKey string) *corev1.Secret { diff --git a/k8s-operator/api.md b/k8s-operator/api.md index 190f99d24..89507ca10 100644 --- a/k8s-operator/api.md +++ b/k8s-operator/api.md @@ -726,6 +726,7 @@ _Appears in:_ | `imagePullSecrets` _[LocalObjectReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.3/#localobjectreference-v1-core) array_ | Image pull Secrets for Recorder Pods.
https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#PodSpec | | | | `nodeSelector` _object (keys:string, values:string)_ | Node selector rules for Recorder Pods. By default, the operator does
not apply any node selector rules.
https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#scheduling | | | | `tolerations` _[Toleration](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.3/#toleration-v1-core) array_ | Tolerations for Recorder Pods. By default, the operator does not apply
any tolerations.
https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#scheduling | | | +| `serviceAccountName` _string_ | The service account to use for the Recorder's StatefulSet. If not set,
the operator will create a service account with the same name as the
Recorder resource.
https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#service-account | | | #### RecorderSpec diff --git a/k8s-operator/apis/v1alpha1/types_recorder.go b/k8s-operator/apis/v1alpha1/types_recorder.go index 6e5416ea5..5fbad6ea6 100644 --- a/k8s-operator/apis/v1alpha1/types_recorder.go +++ b/k8s-operator/apis/v1alpha1/types_recorder.go @@ -94,6 +94,7 @@ type RecorderStatefulSet struct { // Configuration for pods created by the Recorder's StatefulSet. // +optional Pod RecorderPod `json:"pod,omitempty"` + } type RecorderPod struct { @@ -142,6 +143,13 @@ type RecorderPod struct { // https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#scheduling // +optional Tolerations []corev1.Toleration `json:"tolerations,omitempty"` + + // The service account to use for the Recorder's StatefulSet. If not set, + // the operator will create a service account with the same name as the + // Recorder resource. + // https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#service-account + // +optional + ServiceAccountName string `json:"serviceAccountName,omitempty"` } type RecorderContainer struct {