@ -77,12 +77,12 @@ type HAServiceReconciler struct {
// Reconcile reconciles Services that should be exposed over Tailscale in HA
// Reconcile reconciles Services that should be exposed over Tailscale in HA
// mode (on a ProxyGroup). It looks at all Services with
// mode (on a ProxyGroup). It looks at all Services with
// tailscale.com/proxy-group annotation. For each such Service, it ensures that
// tailscale.com/proxy-group annotation. For each such Service, it ensures that
// a VIP Service named after the hostname of the Service exists and is up to
// a Tailscale Service named after the hostname of the Service exists and is up to
// date.
// date.
// HA Servicees support multi-cluster Service setup.
// HA Servicees support multi-cluster Service setup.
// Each VIP Service contains a list of owner references that uniquely identify
// Each Tailscale Service contains a list of owner references that uniquely identify
// the operator. When an Service that acts as a
// the operator. When an Service that acts as a
// backend is being deleted, the corresponding VIP Service is only deleted if the
// backend is being deleted, the corresponding Tailscale Service is only deleted if the
// only owner reference that it contains is for this operator. If other owner
// only owner reference that it contains is for this operator. If other owner
// references are found, then cleanup operation only removes this operator's owner
// references are found, then cleanup operation only removes this operator's owner
// reference.
// reference.
@ -110,9 +110,9 @@ func (r *HAServiceReconciler) Reconcile(ctx context.Context, req reconcile.Reque
return res , err
return res , err
}
}
// needsRequeue is set to true if the underlying VIP Service has changed as a result of this reconcile. If that
// needsRequeue is set to true if the underlying Tailscale Service has changed as a result of this reconcile. If that
// is the case, we reconcile the Ingress one more time to ensure that concurrent updates to the VIP Service in a
// is the case, we reconcile the Ingress one more time to ensure that concurrent updates to the Tailscale Service in a
// multi-cluster Ingress setup have not resulted in another actor overwriting our VIP Service update.
// multi-cluster Ingress setup have not resulted in another actor overwriting our Tailscale Service update.
needsRequeue := false
needsRequeue := false
needsRequeue , err = r . maybeProvision ( ctx , hostname , svc , logger )
needsRequeue , err = r . maybeProvision ( ctx , hostname , svc , logger )
if err != nil {
if err != nil {
@ -129,14 +129,14 @@ func (r *HAServiceReconciler) Reconcile(ctx context.Context, req reconcile.Reque
return reconcile . Result { } , nil
return reconcile . Result { } , nil
}
}
// maybeProvision ensures that a VIP Service for this Ingress exists and is up to date and that the serve config for the
// maybeProvision ensures that a Tailscale Service for this Ingress exists and is up to date and that the serve config for the
// corresponding ProxyGroup contains the Ingress backend's definition.
// corresponding ProxyGroup contains the Ingress backend's definition.
// If a VIP Service does not exist, it will be created.
// If a Tailscale Service does not exist, it will be created.
// If a VIP Service exists, but only with owner references from other operator instances, an owner reference for this
// If a Tailscale Service exists, but only with owner references from other operator instances, an owner reference for this
// operator instance is added.
// operator instance is added.
// If a VIP Service exists, but does not have an owner reference from any operator, we error
// If a Tailscale Service exists, but does not have an owner reference from any operator, we error
// out assuming that this is an owner reference created by an unknown actor.
// out assuming that this is an owner reference created by an unknown actor.
// Returns true if the operation resulted in a VIP Service update.
// Returns true if the operation resulted in a Tailscale Service update.
func ( r * HAServiceReconciler ) maybeProvision ( ctx context . Context , hostname string , svc * corev1 . Service , logger * zap . SugaredLogger ) ( svcsChanged bool , err error ) {
func ( r * HAServiceReconciler ) maybeProvision ( ctx context . Context , hostname string , svc * corev1 . Service , logger * zap . SugaredLogger ) ( svcsChanged bool , err error ) {
oldSvcStatus := svc . Status . DeepCopy ( )
oldSvcStatus := svc . Status . DeepCopy ( )
defer func ( ) {
defer func ( ) {
@ -148,7 +148,7 @@ func (r *HAServiceReconciler) maybeProvision(ctx context.Context, hostname strin
pgName := svc . Annotations [ AnnotationProxyGroup ]
pgName := svc . Annotations [ AnnotationProxyGroup ]
if pgName == "" {
if pgName == "" {
logger . Infof ( "[unexpected] no ProxyGroup annotation, skipping VIP Service provisioning")
logger . Infof ( "[unexpected] no ProxyGroup annotation, skipping Tailscale Service provisioning")
return false , nil
return false , nil
}
}
@ -194,23 +194,23 @@ func (r *HAServiceReconciler) maybeProvision(ctx context.Context, hostname strin
r . mu . Unlock ( )
r . mu . Unlock ( )
}
}
// 1. Ensure that if Service's hostname/name has changed, any VIP Service
// 1. Ensure that if Service's hostname/name has changed, any Tailscale Service
// resources corresponding to the old hostname are cleaned up.
// resources corresponding to the old hostname are cleaned up.
// In practice, this function will ensure that any VIP Services that are
// In practice, this function will ensure that any Tailscale Services that are
// associated with the provided ProxyGroup and no longer owned by a
// associated with the provided ProxyGroup and no longer owned by a
// Service are cleaned up. This is fine- it is not expensive and ensures
// Service are cleaned up. This is fine- it is not expensive and ensures
// that in edge cases (a single update changed both hostname and removed
// that in edge cases (a single update changed both hostname and removed
// ProxyGroup annotation) the VIP Service is more likely to be
// ProxyGroup annotation) the Tailscale Service is more likely to be
// (eventually) removed.
// (eventually) removed.
svcsChanged , err = r . maybeCleanupProxyGroup ( ctx , pgName , logger )
svcsChanged , err = r . maybeCleanupProxyGroup ( ctx , pgName , logger )
if err != nil {
if err != nil {
return false , fmt . Errorf ( "failed to cleanup VIP Service resources for ProxyGroup: %w", err )
return false , fmt . Errorf ( "failed to cleanup Tailscale Service resources for ProxyGroup: %w", err )
}
}
// 2. Ensure that there isn't a VIP Service with the same hostname
// 2. Ensure that there isn't a Tailscale Service with the same hostname
// already created and not owned by this Service.
// already created and not owned by this Service.
serviceName := tailcfg . ServiceName ( "svc:" + hostname )
serviceName := tailcfg . ServiceName ( "svc:" + hostname )
existing VIP Svc, err := r . tsClient . GetVIPService ( ctx , serviceName )
existing TS Svc, err := r . tsClient . GetVIPService ( ctx , serviceName )
if isErrorFeatureFlagNotEnabled ( err ) {
if isErrorFeatureFlagNotEnabled ( err ) {
logger . Warn ( msgFeatureFlagNotEnabled )
logger . Warn ( msgFeatureFlagNotEnabled )
r . recorder . Event ( svc , corev1 . EventTypeWarning , warningTailscaleServiceFeatureFlagNotEnabled , msgFeatureFlagNotEnabled )
r . recorder . Event ( svc , corev1 . EventTypeWarning , warningTailscaleServiceFeatureFlagNotEnabled , msgFeatureFlagNotEnabled )
@ -220,16 +220,16 @@ func (r *HAServiceReconciler) maybeProvision(ctx context.Context, hostname strin
return false , fmt . Errorf ( "error getting Tailscale Service %q: %w" , hostname , err )
return false , fmt . Errorf ( "error getting Tailscale Service %q: %w" , hostname , err )
}
}
// 3. Generate the VIP Service owner annotation for new or existing Tailscale Service.
// 3. Generate the Tailscale Service owner annotation for new or existing Tailscale Service.
// This checks and ensures that VIP Service's owner references are updated
// This checks and ensures that Tailscale Service's owner references are updated
// for this Service and errors if that is not possible (i.e. because it
// for this Service and errors if that is not possible (i.e. because it
// appears that the VIP Service has been created by a non-operator actor).
// appears that the Tailscale Service has been created by a non-operator actor).
updatedAnnotations , err := r . ownerAnnotations ( existing VIP Svc)
updatedAnnotations , err := r . ownerAnnotations ( existing TS Svc)
if err != nil {
if err != nil {
instr := fmt . Sprintf ( "To proceed, you can either manually delete the existing Tailscale Service or choose a different hostname with the '%s' annotaion" , AnnotationHostname )
instr := fmt . Sprintf ( "To proceed, you can either manually delete the existing Tailscale Service or choose a different hostname with the '%s' annotaion" , AnnotationHostname )
msg := fmt . Sprintf ( "error ensuring ownership of VIP Service %s: %v. %s", hostname , err , instr )
msg := fmt . Sprintf ( "error ensuring ownership of Tailscale Service %s: %v. %s", hostname , err , instr )
logger . Warn ( msg )
logger . Warn ( msg )
r . recorder . Event ( svc , corev1 . EventTypeWarning , "Invalid VIP Service", msg )
r . recorder . Event ( svc , corev1 . EventTypeWarning , "Invalid Tailscale Service", msg )
tsoperator . SetServiceCondition ( svc , tsapi . IngressSvcValid , metav1 . ConditionFalse , reasonIngressSvcInvalid , msg , r . clock , logger )
tsoperator . SetServiceCondition ( svc , tsapi . IngressSvcValid , metav1 . ConditionFalse , reasonIngressSvcInvalid , msg , r . clock , logger )
return false , nil
return false , nil
}
}
@ -239,28 +239,28 @@ func (r *HAServiceReconciler) maybeProvision(ctx context.Context, hostname strin
tags = strings . Split ( tstr , "," )
tags = strings . Split ( tstr , "," )
}
}
vip Svc := & tailscale . VIPService {
ts Svc := & tailscale . VIPService {
Name : serviceName ,
Name : serviceName ,
Tags : tags ,
Tags : tags ,
Ports : [ ] string { "do-not-validate" } , // we don't want to validate ports
Ports : [ ] string { "do-not-validate" } , // we don't want to validate ports
Comment : managedTSServiceComment ,
Comment : managedTSServiceComment ,
Annotations : updatedAnnotations ,
Annotations : updatedAnnotations ,
}
}
if existing VIP Svc != nil {
if existing TS Svc != nil {
vipSvc. Addrs = existingVIP Svc. Addrs
tsSvc. Addrs = existingTS Svc. Addrs
}
}
// TODO(irbekrm): right now if two Service resources attempt to apply different VIP Service configs (different
// TODO(irbekrm): right now if two Service resources attempt to apply different Tailscale Service configs (different
// tags) we can end up reconciling those in a loop. We should detect when a Service
// tags) we can end up reconciling those in a loop. We should detect when a Service
// with the same generation number has been reconciled ~more than N times and stop attempting to apply updates.
// with the same generation number has been reconciled ~more than N times and stop attempting to apply updates.
if existing VIP Svc == nil ||
if existing TS Svc == nil ||
! reflect . DeepEqual ( vipSvc. Tags , existingVIP Svc. Tags ) ||
! reflect . DeepEqual ( tsSvc. Tags , existingTS Svc. Tags ) ||
! ownersAreSetAndEqual ( vipSvc, existingVIP Svc) {
! ownersAreSetAndEqual ( tsSvc, existingTS Svc) {
logger . Infof ( "Ensuring VIP Service exists and is up to date")
logger . Infof ( "Ensuring Tailscale Service exists and is up to date")
if err := r . tsClient . CreateOrUpdateVIPService ( ctx , vip Svc) ; err != nil {
if err := r . tsClient . CreateOrUpdateVIPService ( ctx , ts Svc) ; err != nil {
return false , fmt . Errorf ( "error creating VIP Service: %w", err )
return false , fmt . Errorf ( "error creating Tailscale Service: %w", err )
}
}
existing VIPSvc = vip Svc
existing TSSvc = ts Svc
}
}
cm , cfgs , err := ingressSvcsConfigs ( ctx , r . Client , pgName , r . tsNamespace )
cm , cfgs , err := ingressSvcsConfigs ( ctx , r . Client , pgName , r . tsNamespace )
@ -272,29 +272,29 @@ func (r *HAServiceReconciler) maybeProvision(ctx context.Context, hostname strin
return false , nil
return false , nil
}
}
if existing VIP Svc. Addrs == nil {
if existing TS Svc. Addrs == nil {
existing VIP Svc, err = r . tsClient . GetVIPService ( ctx , vip Svc. Name )
existing TS Svc, err = r . tsClient . GetVIPService ( ctx , ts Svc. Name )
if err != nil {
if err != nil {
return false , fmt . Errorf ( "error getting VIP Service: %w", err )
return false , fmt . Errorf ( "error getting Tailscale Service: %w", err )
}
}
if existing VIP Svc. Addrs == nil {
if existing TS Svc. Addrs == nil {
// TODO(irbekrm): this should be a retry
// TODO(irbekrm): this should be a retry
return false , fmt . Errorf ( "unexpected: VIP Service addresses not populated")
return false , fmt . Errorf ( "unexpected: Tailscale Service addresses not populated")
}
}
}
}
var vip v4 netip . Addr
var tsSvcIP v4 netip . Addr
var vip v6 netip . Addr
var tsSvcIP v6 netip . Addr
for _ , vip := range existingVIP Svc. Addrs {
for _ , tsip := range existingTS Svc. Addrs {
ip , err := netip . ParseAddr ( v ip)
ip , err := netip . ParseAddr ( ts ip)
if err != nil {
if err != nil {
return false , fmt . Errorf ( "error parsing Tailscale Service address: %w" , err )
return false , fmt . Errorf ( "error parsing Tailscale Service address: %w" , err )
}
}
if ip . Is4 ( ) {
if ip . Is4 ( ) {
vip v4 = ip
tsSvcIP v4 = ip
} else if ip . Is6 ( ) {
} else if ip . Is6 ( ) {
vip v6 = ip
tsSvcIP v6 = ip
}
}
}
}
@ -308,12 +308,12 @@ func (r *HAServiceReconciler) maybeProvision(ctx context.Context, hostname strin
if ip . Is4 ( ) {
if ip . Is4 ( ) {
cfg . IPv4Mapping = & ingressservices . Mapping {
cfg . IPv4Mapping = & ingressservices . Mapping {
ClusterIP : ip ,
ClusterIP : ip ,
TailscaleServiceIP : vip v4,
TailscaleServiceIP : tsSvcIP v4,
}
}
} else if ip . Is6 ( ) {
} else if ip . Is6 ( ) {
cfg . IPv6Mapping = & ingressservices . Mapping {
cfg . IPv6Mapping = & ingressservices . Mapping {
ClusterIP : ip ,
ClusterIP : ip ,
TailscaleServiceIP : vip v6,
TailscaleServiceIP : tsSvcIP v6,
}
}
}
}
}
}
@ -332,7 +332,7 @@ func (r *HAServiceReconciler) maybeProvision(ctx context.Context, hostname strin
}
}
logger . Infof ( "updating AdvertiseServices config" )
logger . Infof ( "updating AdvertiseServices config" )
// 4. Update tailscaled's AdvertiseServices config, which should add the VIP Service
// 4. Update tailscaled's AdvertiseServices config, which should add the Tailscale Service
// IPs to the ProxyGroup Pods' AllowedIPs in the next netmap update if approved.
// IPs to the ProxyGroup Pods' AllowedIPs in the next netmap update if approved.
if err = r . maybeUpdateAdvertiseServicesConfig ( ctx , svc , pg . Name , serviceName , & cfg , true , logger ) ; err != nil {
if err = r . maybeUpdateAdvertiseServicesConfig ( ctx , svc , pg . Name , serviceName , & cfg , true , logger ) ; err != nil {
return false , fmt . Errorf ( "failed to update tailscaled config: %w" , err )
return false , fmt . Errorf ( "failed to update tailscaled config: %w" , err )
@ -343,7 +343,7 @@ func (r *HAServiceReconciler) maybeProvision(ctx context.Context, hostname strin
return false , fmt . Errorf ( "failed to get number of advertised Pods: %w" , err )
return false , fmt . Errorf ( "failed to get number of advertised Pods: %w" , err )
}
}
// TODO(irbekrm): here and when creating the VIP Service, verify if the
// TODO(irbekrm): here and when creating the Tailscale Service, verify if the
// error is not terminal (and therefore should not be reconciled). For
// error is not terminal (and therefore should not be reconciled). For
// example, if the hostname is already a hostname of a Tailscale node,
// example, if the hostname is already a hostname of a Tailscale node,
// the GET here will fail.
// the GET here will fail.
@ -362,7 +362,7 @@ func (r *HAServiceReconciler) maybeProvision(ctx context.Context, hostname strin
lbs = [ ] corev1 . LoadBalancerIngress {
lbs = [ ] corev1 . LoadBalancerIngress {
{
{
Hostname : dnsName ,
Hostname : dnsName ,
IP : vip v4. String ( ) ,
IP : tsSvcIP v4. String ( ) ,
} ,
} ,
}
}
@ -376,8 +376,8 @@ func (r *HAServiceReconciler) maybeProvision(ctx context.Context, hostname strin
return svcsChanged , nil
return svcsChanged , nil
}
}
// maybeCleanup ensures that any resources, such as a VIP Service created for this Service, are cleaned up when the
// maybeCleanup ensures that any resources, such as a Tailscale Service created for this Service, are cleaned up when the
// Service is being deleted or is unexposed. The cleanup is safe for a multi-cluster setup- the VIP Service is only
// Service is being deleted or is unexposed. The cleanup is safe for a multi-cluster setup- the Tailscale Service is only
// deleted if it does not contain any other owner references. If it does the cleanup only removes the owner reference
// deleted if it does not contain any other owner references. If it does the cleanup only removes the owner reference
// corresponding to this Service.
// corresponding to this Service.
func ( r * HAServiceReconciler ) maybeCleanup ( ctx context . Context , hostname string , svc * corev1 . Service , logger * zap . SugaredLogger ) ( svcChanged bool , err error ) {
func ( r * HAServiceReconciler ) maybeCleanup ( ctx context . Context , hostname string , svc * corev1 . Service , logger * zap . SugaredLogger ) ( svcChanged bool , err error ) {
@ -387,7 +387,7 @@ func (r *HAServiceReconciler) maybeCleanup(ctx context.Context, hostname string,
logger . Debugf ( "no finalizer, nothing to do" )
logger . Debugf ( "no finalizer, nothing to do" )
return false , nil
return false , nil
}
}
logger . Infof ( "Ensuring that VIP Service %q configuration is cleaned up", hostname )
logger . Infof ( "Ensuring that Tailscale Service %q configuration is cleaned up", hostname )
defer func ( ) {
defer func ( ) {
if err != nil {
if err != nil {
@ -397,13 +397,13 @@ func (r *HAServiceReconciler) maybeCleanup(ctx context.Context, hostname string,
} ( )
} ( )
serviceName := tailcfg . ServiceName ( "svc:" + hostname )
serviceName := tailcfg . ServiceName ( "svc:" + hostname )
// 1. Clean up the VIP Service.
// 1. Clean up the Tailscale Service.
svcChanged , err = r . cleanup VIP Service( ctx , serviceName , logger )
svcChanged , err = r . cleanup Tailscale Service( ctx , serviceName , logger )
if err != nil {
if err != nil {
return false , fmt . Errorf ( "error deleting VIP Service: %w", err )
return false , fmt . Errorf ( "error deleting Tailscale Service: %w", err )
}
}
// 2. Unadvertise the VIP Service.
// 2. Unadvertise the Tailscale Service.
pgName := svc . Annotations [ AnnotationProxyGroup ]
pgName := svc . Annotations [ AnnotationProxyGroup ]
if err = r . maybeUpdateAdvertiseServicesConfig ( ctx , svc , pgName , serviceName , nil , false , logger ) ; err != nil {
if err = r . maybeUpdateAdvertiseServicesConfig ( ctx , svc , pgName , serviceName , nil , false , logger ) ; err != nil {
return false , fmt . Errorf ( "failed to update tailscaled config services: %w" , err )
return false , fmt . Errorf ( "failed to update tailscaled config services: %w" , err )
@ -419,7 +419,7 @@ func (r *HAServiceReconciler) maybeCleanup(ctx context.Context, hostname string,
if cm == nil || cfgs == nil {
if cm == nil || cfgs == nil {
return true , nil
return true , nil
}
}
logger . Infof ( "Removing VIP Service %q from ingress config for ProxyGroup %q", hostname , pgName )
logger . Infof ( "Removing Tailscale Service %q from ingress config for ProxyGroup %q", hostname , pgName )
delete ( cfgs , serviceName . String ( ) )
delete ( cfgs , serviceName . String ( ) )
cfgBytes , err := json . Marshal ( cfgs )
cfgBytes , err := json . Marshal ( cfgs )
if err != nil {
if err != nil {
@ -429,8 +429,8 @@ func (r *HAServiceReconciler) maybeCleanup(ctx context.Context, hostname string,
return true , r . Update ( ctx , cm )
return true , r . Update ( ctx , cm )
}
}
// VIP Services that are associated with the provided ProxyGroup and no longer managed this operator's instance are deleted, if not owned by other operator instances, else the owner reference is cleaned up.
// Tailscale Services that are associated with the provided ProxyGroup and no longer managed this operator's instance are deleted, if not owned by other operator instances, else the owner reference is cleaned up.
// Returns true if the operation resulted in existing VIP Service updates (owner reference removal).
// Returns true if the operation resulted in existing Tailscale Service updates (owner reference removal).
func ( r * HAServiceReconciler ) maybeCleanupProxyGroup ( ctx context . Context , proxyGroupName string , logger * zap . SugaredLogger ) ( svcsChanged bool , err error ) {
func ( r * HAServiceReconciler ) maybeCleanupProxyGroup ( ctx context . Context , proxyGroupName string , logger * zap . SugaredLogger ) ( svcsChanged bool , err error ) {
cm , config , err := ingressSvcsConfigs ( ctx , r . Client , proxyGroupName , r . tsNamespace )
cm , config , err := ingressSvcsConfigs ( ctx , r . Client , proxyGroupName , r . tsNamespace )
if err != nil {
if err != nil {
@ -443,31 +443,31 @@ func (r *HAServiceReconciler) maybeCleanupProxyGroup(ctx context.Context, proxyG
}
}
ingressConfigChanged := false
ingressConfigChanged := false
for vip SvcName, cfg := range config {
for ts SvcName, cfg := range config {
found := false
found := false
for _ , svc := range svcList . Items {
for _ , svc := range svcList . Items {
if strings . EqualFold ( fmt . Sprintf ( "svc:%s" , nameForService ( & svc ) ) , vip SvcName) {
if strings . EqualFold ( fmt . Sprintf ( "svc:%s" , nameForService ( & svc ) ) , ts SvcName) {
found = true
found = true
break
break
}
}
}
}
if ! found {
if ! found {
logger . Infof ( " VIPService %q is not owned by any Service, cleaning up", vip SvcName)
logger . Infof ( " Tailscale Service %q is not owned by any Service, cleaning up", ts SvcName)
// Make sure the VIP Service is not advertised in tailscaled or serve config.
// Make sure the Tailscale Service is not advertised in tailscaled or serve config.
if err = r . maybeUpdateAdvertiseServicesConfig ( ctx , nil , proxyGroupName , tailcfg . ServiceName ( vip SvcName) , & cfg , false , logger ) ; err != nil {
if err = r . maybeUpdateAdvertiseServicesConfig ( ctx , nil , proxyGroupName , tailcfg . ServiceName ( ts SvcName) , & cfg , false , logger ) ; err != nil {
return false , fmt . Errorf ( "failed to update tailscaled config services: %w" , err )
return false , fmt . Errorf ( "failed to update tailscaled config services: %w" , err )
}
}
svcsChanged , err = r . cleanup VIPService( ctx , tailcfg . ServiceName ( vip SvcName) , logger )
svcsChanged , err = r . cleanup TailscaleService( ctx , tailcfg . ServiceName ( ts SvcName) , logger )
if err != nil {
if err != nil {
return false , fmt . Errorf ( "deleting VIPService %q: %w", vip SvcName, err )
return false , fmt . Errorf ( "deleting Tailscale Service %q: %w", ts SvcName, err )
}
}
_ , ok := config [ vip SvcName]
_ , ok := config [ ts SvcName]
if ok {
if ok {
logger . Infof ( "Removing VIPService %q from serve config", vip SvcName)
logger . Infof ( "Removing Tailscale Service %q from serve config", ts SvcName)
delete ( config , vip SvcName)
delete ( config , ts SvcName)
ingressConfigChanged = true
ingressConfigChanged = true
}
}
}
}
@ -528,11 +528,11 @@ func (r *HAServiceReconciler) tailnetCertDomain(ctx context.Context) (string, er
return st . CurrentTailnet . MagicDNSSuffix , nil
return st . CurrentTailnet . MagicDNSSuffix , nil
}
}
// cleanup VIPService deletes any VIP Service by the provided name if it is not owned by operator instances other than this one.
// cleanup TailscaleService deletes any Tailscale Service by the provided name if it is not owned by operator instances other than this one.
// If a VIP Service is found, but contains other owner references, only removes this operator's owner reference.
// If a Tailscale Service is found, but contains other owner references, only removes this operator's owner reference.
// If a VIP Service by the given name is not found or does not contain this operator's owner reference, do nothing.
// If a Tailscale Service by the given name is not found or does not contain this operator's owner reference, do nothing.
// It returns true if an existing VIP Service was updated to remove owner reference, as well as any error that occurred.
// It returns true if an existing Tailscale Service was updated to remove owner reference, as well as any error that occurred.
func ( r * HAServiceReconciler ) cleanup VIP Service( ctx context . Context , name tailcfg . ServiceName , logger * zap . SugaredLogger ) ( updated bool , err error ) {
func ( r * HAServiceReconciler ) cleanup Tailscale Service( ctx context . Context , name tailcfg . ServiceName , logger * zap . SugaredLogger ) ( updated bool , err error ) {
svc , err := r . tsClient . GetVIPService ( ctx , name )
svc , err := r . tsClient . GetVIPService ( ctx , name )
if isErrorFeatureFlagNotEnabled ( err ) {
if isErrorFeatureFlagNotEnabled ( err ) {
msg := fmt . Sprintf ( "Unable to proceed with cleanup: %s." , msgFeatureFlagNotEnabled )
msg := fmt . Sprintf ( "Unable to proceed with cleanup: %s." , msgFeatureFlagNotEnabled )
@ -546,23 +546,23 @@ func (r *HAServiceReconciler) cleanupVIPService(ctx context.Context, name tailcf
return false , nil
return false , nil
}
}
if ! ok {
if ! ok {
return false , fmt . Errorf ( "unexpected error getting VIP Service %q: %w", name . String ( ) , err )
return false , fmt . Errorf ( "unexpected error getting Tailscale Service %q: %w", name . String ( ) , err )
}
}
return false , fmt . Errorf ( "error getting VIP Service: %w", err )
return false , fmt . Errorf ( "error getting Tailscale Service: %w", err )
}
}
if svc == nil {
if svc == nil {
return false , nil
return false , nil
}
}
o , err := parseOwnerAnnotation ( svc )
o , err := parseOwnerAnnotation ( svc )
if err != nil {
if err != nil {
return false , fmt . Errorf ( "error parsing VIP Service owner annotation: %w", err )
return false , fmt . Errorf ( "error parsing Tailscale Service owner annotation: %w", err )
}
}
if o == nil || len ( o . OwnerRefs ) == 0 {
if o == nil || len ( o . OwnerRefs ) == 0 {
return false , nil
return false , nil
}
}
// Comparing with the operatorID only means that we will not be able to
// Comparing with the operatorID only means that we will not be able to
// clean up VIP Services in cases where the operator was deleted from the
// clean up Tailscale Services in cases where the operator was deleted from the
// cluster before deleting the Ingress. Perhaps the comparison could be
// cluster before deleting the Ingress. Perhaps the comparison could be
// 'if or.OperatorID == r.operatorID || or.ingressUID == r.ingressUID'.
// 'if or.OperatorID == r.operatorID || or.ingressUID == r.ingressUID'.
ix := slices . IndexFunc ( o . OwnerRefs , func ( or OwnerRef ) bool {
ix := slices . IndexFunc ( o . OwnerRefs , func ( or OwnerRef ) bool {
@ -572,14 +572,14 @@ func (r *HAServiceReconciler) cleanupVIPService(ctx context.Context, name tailcf
return false , nil
return false , nil
}
}
if len ( o . OwnerRefs ) == 1 {
if len ( o . OwnerRefs ) == 1 {
logger . Infof ( "Deleting VIP Service %q", name )
logger . Infof ( "Deleting Tailscale Service %q", name )
return false , r . tsClient . DeleteVIPService ( ctx , name )
return false , r . tsClient . DeleteVIPService ( ctx , name )
}
}
o . OwnerRefs = slices . Delete ( o . OwnerRefs , ix , ix + 1 )
o . OwnerRefs = slices . Delete ( o . OwnerRefs , ix , ix + 1 )
logger . Infof ( "Updating VIP Service %q", name )
logger . Infof ( "Updating Tailscale Service %q", name )
json , err := json . Marshal ( o )
json , err := json . Marshal ( o )
if err != nil {
if err != nil {
return false , fmt . Errorf ( "error marshalling updated VIP Service owner reference: %w", err )
return false , fmt . Errorf ( "error marshalling updated Tailscale Service owner reference: %w", err )
}
}
svc . Annotations [ ownerAnnotation ] = string ( json )
svc . Annotations [ ownerAnnotation ] = string ( json )
return true , r . tsClient . CreateOrUpdateVIPService ( ctx , svc )
return true , r . tsClient . CreateOrUpdateVIPService ( ctx , svc )
@ -618,7 +618,7 @@ func (a *HAServiceReconciler) backendRoutesSetup(ctx context.Context, serviceNam
return false , fmt . Errorf ( "error checking ingress config status: %w" , err )
return false , fmt . Errorf ( "error checking ingress config status: %w" , err )
}
}
if ! statusUpToDate || ! reflect . DeepEqual ( gotCfgs . Configs . GetConfig ( serviceName ) , wantsCfg ) {
if ! statusUpToDate || ! reflect . DeepEqual ( gotCfgs . Configs . GetConfig ( serviceName ) , wantsCfg ) {
logger . Debugf ( "Pod %q is not ready to advertise VIP Service", pod . Name )
logger . Debugf ( "Pod %q is not ready to advertise Tailscale Service", pod . Name )
return false , nil
return false , nil
}
}
return true , nil
return true , nil
@ -746,9 +746,9 @@ func (a *HAServiceReconciler) numberPodsAdvertising(ctx context.Context, pgName
}
}
// ownerAnnotations returns the updated annotations required to ensure this
// ownerAnnotations returns the updated annotations required to ensure this
// instance of the operator is included as an owner. If the VIP Service is not
// instance of the operator is included as an owner. If the Tailscale Service is not
// nil, but does not contain an owner we return an error as this likely means
// nil, but does not contain an owner we return an error as this likely means
// that the VIP Service was created by something other than a Tailscale
// that the Tailscale Service was created by something other than a Tailscale
// Kubernetes operator.
// Kubernetes operator.
func ( r * HAServiceReconciler ) ownerAnnotations ( svc * tailscale . VIPService ) ( map [ string ] string , error ) {
func ( r * HAServiceReconciler ) ownerAnnotations ( svc * tailscale . VIPService ) ( map [ string ] string , error ) {
ref := OwnerRef {
ref := OwnerRef {
@ -758,7 +758,7 @@ func (r *HAServiceReconciler) ownerAnnotations(svc *tailscale.VIPService) (map[s
c := ownerAnnotationValue { OwnerRefs : [ ] OwnerRef { ref } }
c := ownerAnnotationValue { OwnerRefs : [ ] OwnerRef { ref } }
json , err := json . Marshal ( c )
json , err := json . Marshal ( c )
if err != nil {
if err != nil {
return nil , fmt . Errorf ( "[unexpected] unable to marshal VIP Service owner annotation contents: %w, please report this", err )
return nil , fmt . Errorf ( "[unexpected] unable to marshal Tailscale Service owner annotation contents: %w, please report this", err )
}
}
return map [ string ] string {
return map [ string ] string {
ownerAnnotation : string ( json ) ,
ownerAnnotation : string ( json ) ,
@ -769,7 +769,7 @@ func (r *HAServiceReconciler) ownerAnnotations(svc *tailscale.VIPService) (map[s
return nil , err
return nil , err
}
}
if o == nil || len ( o . OwnerRefs ) == 0 {
if o == nil || len ( o . OwnerRefs ) == 0 {
return nil , fmt . Errorf ( " VIP Service %s exists, but does not contain owner annotation with owner references; not proceeding as this is likely a resource created by something other than the Tailscale Kubernetes operator", svc . Name )
return nil , fmt . Errorf ( " Tailscale Service %s exists, but does not contain owner annotation with owner references; not proceeding as this is likely a resource created by something other than the Tailscale Kubernetes operator", svc . Name )
}
}
if slices . Contains ( o . OwnerRefs , ref ) { // up to date
if slices . Contains ( o . OwnerRefs , ref ) { // up to date
return svc . Annotations , nil
return svc . Annotations , nil
@ -788,7 +788,7 @@ func (r *HAServiceReconciler) ownerAnnotations(svc *tailscale.VIPService) (map[s
return newAnnots , nil
return newAnnots , nil
}
}
// dnsNameForService returns the DNS name for the given VIP Service name.
// dnsNameForService returns the DNS name for the given Tailscale Service name.
func ( r * HAServiceReconciler ) dnsNameForService ( ctx context . Context , svc tailcfg . ServiceName ) ( string , error ) {
func ( r * HAServiceReconciler ) dnsNameForService ( ctx context . Context , svc tailcfg . ServiceName ) ( string , error ) {
s := svc . WithoutPrefix ( )
s := svc . WithoutPrefix ( )
tcd , err := r . tailnetCertDomain ( ctx )
tcd , err := r . tailnetCertDomain ( ctx )