cmd/k8s-operator: allow specifying replicas for connectors (#16721)

This commit adds a `replicas` field to the `Connector` custom resource that
allows users to specify the number of desired replicas deployed for their
connectors.

This allows users to deploy exit nodes, subnet routers and app connectors
in a highly available fashion.

Fixes #14020

Signed-off-by: David Bond <davidsbond93@gmail.com>
This commit is contained in:
David Bond
2025-09-02 13:10:03 +01:00
committed by GitHub
parent d05e6dc09e
commit 12ad630128
13 changed files with 665 additions and 202 deletions

View File

@@ -25,7 +25,6 @@ import (
"k8s.io/client-go/tools/record"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
tsoperator "tailscale.com/k8s-operator"
tsapi "tailscale.com/k8s-operator/apis/v1alpha1"
"tailscale.com/kube/kubetypes"
@@ -176,6 +175,7 @@ func (a *ConnectorReconciler) maybeProvisionConnector(ctx context.Context, logge
if cn.Spec.Hostname != "" {
hostname = string(cn.Spec.Hostname)
}
crl := childResourceLabels(cn.Name, a.tsnamespace, "connector")
proxyClass := cn.Spec.ProxyClass
@@ -188,10 +188,17 @@ func (a *ConnectorReconciler) maybeProvisionConnector(ctx context.Context, logge
}
}
var replicas int32 = 1
if cn.Spec.Replicas != nil {
replicas = *cn.Spec.Replicas
}
sts := &tailscaleSTSConfig{
Replicas: replicas,
ParentResourceName: cn.Name,
ParentResourceUID: string(cn.UID),
Hostname: hostname,
HostnamePrefix: string(cn.Spec.HostnamePrefix),
ChildResourceLabels: crl,
Tags: cn.Spec.Tags.Stringify(),
Connector: &connector{
@@ -219,16 +226,19 @@ func (a *ConnectorReconciler) maybeProvisionConnector(ctx context.Context, logge
} else {
a.exitNodes.Remove(cn.UID)
}
if cn.Spec.SubnetRouter != nil {
a.subnetRouters.Add(cn.GetUID())
} else {
a.subnetRouters.Remove(cn.GetUID())
}
if cn.Spec.AppConnector != nil {
a.appConnectors.Add(cn.GetUID())
} else {
a.appConnectors.Remove(cn.GetUID())
}
a.mu.Unlock()
gaugeConnectorSubnetRouterResources.Set(int64(a.subnetRouters.Len()))
gaugeConnectorExitNodeResources.Set(int64(a.exitNodes.Len()))
@@ -244,21 +254,23 @@ func (a *ConnectorReconciler) maybeProvisionConnector(ctx context.Context, logge
return err
}
dev, err := a.ssr.DeviceInfo(ctx, crl, logger)
devices, err := a.ssr.DeviceInfo(ctx, crl, logger)
if err != nil {
return err
}
if dev == nil || dev.hostname == "" {
logger.Debugf("no Tailscale hostname known yet, waiting for Connector Pod to finish auth")
// No hostname yet. Wait for the connector pod to auth.
cn.Status.TailnetIPs = nil
cn.Status.Hostname = ""
return nil
cn.Status.Devices = make([]tsapi.ConnectorDevice, len(devices))
for i, dev := range devices {
cn.Status.Devices[i] = tsapi.ConnectorDevice{
Hostname: dev.hostname,
TailnetIPs: dev.ips,
}
}
cn.Status.TailnetIPs = dev.ips
cn.Status.Hostname = dev.hostname
if len(cn.Status.Devices) > 0 {
cn.Status.Hostname = cn.Status.Devices[0].Hostname
cn.Status.TailnetIPs = cn.Status.Devices[0].TailnetIPs
}
return nil
}
@@ -302,6 +314,15 @@ func (a *ConnectorReconciler) validate(cn *tsapi.Connector) error {
if (cn.Spec.SubnetRouter != nil || cn.Spec.ExitNode) && cn.Spec.AppConnector != nil {
return errors.New("invalid spec: a Connector that is configured as an app connector must not be also configured as a subnet router or exit node")
}
// These two checks should be caught by the Connector schema validation.
if cn.Spec.Replicas != nil && *cn.Spec.Replicas > 1 && cn.Spec.Hostname != "" {
return errors.New("invalid spec: a Connector that is configured with multiple replicas cannot specify a hostname. Instead, use a hostnamePrefix")
}
if cn.Spec.HostnamePrefix != "" && cn.Spec.Hostname != "" {
return errors.New("invalid spec: a Connect cannot use both a hostname and hostname prefix")
}
if cn.Spec.AppConnector != nil {
return validateAppConnector(cn.Spec.AppConnector)
}