Files
tailscale/k8s-operator/reconciler/tailscaled/proxyclass.go
T
David BondandGitHub be0e460a20 cmd/k8s-operator,k8s-operator: Kubernetes Peer Relays (#20495)
This commit contains the Kubernetes implementation of peer relays via the new `PeerRelay` CRD. It's a mega branch consisting of the commits of other PRs gone into this work:

1. https://github.com/tailscale/tailscale/pull/20211
2. https://github.com/tailscale/tailscale/pull/20329
3. https://github.com/tailscale/tailscale/pull/20423
4. https://github.com/tailscale/tailscale/pull/20503

An instance of the `PeerRelay` CRD deploys a `StatefulSet` of containerboot instances configured to advertise themselves as peer relays using the IP addresses configured via `LoadBalancer` services on each cloud provider (with some AWS specifics as it's less automatic than its competing cloud providers). 

Per replica, a `LoadBalancer` type `Service` resource is provisioned and its IP address is used to configure the respective relay.

This has been tested with success in AWS, GCP & Azure and provides additional modification to `Service` resources via the CRD for any other kinds of deployment environments. It also contains some work that may appear to be duplication of what already exists within `cmd/k8s-operator` so we can start building an appropriate migration path for `Connector`, `ProxyGroup` etc into respective `k8s-operator/reconciler/*` packages.

Closes https://github.com/tailscale/corp/issues/34524
2026-07-20 16:37:15 +01:00

116 lines
3.5 KiB
Go

// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build !plan9
package tailscaled
import (
"slices"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
tsapi "tailscale.com/k8s-operator/apis/v1alpha1"
)
// ApplyProxyClass overlays the settings in pc onto ss. It's the generic slice of ProxyClass application used by
// any reconciler that produces a tailscaled StatefulSet (peer relay, connector, proxy group, etc.).
func ApplyProxyClass(ss *appsv1.StatefulSet, pc *tsapi.ProxyClass, managedLabels, managedAnnotations []string) *appsv1.StatefulSet {
if pc == nil || ss == nil || pc.Spec.StatefulSet == nil {
return ss
}
if wantsLabels := pc.Spec.StatefulSet.Labels.Parse(); len(wantsLabels) > 0 {
ss.ObjectMeta.Labels = mergeProtected(ss.ObjectMeta.Labels, wantsLabels, managedLabels)
}
if wantsAnnots := pc.Spec.StatefulSet.Annotations; len(wantsAnnots) > 0 {
ss.ObjectMeta.Annotations = mergeProtected(ss.ObjectMeta.Annotations, wantsAnnots, managedAnnotations)
}
if pc.Spec.StatefulSet.Pod == nil {
return ss
}
wantsPod := pc.Spec.StatefulSet.Pod
if wantsPodLabels := wantsPod.Labels.Parse(); len(wantsPodLabels) > 0 {
ss.Spec.Template.ObjectMeta.Labels = mergeProtected(ss.Spec.Template.ObjectMeta.Labels, wantsPodLabels, managedLabels)
}
if wantsPodAnnots := wantsPod.Annotations; len(wantsPodAnnots) > 0 {
ss.Spec.Template.ObjectMeta.Annotations = mergeProtected(ss.Spec.Template.ObjectMeta.Annotations, wantsPodAnnots, managedAnnotations)
}
ss.Spec.Template.Spec.SecurityContext = wantsPod.SecurityContext
ss.Spec.Template.Spec.ImagePullSecrets = wantsPod.ImagePullSecrets
ss.Spec.Template.Spec.NodeName = wantsPod.NodeName
ss.Spec.Template.Spec.NodeSelector = wantsPod.NodeSelector
ss.Spec.Template.Spec.Affinity = wantsPod.Affinity
ss.Spec.Template.Spec.Tolerations = wantsPod.Tolerations
ss.Spec.Template.Spec.PriorityClassName = wantsPod.PriorityClassName
ss.Spec.Template.Spec.TopologySpreadConstraints = wantsPod.TopologySpreadConstraints
if wantsPod.DNSPolicy != nil {
ss.Spec.Template.Spec.DNSPolicy = *wantsPod.DNSPolicy
}
if wantsPod.DNSConfig != nil {
ss.Spec.Template.Spec.DNSConfig = wantsPod.DNSConfig
}
if wantsPod.TailscaleContainer != nil {
for i := range ss.Spec.Template.Spec.Containers {
c := &ss.Spec.Template.Spec.Containers[i]
if c.Name != containerName {
continue
}
applyContainerOverlay(c, wantsPod.TailscaleContainer)
break
}
}
return ss
}
func mergeProtected(current, custom map[string]string, protected []string) map[string]string {
if custom == nil {
custom = make(map[string]string)
}
for k, v := range current {
if slices.Contains(protected, k) {
custom[k] = v
}
}
return custom
}
func applyContainerOverlay(c *corev1.Container, overlay *tsapi.Container) {
if overlay.SecurityContext != nil {
c.SecurityContext = overlay.SecurityContext
}
if len(overlay.Resources.Requests) > 0 {
c.Resources.Requests = overlay.Resources.Requests
}
if len(overlay.Resources.Limits) > 0 {
c.Resources.Limits = overlay.Resources.Limits
}
for _, e := range overlay.Env {
// Env vars added by ProxyClass are appended; Kubernetes uses the last entry for a duplicate name, so this
// lets the user override anything we set (e.g. TS_USERSPACE) without us having to know the full list.
c.Env = append(c.Env, corev1.EnvVar{Name: string(e.Name), Value: e.Value})
}
if overlay.Image != "" {
c.Image = overlay.Image
}
if overlay.ImagePullPolicy != "" {
c.ImagePullPolicy = overlay.ImagePullPolicy
}
}