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
116 lines
3.5 KiB
Go
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
|
|
}
|
|
}
|