From 1a6255feb3f94d6b92ae375b000618a875bc1b86 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 Fixes #15875 Signed-off-by: Lee Briggs --- .../deploy/crds/tailscale.com_recorders.yaml | 7 ++++++ .../deploy/manifests/operator.yaml | 7 ++++++ cmd/k8s-operator/tsrecorder.go | 17 +++++++------ cmd/k8s-operator/tsrecorder_specs.go | 25 +++++++++++++------ k8s-operator/api.md | 1 + k8s-operator/apis/v1alpha1/types_recorder.go | 7 ++++++ 6 files changed, 50 insertions(+), 14 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..cf642d811 100644 --- a/cmd/k8s-operator/tsrecorder.go +++ b/cmd/k8s-operator/tsrecorder.go @@ -169,13 +169,16 @@ 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) { diff --git a/cmd/k8s-operator/tsrecorder_specs.go b/cmd/k8s-operator/tsrecorder_specs.go index 4a7bf9887..d7a21c713 100644 --- a/cmd/k8s-operator/tsrecorder_specs.go +++ b/cmd/k8s-operator/tsrecorder_specs.go @@ -39,12 +39,18 @@ func tsrStatefulSet(tsr *tsapi.Recorder, namespace string) *appsv1.StatefulSet { Annotations: tsr.Spec.StatefulSet.Pod.Annotations, }, Spec: corev1.PodSpec{ - ServiceAccountName: tsr.Name, - Affinity: tsr.Spec.StatefulSet.Pod.Affinity, - SecurityContext: tsr.Spec.StatefulSet.Pod.SecurityContext, - ImagePullSecrets: tsr.Spec.StatefulSet.Pod.ImagePullSecrets, - NodeSelector: tsr.Spec.StatefulSet.Pod.NodeSelector, - Tolerations: tsr.Spec.StatefulSet.Pod.Tolerations, + 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, + NodeSelector: tsr.Spec.StatefulSet.Pod.NodeSelector, + Tolerations: tsr.Spec.StatefulSet.Pod.Tolerations, Containers: []corev1.Container{ { Name: "recorder", @@ -144,6 +150,11 @@ func tsrRole(tsr *tsapi.Recorder, namespace string) *rbacv1.Role { } func tsrRoleBinding(tsr *tsapi.Recorder, namespace string) *rbacv1.RoleBinding { + saName := tsr.Spec.StatefulSet.Pod.ServiceAccountName + if saName == "" { + saName = tsr.Name + } + return &rbacv1.RoleBinding{ ObjectMeta: metav1.ObjectMeta{ Name: tsr.Name, @@ -154,7 +165,7 @@ func tsrRoleBinding(tsr *tsapi.Recorder, namespace string) *rbacv1.RoleBinding { Subjects: []rbacv1.Subject{ { Kind: "ServiceAccount", - Name: tsr.Name, + Name: saName, Namespace: namespace, }, }, 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..5f8cc4f7a 100644 --- a/k8s-operator/apis/v1alpha1/types_recorder.go +++ b/k8s-operator/apis/v1alpha1/types_recorder.go @@ -142,6 +142,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 {