// Copyright (c) Tailscale Inc & AUTHORS // SPDX-License-Identifier: BSD-3-Clause //go:build !plan9 package main import ( "fmt" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" tsapi "tailscale.com/k8s-operator/apis/v1alpha1" "tailscale.com/types/ptr" "tailscale.com/version" ) func tsrStatefulSet(tsr *tsapi.Recorder, namespace string) *appsv1.StatefulSet { return &appsv1.StatefulSet{ ObjectMeta: metav1.ObjectMeta{ Name: tsr.Name, Namespace: namespace, Labels: labels("recorder", tsr.Name, tsr.Spec.StatefulSet.Labels), OwnerReferences: tsrOwnerReference(tsr), Annotations: tsr.Spec.StatefulSet.Annotations, }, Spec: appsv1.StatefulSetSpec{ Replicas: ptr.To[int32](1), Selector: &metav1.LabelSelector{ MatchLabels: labels("recorder", tsr.Name, tsr.Spec.StatefulSet.Pod.Labels), }, Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Name: tsr.Name, Namespace: namespace, Labels: labels("recorder", tsr.Name, tsr.Spec.StatefulSet.Pod.Labels), 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, Containers: []corev1.Container{ { Name: "recorder", Image: func() string { image := tsr.Spec.StatefulSet.Pod.Container.Image if image == "" { image = fmt.Sprintf("tailscale/tsrecorder:%s", selfVersionImageTag()) } return image }(), ImagePullPolicy: tsr.Spec.StatefulSet.Pod.Container.ImagePullPolicy, Resources: tsr.Spec.StatefulSet.Pod.Container.Resources, SecurityContext: tsr.Spec.StatefulSet.Pod.Container.SecurityContext, Env: env(tsr), EnvFrom: func() []corev1.EnvFromSource { if tsr.Spec.Storage.S3 == nil || tsr.Spec.Storage.S3.Credentials.Secret.Name == "" { return nil } return []corev1.EnvFromSource{{ SecretRef: &corev1.SecretEnvSource{ LocalObjectReference: corev1.LocalObjectReference{ Name: tsr.Spec.Storage.S3.Credentials.Secret.Name, }, }, }} }(), Command: []string{"/tsrecorder"}, VolumeMounts: []corev1.VolumeMount{ { Name: "data", MountPath: "/data", ReadOnly: false, }, }, }, }, Volumes: []corev1.Volume{ { Name: "data", VolumeSource: corev1.VolumeSource{ EmptyDir: &corev1.EmptyDirVolumeSource{}, }, }, }, }, }, }, } } func tsrServiceAccount(tsr *tsapi.Recorder, namespace string) *corev1.ServiceAccount { return &corev1.ServiceAccount{ ObjectMeta: metav1.ObjectMeta{ Name: tsr.Name, Namespace: namespace, Labels: labels("recorder", tsr.Name, nil), OwnerReferences: tsrOwnerReference(tsr), }, } } func tsrRole(tsr *tsapi.Recorder, namespace string) *rbacv1.Role { return &rbacv1.Role{ ObjectMeta: metav1.ObjectMeta{ Name: tsr.Name, Namespace: namespace, Labels: labels("recorder", tsr.Name, nil), OwnerReferences: tsrOwnerReference(tsr), }, Rules: []rbacv1.PolicyRule{ { APIGroups: []string{""}, Resources: []string{"secrets"}, Verbs: []string{ "get", "patch", "update", }, ResourceNames: []string{ tsr.Name, // Contains the auth key. fmt.Sprintf("%s-0", tsr.Name), // Contains the node state. }, }, { APIGroups: []string{""}, Resources: []string{"events"}, Verbs: []string{ "get", "create", "patch", }, }, }, } } 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, }, } } func tsrAuthSecret(tsr *tsapi.Recorder, namespace string, authKey string) *corev1.Secret { return &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Namespace: namespace, Name: tsr.Name, Labels: labels("recorder", tsr.Name, nil), OwnerReferences: tsrOwnerReference(tsr), }, StringData: map[string]string{ "authkey": authKey, }, } } func tsrStateSecret(tsr *tsapi.Recorder, namespace string) *corev1.Secret { return &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: fmt.Sprintf("%s-0", tsr.Name), Namespace: namespace, Labels: labels("recorder", tsr.Name, nil), OwnerReferences: tsrOwnerReference(tsr), }, } } func env(tsr *tsapi.Recorder) []corev1.EnvVar { envs := []corev1.EnvVar{ { Name: "TS_AUTHKEY", ValueFrom: &corev1.EnvVarSource{ SecretKeyRef: &corev1.SecretKeySelector{ LocalObjectReference: corev1.LocalObjectReference{ Name: tsr.Name, }, Key: "authkey", }, }, }, { Name: "POD_NAME", ValueFrom: &corev1.EnvVarSource{ FieldRef: &corev1.ObjectFieldSelector{ // Secret is named after the pod. FieldPath: "metadata.name", }, }, }, { Name: "POD_UID", ValueFrom: &corev1.EnvVarSource{ FieldRef: &corev1.ObjectFieldSelector{ FieldPath: "metadata.uid", }, }, }, { Name: "TS_STATE", Value: "kube:$(POD_NAME)", }, { Name: "TSRECORDER_HOSTNAME", Value: "$(POD_NAME)", }, } for _, env := range tsr.Spec.StatefulSet.Pod.Container.Env { envs = append(envs, corev1.EnvVar{ Name: string(env.Name), Value: env.Value, }) } if tsr.Spec.Storage.S3 != nil { envs = append(envs, corev1.EnvVar{ Name: "TSRECORDER_DST", Value: fmt.Sprintf("s3://%s", tsr.Spec.Storage.S3.Endpoint), }, corev1.EnvVar{ Name: "TSRECORDER_BUCKET", Value: tsr.Spec.Storage.S3.Bucket, }, ) } else { envs = append(envs, corev1.EnvVar{ Name: "TSRECORDER_DST", Value: "/data/recordings", }) } if tsr.Spec.EnableUI { envs = append(envs, corev1.EnvVar{ Name: "TSRECORDER_UI", Value: "true", }) } return envs } func labels(app, instance string, customLabels map[string]string) map[string]string { l := make(map[string]string, len(customLabels)+3) for k, v := range customLabels { l[k] = v } // ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/ l["app.kubernetes.io/name"] = app l["app.kubernetes.io/instance"] = instance l["app.kubernetes.io/managed-by"] = "tailscale-operator" return l } func tsrOwnerReference(owner metav1.Object) []metav1.OwnerReference { return []metav1.OwnerReference{*metav1.NewControllerRef(owner, tsapi.SchemeGroupVersion.WithKind("Recorder"))} } // selfVersionImageTag returns the container image tag of the running operator // build. func selfVersionImageTag() string { meta := version.GetMeta() var versionPrefix string if meta.UnstableBranch { versionPrefix = "unstable-" } return fmt.Sprintf("%sv%s", versionPrefix, meta.MajorMinorPatch) }