mirror of
				https://github.com/tailscale/tailscale.git
				synced 2025-10-31 08:11:32 +01:00 
			
		
		
		
	Adds a new reconciler for ProxyGroups of type kube-apiserver that will provision a Tailscale Service for each replica to advertise. Adds two new condition types to the ProxyGroup, TailscaleServiceValid and TailscaleServiceConfigured, to post updates on the state of that reconciler in a way that's consistent with the service-pg reconciler. The created Tailscale Service name is configurable via a new ProxyGroup field spec.kubeAPISserver.ServiceName, which expects a string of the form "svc:<dns-label>". Lots of supporting changes were needed to implement this in a way that's consistent with other operator workflows, including: * Pulled containerboot's ensureServicesUnadvertised and certManager into kube/ libraries to be shared with k8s-proxy. Use those in k8s-proxy to aid Service cert sharing between replicas and graceful Service shutdown. * For certManager, add an initial wait to the cert loop to wait until the domain appears in the devices's netmap to avoid a guaranteed error on the first issue attempt when it's quick to start. * Made several methods in ingress-for-pg.go and svc-for-pg.go into functions to share with the new reconciler * Added a Resource struct to the owner refs stored in Tailscale Service annotations to be able to distinguish between Ingress- and ProxyGroup- based Services that need cleaning up in the Tailscale API. * Added a ListVIPServices method to the internal tailscale client to aid cleaning up orphaned Services * Support for reading config from a kube Secret, and partial support for config reloading, to prevent us having to force Pod restarts when config changes. * Fixed up the zap logger so it's possible to set debug log level. Updates #13358 Change-Id: Ia9607441157dd91fb9b6ecbc318eecbef446e116 Signed-off-by: Tom Proctor <tomhjp@users.noreply.github.com>
		
			
				
	
	
		
			190 lines
		
	
	
		
			7.6 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			190 lines
		
	
	
		
			7.6 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // Copyright (c) Tailscale Inc & AUTHORS
 | |
| // SPDX-License-Identifier: BSD-3-Clause
 | |
| 
 | |
| //go:build !plan9
 | |
| 
 | |
| package kube
 | |
| 
 | |
| import (
 | |
| 	"slices"
 | |
| 	"time"
 | |
| 
 | |
| 	"go.uber.org/zap"
 | |
| 	xslices "golang.org/x/exp/slices"
 | |
| 	corev1 "k8s.io/api/core/v1"
 | |
| 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 | |
| 	tsapi "tailscale.com/k8s-operator/apis/v1alpha1"
 | |
| 	"tailscale.com/tstime"
 | |
| )
 | |
| 
 | |
| // SetConnectorCondition ensures that Connector status has a condition with the
 | |
| // given attributes. LastTransitionTime gets set every time condition's status
 | |
| // changes.
 | |
| func SetConnectorCondition(cn *tsapi.Connector, conditionType tsapi.ConditionType, status metav1.ConditionStatus, reason, message string, gen int64, clock tstime.Clock, logger *zap.SugaredLogger) {
 | |
| 	conds := updateCondition(cn.Status.Conditions, conditionType, status, reason, message, gen, clock, logger)
 | |
| 	cn.Status.Conditions = conds
 | |
| }
 | |
| 
 | |
| // RemoveConnectorCondition will remove condition of the given type if it exists.
 | |
| func RemoveConnectorCondition(conn *tsapi.Connector, conditionType tsapi.ConditionType) {
 | |
| 	conn.Status.Conditions = slices.DeleteFunc(conn.Status.Conditions, func(cond metav1.Condition) bool {
 | |
| 		return cond.Type == string(conditionType)
 | |
| 	})
 | |
| }
 | |
| 
 | |
| // SetProxyClassCondition ensures that ProxyClass status has a condition with the
 | |
| // given attributes. LastTransitionTime gets set every time condition's status
 | |
| // changes.
 | |
| func SetProxyClassCondition(pc *tsapi.ProxyClass, conditionType tsapi.ConditionType, status metav1.ConditionStatus, reason, message string, gen int64, clock tstime.Clock, logger *zap.SugaredLogger) {
 | |
| 	conds := updateCondition(pc.Status.Conditions, conditionType, status, reason, message, gen, clock, logger)
 | |
| 	pc.Status.Conditions = conds
 | |
| }
 | |
| 
 | |
| // SetDNSConfigCondition ensures that DNSConfig status has a condition with the
 | |
| // given attributes. LastTransitionTime gets set every time condition's status
 | |
| // changes
 | |
| func SetDNSConfigCondition(dnsCfg *tsapi.DNSConfig, conditionType tsapi.ConditionType, status metav1.ConditionStatus, reason, message string, gen int64, clock tstime.Clock, logger *zap.SugaredLogger) {
 | |
| 	conds := updateCondition(dnsCfg.Status.Conditions, conditionType, status, reason, message, gen, clock, logger)
 | |
| 	dnsCfg.Status.Conditions = conds
 | |
| }
 | |
| 
 | |
| // SetServiceCondition ensures that Service status has a condition with the
 | |
| // given attributes. LastTransitionTime gets set every time condition's status
 | |
| // changes.
 | |
| func SetServiceCondition(svc *corev1.Service, conditionType tsapi.ConditionType, status metav1.ConditionStatus, reason, message string, clock tstime.Clock, logger *zap.SugaredLogger) {
 | |
| 	conds := updateCondition(svc.Status.Conditions, conditionType, status, reason, message, 0, clock, logger)
 | |
| 	svc.Status.Conditions = conds
 | |
| }
 | |
| 
 | |
| // GetServiceCondition returns Service condition with the specified type, if it exists on the Service.
 | |
| func GetServiceCondition(svc *corev1.Service, conditionType tsapi.ConditionType) *metav1.Condition {
 | |
| 	idx := xslices.IndexFunc(svc.Status.Conditions, func(cond metav1.Condition) bool {
 | |
| 		return cond.Type == string(conditionType)
 | |
| 	})
 | |
| 
 | |
| 	if idx == -1 {
 | |
| 		return nil
 | |
| 	}
 | |
| 	return &svc.Status.Conditions[idx]
 | |
| }
 | |
| 
 | |
| // RemoveServiceCondition will remove condition of the given type if it exists.
 | |
| func RemoveServiceCondition(svc *corev1.Service, conditionType tsapi.ConditionType) {
 | |
| 	svc.Status.Conditions = slices.DeleteFunc(svc.Status.Conditions, func(cond metav1.Condition) bool {
 | |
| 		return cond.Type == string(conditionType)
 | |
| 	})
 | |
| }
 | |
| 
 | |
| // SetRecorderCondition ensures that Recorder status has a condition with the
 | |
| // given attributes. LastTransitionTime gets set every time condition's status
 | |
| // changes.
 | |
| func SetRecorderCondition(tsr *tsapi.Recorder, conditionType tsapi.ConditionType, status metav1.ConditionStatus, reason, message string, gen int64, clock tstime.Clock, logger *zap.SugaredLogger) {
 | |
| 	conds := updateCondition(tsr.Status.Conditions, conditionType, status, reason, message, gen, clock, logger)
 | |
| 	tsr.Status.Conditions = conds
 | |
| }
 | |
| 
 | |
| // SetProxyGroupCondition ensures that ProxyGroup status has a condition with the
 | |
| // given attributes. LastTransitionTime gets set every time condition's status
 | |
| // changes.
 | |
| func SetProxyGroupCondition(pg *tsapi.ProxyGroup, conditionType tsapi.ConditionType, status metav1.ConditionStatus, reason, message string, gen int64, clock tstime.Clock, logger *zap.SugaredLogger) {
 | |
| 	conds := updateCondition(pg.Status.Conditions, conditionType, status, reason, message, gen, clock, logger)
 | |
| 	pg.Status.Conditions = conds
 | |
| }
 | |
| 
 | |
| func updateCondition(conds []metav1.Condition, conditionType tsapi.ConditionType, status metav1.ConditionStatus, reason, message string, gen int64, clock tstime.Clock, logger *zap.SugaredLogger) []metav1.Condition {
 | |
| 	newCondition := metav1.Condition{
 | |
| 		Type:               string(conditionType),
 | |
| 		Status:             status,
 | |
| 		Reason:             reason,
 | |
| 		Message:            message,
 | |
| 		ObservedGeneration: gen,
 | |
| 	}
 | |
| 
 | |
| 	nowTime := metav1.NewTime(clock.Now().Truncate(time.Second))
 | |
| 	newCondition.LastTransitionTime = nowTime
 | |
| 
 | |
| 	idx := xslices.IndexFunc(conds, func(cond metav1.Condition) bool {
 | |
| 		return cond.Type == string(conditionType)
 | |
| 	})
 | |
| 
 | |
| 	if idx == -1 {
 | |
| 		conds = append(conds, newCondition)
 | |
| 		return conds
 | |
| 	}
 | |
| 
 | |
| 	cond := conds[idx] // update the existing condition
 | |
| 
 | |
| 	// If this update doesn't contain a state transition, don't update last
 | |
| 	// transition time.
 | |
| 	if cond.Status == status {
 | |
| 		newCondition.LastTransitionTime = cond.LastTransitionTime
 | |
| 	} else {
 | |
| 		logger.Infof("Status change for condition %s from %s to %s", conditionType, cond.Status, status)
 | |
| 	}
 | |
| 	conds[idx] = newCondition
 | |
| 	return conds
 | |
| }
 | |
| 
 | |
| func ProxyClassIsReady(pc *tsapi.ProxyClass) bool {
 | |
| 	idx := xslices.IndexFunc(pc.Status.Conditions, func(cond metav1.Condition) bool {
 | |
| 		return cond.Type == string(tsapi.ProxyClassReady)
 | |
| 	})
 | |
| 	if idx == -1 {
 | |
| 		return false
 | |
| 	}
 | |
| 	cond := pc.Status.Conditions[idx]
 | |
| 	return cond.Status == metav1.ConditionTrue && cond.ObservedGeneration == pc.Generation
 | |
| }
 | |
| 
 | |
| func ProxyGroupIsReady(pg *tsapi.ProxyGroup) bool {
 | |
| 	cond := proxyGroupCondition(pg, tsapi.ProxyGroupReady)
 | |
| 	return cond != nil && cond.Status == metav1.ConditionTrue && cond.ObservedGeneration == pg.Generation
 | |
| }
 | |
| 
 | |
| func ProxyGroupAvailable(pg *tsapi.ProxyGroup) bool {
 | |
| 	cond := proxyGroupCondition(pg, tsapi.ProxyGroupAvailable)
 | |
| 	return cond != nil && cond.Status == metav1.ConditionTrue
 | |
| }
 | |
| 
 | |
| func KubeAPIServerProxyValid(pg *tsapi.ProxyGroup) (valid bool, set bool) {
 | |
| 	cond := proxyGroupCondition(pg, tsapi.KubeAPIServerProxyValid)
 | |
| 	return cond != nil && cond.Status == metav1.ConditionTrue && cond.ObservedGeneration == pg.Generation, cond != nil
 | |
| }
 | |
| 
 | |
| func KubeAPIServerProxyConfigured(pg *tsapi.ProxyGroup) bool {
 | |
| 	cond := proxyGroupCondition(pg, tsapi.KubeAPIServerProxyConfigured)
 | |
| 	return cond != nil && cond.Status == metav1.ConditionTrue && cond.ObservedGeneration == pg.Generation
 | |
| }
 | |
| 
 | |
| func proxyGroupCondition(pg *tsapi.ProxyGroup, condType tsapi.ConditionType) *metav1.Condition {
 | |
| 	idx := xslices.IndexFunc(pg.Status.Conditions, func(cond metav1.Condition) bool {
 | |
| 		return cond.Type == string(condType)
 | |
| 	})
 | |
| 	if idx == -1 {
 | |
| 		return nil
 | |
| 	}
 | |
| 	return &pg.Status.Conditions[idx]
 | |
| }
 | |
| 
 | |
| func DNSCfgIsReady(cfg *tsapi.DNSConfig) bool {
 | |
| 	idx := xslices.IndexFunc(cfg.Status.Conditions, func(cond metav1.Condition) bool {
 | |
| 		return cond.Type == string(tsapi.NameserverReady)
 | |
| 	})
 | |
| 	if idx == -1 {
 | |
| 		return false
 | |
| 	}
 | |
| 	cond := cfg.Status.Conditions[idx]
 | |
| 	return cond.Status == metav1.ConditionTrue && cond.ObservedGeneration == cfg.Generation
 | |
| }
 | |
| 
 | |
| func SvcIsReady(svc *corev1.Service) bool {
 | |
| 	idx := xslices.IndexFunc(svc.Status.Conditions, func(cond metav1.Condition) bool {
 | |
| 		return cond.Type == string(tsapi.ProxyReady)
 | |
| 	})
 | |
| 	if idx == -1 {
 | |
| 		return false
 | |
| 	}
 | |
| 	cond := svc.Status.Conditions[idx]
 | |
| 	return cond.Status == metav1.ConditionTrue
 | |
| }
 |