cmd/{k8s-operator,k8s-proxy}: add kube-apiserver ProxyGroup type (#16266)

Adds a new k8s-proxy command to convert operator's in-process proxy to
a separately deployable type of ProxyGroup: kube-apiserver. k8s-proxy
reads in a new config file written by the operator, modelled on tailscaled's
conffile but with some modifications to ensure multiple versions of the
config can co-exist within a file. This should make it much easier to
support reading that config file from a Kube Secret with a stable file name.

To avoid needing to give the operator ClusterRole{,Binding} permissions,
the helm chart now optionally deploys a new static ServiceAccount for
the API Server proxy to use if in auth mode.

Proxies deployed by kube-apiserver ProxyGroups currently work the same as
the operator's in-process proxy. They do not yet leverage Tailscale Services
for presenting a single HA DNS name.

Updates #13358

Change-Id: Ib6ead69b2173c5e1929f3c13fb48a9a5362195d8
Signed-off-by: Tom Proctor <tomhjp@users.noreply.github.com>
This commit is contained in:
Tom Proctor
2025-07-09 09:21:56 +01:00
committed by GitHub
parent 90bf0a97b3
commit 4dfed6b146
31 changed files with 1788 additions and 351 deletions

View File

@@ -26,6 +26,7 @@ import (
networkingv1 "k8s.io/api/networking/v1"
rbacv1 "k8s.io/api/rbac/v1"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
apiequality "k8s.io/apimachinery/pkg/api/equality"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/fields"
klabels "k8s.io/apimachinery/pkg/labels"
@@ -77,6 +78,7 @@ func main() {
tsNamespace = defaultEnv("OPERATOR_NAMESPACE", "")
tslogging = defaultEnv("OPERATOR_LOGGING", "info")
image = defaultEnv("PROXY_IMAGE", "tailscale/tailscale:latest")
k8sProxyImage = defaultEnv("K8S_PROXY_IMAGE", "tailscale/k8s-proxy:latest")
priorityClassName = defaultEnv("PROXY_PRIORITY_CLASS_NAME", "")
tags = defaultEnv("PROXY_TAGS", "tag:k8s")
tsFirewallMode = defaultEnv("PROXY_FIREWALL_MODE", "")
@@ -110,17 +112,27 @@ func main() {
// The operator can run either as a plain operator or it can
// additionally act as api-server proxy
// https://tailscale.com/kb/1236/kubernetes-operator/?q=kubernetes#accessing-the-kubernetes-control-plane-using-an-api-server-proxy.
mode := apiproxy.ParseAPIProxyMode()
if mode == apiproxy.APIServerProxyModeDisabled {
mode := parseAPIProxyMode()
if mode == apiServerProxyModeDisabled {
hostinfo.SetApp(kubetypes.AppOperator)
} else {
hostinfo.SetApp(kubetypes.AppAPIServerProxy)
hostinfo.SetApp(kubetypes.AppInProcessAPIServerProxy)
}
s, tsc := initTSNet(zlog, loginServer)
defer s.Close()
restConfig := config.GetConfigOrDie()
apiproxy.MaybeLaunchAPIServerProxy(zlog, restConfig, s, mode)
if mode != apiServerProxyModeDisabled {
ap, err := apiproxy.NewAPIServerProxy(zlog, restConfig, s, mode == apiServerProxyModeEnabled)
if err != nil {
zlog.Fatalf("error creating API server proxy: %v", err)
}
go func() {
if err := ap.Run(context.Background()); err != nil {
zlog.Fatalf("error running API server proxy: %v", err)
}
}()
}
rOpts := reconcilerOpts{
log: zlog,
tsServer: s,
@@ -128,6 +140,7 @@ func main() {
tailscaleNamespace: tsNamespace,
restConfig: restConfig,
proxyImage: image,
k8sProxyImage: k8sProxyImage,
proxyPriorityClassName: priorityClassName,
proxyActAsDefaultLoadBalancer: isDefaultLoadBalancer,
proxyTags: tags,
@@ -415,7 +428,6 @@ func runReconcilers(opts reconcilerOpts) {
Complete(&HAServiceReconciler{
recorder: eventRecorder,
tsClient: opts.tsClient,
tsnetServer: opts.tsServer,
defaultTags: strings.Split(opts.proxyTags, ","),
Client: mgr.GetClient(),
logger: opts.log.Named("service-pg-reconciler"),
@@ -625,13 +637,14 @@ func runReconcilers(opts reconcilerOpts) {
ownedByProxyGroupFilter := handler.EnqueueRequestForOwner(mgr.GetScheme(), mgr.GetRESTMapper(), &tsapi.ProxyGroup{})
proxyClassFilterForProxyGroup := handler.EnqueueRequestsFromMapFunc(proxyClassHandlerForProxyGroup(mgr.GetClient(), startlog))
nodeFilterForProxyGroup := handler.EnqueueRequestsFromMapFunc(nodeHandlerForProxyGroup(mgr.GetClient(), opts.defaultProxyClass, startlog))
saFilterForProxyGroup := handler.EnqueueRequestsFromMapFunc(serviceAccountHandlerForProxyGroup(mgr.GetClient(), startlog))
err = builder.ControllerManagedBy(mgr).
For(&tsapi.ProxyGroup{}).
Named("proxygroup-reconciler").
Watches(&corev1.Service{}, ownedByProxyGroupFilter).
Watches(&appsv1.StatefulSet{}, ownedByProxyGroupFilter).
Watches(&corev1.ConfigMap{}, ownedByProxyGroupFilter).
Watches(&corev1.ServiceAccount{}, ownedByProxyGroupFilter).
Watches(&corev1.ServiceAccount{}, saFilterForProxyGroup).
Watches(&corev1.Secret{}, ownedByProxyGroupFilter).
Watches(&rbacv1.Role{}, ownedByProxyGroupFilter).
Watches(&rbacv1.RoleBinding{}, ownedByProxyGroupFilter).
@@ -645,7 +658,8 @@ func runReconcilers(opts reconcilerOpts) {
tsClient: opts.tsClient,
tsNamespace: opts.tailscaleNamespace,
proxyImage: opts.proxyImage,
tsProxyImage: opts.proxyImage,
k8sProxyImage: opts.k8sProxyImage,
defaultTags: strings.Split(opts.proxyTags, ","),
tsFirewallMode: opts.proxyFirewallMode,
defaultProxyClass: opts.defaultProxyClass,
@@ -668,6 +682,7 @@ type reconcilerOpts struct {
tailscaleNamespace string // namespace in which operator resources will be deployed
restConfig *rest.Config // config for connecting to the kube API server
proxyImage string // <proxy-image-repo>:<proxy-image-tag>
k8sProxyImage string // <k8s-proxy-image-repo>:<k8s-proxy-image-tag>
// proxyPriorityClassName isPriorityClass to be set for proxy Pods. This
// is a legacy mechanism for cluster resource configuration options -
// going forward use ProxyClass.
@@ -996,8 +1011,8 @@ func nodeHandlerForProxyGroup(cl client.Client, defaultProxyClass string, logger
}
// proxyClassHandlerForProxyGroup returns a handler that, for a given ProxyClass,
// returns a list of reconcile requests for all Connectors that have
// .spec.proxyClass set.
// returns a list of reconcile requests for all ProxyGroups that have
// .spec.proxyClass set to that ProxyClass.
func proxyClassHandlerForProxyGroup(cl client.Client, logger *zap.SugaredLogger) handler.MapFunc {
return func(ctx context.Context, o client.Object) []reconcile.Request {
pgList := new(tsapi.ProxyGroupList)
@@ -1016,6 +1031,37 @@ func proxyClassHandlerForProxyGroup(cl client.Client, logger *zap.SugaredLogger)
}
}
// serviceAccountHandlerForProxyGroup returns a handler that, for a given ServiceAccount,
// returns a list of reconcile requests for all ProxyGroups that use that ServiceAccount.
// For most ProxyGroups, this will be a dedicated ServiceAccount owned by a specific
// ProxyGroup. But for kube-apiserver ProxyGroups running in auth mode, they use a shared
// static ServiceAccount named "kube-apiserver-auth-proxy".
func serviceAccountHandlerForProxyGroup(cl client.Client, logger *zap.SugaredLogger) handler.MapFunc {
return func(ctx context.Context, o client.Object) []reconcile.Request {
pgList := new(tsapi.ProxyGroupList)
if err := cl.List(ctx, pgList); err != nil {
logger.Debugf("error listing ProxyGroups for ServiceAccount: %v", err)
return nil
}
reqs := make([]reconcile.Request, 0)
saName := o.GetName()
for _, pg := range pgList.Items {
if saName == authAPIServerProxySAName && isAuthAPIServerProxy(&pg) {
reqs = append(reqs, reconcile.Request{NamespacedName: client.ObjectKeyFromObject(&pg)})
}
expectedOwner := pgOwnerReference(&pg)[0]
saOwnerRefs := o.GetOwnerReferences()
for _, ref := range saOwnerRefs {
if apiequality.Semantic.DeepEqual(ref, expectedOwner) {
reqs = append(reqs, reconcile.Request{NamespacedName: client.ObjectKeyFromObject(&pg)})
break
}
}
}
return reqs
}
}
// serviceHandlerForIngress returns a handler for Service events for ingress
// reconciler that ensures that if the Service associated with an event is of
// interest to the reconciler, the associated Ingress(es) gets be reconciled.