api-server-proxy-pg.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479
  1. // Copyright (c) Tailscale Inc & AUTHORS
  2. // SPDX-License-Identifier: BSD-3-Clause
  3. //go:build !plan9
  4. package main
  5. import (
  6. "context"
  7. "encoding/json"
  8. "errors"
  9. "fmt"
  10. "maps"
  11. "slices"
  12. "strings"
  13. "go.uber.org/zap"
  14. corev1 "k8s.io/api/core/v1"
  15. rbacv1 "k8s.io/api/rbac/v1"
  16. apiequality "k8s.io/apimachinery/pkg/api/equality"
  17. apierrors "k8s.io/apimachinery/pkg/api/errors"
  18. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  19. "k8s.io/client-go/tools/record"
  20. "sigs.k8s.io/controller-runtime/pkg/client"
  21. "sigs.k8s.io/controller-runtime/pkg/reconcile"
  22. "tailscale.com/internal/client/tailscale"
  23. tsoperator "tailscale.com/k8s-operator"
  24. tsapi "tailscale.com/k8s-operator/apis/v1alpha1"
  25. "tailscale.com/kube/k8s-proxy/conf"
  26. "tailscale.com/kube/kubetypes"
  27. "tailscale.com/tailcfg"
  28. "tailscale.com/tstime"
  29. )
  30. const (
  31. proxyPGFinalizerName = "tailscale.com/kube-apiserver-finalizer"
  32. // Reasons for KubeAPIServerProxyValid condition.
  33. reasonKubeAPIServerProxyInvalid = "KubeAPIServerProxyInvalid"
  34. reasonKubeAPIServerProxyValid = "KubeAPIServerProxyValid"
  35. // Reasons for KubeAPIServerProxyConfigured condition.
  36. reasonKubeAPIServerProxyConfigured = "KubeAPIServerProxyConfigured"
  37. reasonKubeAPIServerProxyNoBackends = "KubeAPIServerProxyNoBackends"
  38. )
  39. // KubeAPIServerTSServiceReconciler reconciles the Tailscale Services required for an
  40. // HA deployment of the API Server Proxy.
  41. type KubeAPIServerTSServiceReconciler struct {
  42. client.Client
  43. recorder record.EventRecorder
  44. logger *zap.SugaredLogger
  45. tsClient tsClient
  46. tsNamespace string
  47. lc localClient
  48. defaultTags []string
  49. operatorID string // stableID of the operator's Tailscale device
  50. clock tstime.Clock
  51. }
  52. // Reconcile is the entry point for the controller.
  53. func (r *KubeAPIServerTSServiceReconciler) Reconcile(ctx context.Context, req reconcile.Request) (res reconcile.Result, err error) {
  54. logger := r.logger.With("ProxyGroup", req.Name)
  55. logger.Debugf("starting reconcile")
  56. defer logger.Debugf("reconcile finished")
  57. pg := new(tsapi.ProxyGroup)
  58. err = r.Get(ctx, req.NamespacedName, pg)
  59. if apierrors.IsNotFound(err) {
  60. // Request object not found, could have been deleted after reconcile request.
  61. logger.Debugf("ProxyGroup not found, assuming it was deleted")
  62. return res, nil
  63. } else if err != nil {
  64. return res, fmt.Errorf("failed to get ProxyGroup: %w", err)
  65. }
  66. serviceName := serviceNameForAPIServerProxy(pg)
  67. logger = logger.With("Tailscale Service", serviceName)
  68. if markedForDeletion(pg) {
  69. logger.Debugf("ProxyGroup is being deleted, ensuring any created resources are cleaned up")
  70. if err = r.maybeCleanup(ctx, serviceName, pg, logger); err != nil && strings.Contains(err.Error(), optimisticLockErrorMsg) {
  71. logger.Infof("optimistic lock error, retrying: %s", err)
  72. return res, nil
  73. }
  74. return res, err
  75. }
  76. err = r.maybeProvision(ctx, serviceName, pg, logger)
  77. if err != nil {
  78. if strings.Contains(err.Error(), optimisticLockErrorMsg) {
  79. logger.Infof("optimistic lock error, retrying: %s", err)
  80. return reconcile.Result{}, nil
  81. }
  82. return reconcile.Result{}, err
  83. }
  84. return reconcile.Result{}, nil
  85. }
  86. // maybeProvision ensures that a Tailscale Service for this ProxyGroup exists
  87. // and is up to date.
  88. //
  89. // Returns true if the operation resulted in a Tailscale Service update.
  90. func (r *KubeAPIServerTSServiceReconciler) maybeProvision(ctx context.Context, serviceName tailcfg.ServiceName, pg *tsapi.ProxyGroup, logger *zap.SugaredLogger) (err error) {
  91. var dnsName string
  92. oldPGStatus := pg.Status.DeepCopy()
  93. defer func() {
  94. podsAdvertising, podsErr := numberPodsAdvertising(ctx, r.Client, r.tsNamespace, pg.Name, serviceName)
  95. if podsErr != nil {
  96. err = errors.Join(err, fmt.Errorf("failed to get number of advertised Pods: %w", podsErr))
  97. // Continue, updating the status with the best available information.
  98. }
  99. // Update the ProxyGroup status with the Tailscale Service information
  100. // Update the condition based on how many pods are advertising the service
  101. conditionStatus := metav1.ConditionFalse
  102. conditionReason := reasonKubeAPIServerProxyNoBackends
  103. conditionMessage := fmt.Sprintf("%d/%d proxy backends ready and advertising", podsAdvertising, pgReplicas(pg))
  104. pg.Status.URL = ""
  105. if podsAdvertising > 0 {
  106. // At least one pod is advertising the service, consider it configured
  107. conditionStatus = metav1.ConditionTrue
  108. conditionReason = reasonKubeAPIServerProxyConfigured
  109. if dnsName != "" {
  110. pg.Status.URL = "https://" + dnsName
  111. }
  112. }
  113. tsoperator.SetProxyGroupCondition(pg, tsapi.KubeAPIServerProxyConfigured, conditionStatus, conditionReason, conditionMessage, pg.Generation, r.clock, logger)
  114. if !apiequality.Semantic.DeepEqual(oldPGStatus, &pg.Status) {
  115. // An error encountered here should get returned by the Reconcile function.
  116. err = errors.Join(err, r.Client.Status().Update(ctx, pg))
  117. }
  118. }()
  119. if !tsoperator.ProxyGroupAvailable(pg) {
  120. return nil
  121. }
  122. if !slices.Contains(pg.Finalizers, proxyPGFinalizerName) {
  123. // This log line is printed exactly once during initial provisioning,
  124. // because once the finalizer is in place this block gets skipped. So,
  125. // this is a nice place to tell the operator that the high level,
  126. // multi-reconcile operation is underway.
  127. logger.Info("provisioning Tailscale Service for ProxyGroup")
  128. pg.Finalizers = append(pg.Finalizers, proxyPGFinalizerName)
  129. if err := r.Update(ctx, pg); err != nil {
  130. return fmt.Errorf("failed to add finalizer: %w", err)
  131. }
  132. }
  133. // 1. Check there isn't a Tailscale Service with the same hostname
  134. // already created and not owned by this ProxyGroup.
  135. existingTSSvc, err := r.tsClient.GetVIPService(ctx, serviceName)
  136. if isErrorFeatureFlagNotEnabled(err) {
  137. logger.Warn(msgFeatureFlagNotEnabled)
  138. r.recorder.Event(pg, corev1.EventTypeWarning, warningTailscaleServiceFeatureFlagNotEnabled, msgFeatureFlagNotEnabled)
  139. tsoperator.SetProxyGroupCondition(pg, tsapi.KubeAPIServerProxyValid, metav1.ConditionFalse, reasonKubeAPIServerProxyInvalid, msgFeatureFlagNotEnabled, pg.Generation, r.clock, logger)
  140. return nil
  141. }
  142. if err != nil && !isErrorTailscaleServiceNotFound(err) {
  143. return fmt.Errorf("error getting Tailscale Service %q: %w", serviceName, err)
  144. }
  145. updatedAnnotations, err := exclusiveOwnerAnnotations(pg, r.operatorID, existingTSSvc)
  146. if err != nil {
  147. const instr = "To proceed, you can either manually delete the existing Tailscale Service or choose a different Service name in the ProxyGroup's spec.kubeAPIServer.serviceName field"
  148. msg := fmt.Sprintf("error ensuring exclusive ownership of Tailscale Service %s: %v. %s", serviceName, err, instr)
  149. logger.Warn(msg)
  150. r.recorder.Event(pg, corev1.EventTypeWarning, "InvalidTailscaleService", msg)
  151. tsoperator.SetProxyGroupCondition(pg, tsapi.KubeAPIServerProxyValid, metav1.ConditionFalse, reasonKubeAPIServerProxyInvalid, msg, pg.Generation, r.clock, logger)
  152. return nil
  153. }
  154. // After getting this far, we know the Tailscale Service is valid.
  155. tsoperator.SetProxyGroupCondition(pg, tsapi.KubeAPIServerProxyValid, metav1.ConditionTrue, reasonKubeAPIServerProxyValid, reasonKubeAPIServerProxyValid, pg.Generation, r.clock, logger)
  156. // Service tags are limited to matching the ProxyGroup's tags until we have
  157. // support for querying peer caps for a Service-bound request.
  158. serviceTags := r.defaultTags
  159. if len(pg.Spec.Tags) > 0 {
  160. serviceTags = pg.Spec.Tags.Stringify()
  161. }
  162. tsSvc := &tailscale.VIPService{
  163. Name: serviceName,
  164. Tags: serviceTags,
  165. Ports: []string{"tcp:443"},
  166. Comment: managedTSServiceComment,
  167. Annotations: updatedAnnotations,
  168. }
  169. if existingTSSvc != nil {
  170. tsSvc.Addrs = existingTSSvc.Addrs
  171. }
  172. // 2. Ensure the Tailscale Service exists and is up to date.
  173. if existingTSSvc == nil ||
  174. !slices.Equal(tsSvc.Tags, existingTSSvc.Tags) ||
  175. !ownersAreSetAndEqual(tsSvc, existingTSSvc) ||
  176. !slices.Equal(tsSvc.Ports, existingTSSvc.Ports) {
  177. logger.Infof("Ensuring Tailscale Service exists and is up to date")
  178. if err := r.tsClient.CreateOrUpdateVIPService(ctx, tsSvc); err != nil {
  179. return fmt.Errorf("error creating Tailscale Service: %w", err)
  180. }
  181. }
  182. // 3. Ensure that TLS Secret and RBAC exists.
  183. tcd, err := tailnetCertDomain(ctx, r.lc)
  184. if err != nil {
  185. return fmt.Errorf("error determining DNS name base: %w", err)
  186. }
  187. dnsName = serviceName.WithoutPrefix() + "." + tcd
  188. if err = r.ensureCertResources(ctx, pg, dnsName); err != nil {
  189. return fmt.Errorf("error ensuring cert resources: %w", err)
  190. }
  191. // 4. Configure the Pods to advertise the Tailscale Service.
  192. if err = r.maybeAdvertiseServices(ctx, pg, serviceName, logger); err != nil {
  193. return fmt.Errorf("error updating advertised Tailscale Services: %w", err)
  194. }
  195. // 5. Clean up any stale Tailscale Services from previous resource versions.
  196. if err = r.maybeDeleteStaleServices(ctx, pg, logger); err != nil {
  197. return fmt.Errorf("failed to delete stale Tailscale Services: %w", err)
  198. }
  199. return nil
  200. }
  201. // maybeCleanup ensures that any resources, such as a Tailscale Service created for this Service, are cleaned up when the
  202. // Service is being deleted or is unexposed. The cleanup is safe for a multi-cluster setup- the Tailscale Service is only
  203. // deleted if it does not contain any other owner references. If it does, the cleanup only removes the owner reference
  204. // corresponding to this Service.
  205. func (r *KubeAPIServerTSServiceReconciler) maybeCleanup(ctx context.Context, serviceName tailcfg.ServiceName, pg *tsapi.ProxyGroup, logger *zap.SugaredLogger) (err error) {
  206. ix := slices.Index(pg.Finalizers, proxyPGFinalizerName)
  207. if ix < 0 {
  208. logger.Debugf("no finalizer, nothing to do")
  209. return nil
  210. }
  211. logger.Infof("Ensuring that Service %q is cleaned up", serviceName)
  212. defer func() {
  213. if err == nil {
  214. err = r.deleteFinalizer(ctx, pg, logger)
  215. }
  216. }()
  217. if _, err = cleanupTailscaleService(ctx, r.tsClient, serviceName, r.operatorID, logger); err != nil {
  218. return fmt.Errorf("error deleting Tailscale Service: %w", err)
  219. }
  220. if err = cleanupCertResources(ctx, r.Client, r.lc, r.tsNamespace, pg.Name, serviceName); err != nil {
  221. return fmt.Errorf("failed to clean up cert resources: %w", err)
  222. }
  223. return nil
  224. }
  225. // maybeDeleteStaleServices deletes Services that have previously been created for
  226. // this ProxyGroup but are no longer needed.
  227. func (r *KubeAPIServerTSServiceReconciler) maybeDeleteStaleServices(ctx context.Context, pg *tsapi.ProxyGroup, logger *zap.SugaredLogger) error {
  228. serviceName := serviceNameForAPIServerProxy(pg)
  229. svcs, err := r.tsClient.ListVIPServices(ctx)
  230. if err != nil {
  231. return fmt.Errorf("error listing Tailscale Services: %w", err)
  232. }
  233. for _, svc := range svcs.VIPServices {
  234. if svc.Name == serviceName {
  235. continue
  236. }
  237. owners, err := parseOwnerAnnotation(&svc)
  238. if err != nil {
  239. logger.Warnf("error parsing owner annotation for Tailscale Service %s: %v", svc.Name, err)
  240. continue
  241. }
  242. if owners == nil || len(owners.OwnerRefs) != 1 || owners.OwnerRefs[0].OperatorID != r.operatorID {
  243. continue
  244. }
  245. owner := owners.OwnerRefs[0]
  246. if owner.Resource == nil || owner.Resource.Kind != "ProxyGroup" || owner.Resource.UID != string(pg.UID) {
  247. continue
  248. }
  249. logger.Infof("Deleting Tailscale Service %s", svc.Name)
  250. if err := r.tsClient.DeleteVIPService(ctx, svc.Name); err != nil && !isErrorTailscaleServiceNotFound(err) {
  251. return fmt.Errorf("error deleting Tailscale Service %s: %w", svc.Name, err)
  252. }
  253. if err = cleanupCertResources(ctx, r.Client, r.lc, r.tsNamespace, pg.Name, svc.Name); err != nil {
  254. return fmt.Errorf("failed to clean up cert resources: %w", err)
  255. }
  256. }
  257. return nil
  258. }
  259. func (r *KubeAPIServerTSServiceReconciler) deleteFinalizer(ctx context.Context, pg *tsapi.ProxyGroup, logger *zap.SugaredLogger) error {
  260. pg.Finalizers = slices.DeleteFunc(pg.Finalizers, func(f string) bool {
  261. return f == proxyPGFinalizerName
  262. })
  263. logger.Debugf("ensure %q finalizer is removed", proxyPGFinalizerName)
  264. if err := r.Update(ctx, pg); err != nil {
  265. return fmt.Errorf("failed to remove finalizer %q: %w", proxyPGFinalizerName, err)
  266. }
  267. return nil
  268. }
  269. func (r *KubeAPIServerTSServiceReconciler) ensureCertResources(ctx context.Context, pg *tsapi.ProxyGroup, domain string) error {
  270. secret := certSecret(pg.Name, r.tsNamespace, domain, pg)
  271. if _, err := createOrUpdate(ctx, r.Client, r.tsNamespace, secret, func(s *corev1.Secret) {
  272. s.Labels = secret.Labels
  273. }); err != nil {
  274. return fmt.Errorf("failed to create or update Secret %s: %w", secret.Name, err)
  275. }
  276. role := certSecretRole(pg.Name, r.tsNamespace, domain)
  277. if _, err := createOrUpdate(ctx, r.Client, r.tsNamespace, role, func(r *rbacv1.Role) {
  278. r.Labels = role.Labels
  279. r.Rules = role.Rules
  280. }); err != nil {
  281. return fmt.Errorf("failed to create or update Role %s: %w", role.Name, err)
  282. }
  283. rolebinding := certSecretRoleBinding(pg, r.tsNamespace, domain)
  284. if _, err := createOrUpdate(ctx, r.Client, r.tsNamespace, rolebinding, func(rb *rbacv1.RoleBinding) {
  285. rb.Labels = rolebinding.Labels
  286. rb.Subjects = rolebinding.Subjects
  287. rb.RoleRef = rolebinding.RoleRef
  288. }); err != nil {
  289. return fmt.Errorf("failed to create or update RoleBinding %s: %w", rolebinding.Name, err)
  290. }
  291. return nil
  292. }
  293. func (r *KubeAPIServerTSServiceReconciler) maybeAdvertiseServices(ctx context.Context, pg *tsapi.ProxyGroup, serviceName tailcfg.ServiceName, logger *zap.SugaredLogger) error {
  294. // Get all config Secrets for this ProxyGroup
  295. cfgSecrets := &corev1.SecretList{}
  296. if err := r.List(ctx, cfgSecrets, client.InNamespace(r.tsNamespace), client.MatchingLabels(pgSecretLabels(pg.Name, kubetypes.LabelSecretTypeConfig))); err != nil {
  297. return fmt.Errorf("failed to list config Secrets: %w", err)
  298. }
  299. // Only advertise a Tailscale Service once the TLS certs required for
  300. // serving it are available.
  301. shouldBeAdvertised, err := hasCerts(ctx, r.Client, r.lc, r.tsNamespace, serviceName)
  302. if err != nil {
  303. return fmt.Errorf("error checking TLS credentials provisioned for Tailscale Service %q: %w", serviceName, err)
  304. }
  305. var advertiseServices []string
  306. if shouldBeAdvertised {
  307. advertiseServices = []string{serviceName.String()}
  308. }
  309. for _, s := range cfgSecrets.Items {
  310. if len(s.Data[kubetypes.KubeAPIServerConfigFile]) == 0 {
  311. continue
  312. }
  313. // Parse the existing config.
  314. cfg, err := conf.Load(s.Data[kubetypes.KubeAPIServerConfigFile])
  315. if err != nil {
  316. return fmt.Errorf("error loading config from Secret %q: %w", s.Name, err)
  317. }
  318. if cfg.Parsed.APIServerProxy == nil {
  319. return fmt.Errorf("config Secret %q does not contain APIServerProxy config", s.Name)
  320. }
  321. existingCfgSecret := s.DeepCopy()
  322. var updated bool
  323. if cfg.Parsed.APIServerProxy.ServiceName == nil || *cfg.Parsed.APIServerProxy.ServiceName != serviceName {
  324. cfg.Parsed.APIServerProxy.ServiceName = &serviceName
  325. updated = true
  326. }
  327. // Update the services to advertise if required.
  328. if !slices.Equal(cfg.Parsed.AdvertiseServices, advertiseServices) {
  329. cfg.Parsed.AdvertiseServices = advertiseServices
  330. updated = true
  331. }
  332. if !updated {
  333. continue
  334. }
  335. // Update the config Secret.
  336. cfgB, err := json.Marshal(conf.VersionedConfig{
  337. Version: "v1alpha1",
  338. ConfigV1Alpha1: &cfg.Parsed,
  339. })
  340. if err != nil {
  341. return err
  342. }
  343. s.Data[kubetypes.KubeAPIServerConfigFile] = cfgB
  344. if !apiequality.Semantic.DeepEqual(existingCfgSecret, s) {
  345. logger.Debugf("Updating the Tailscale Services in ProxyGroup config Secret %s", s.Name)
  346. if err := r.Update(ctx, &s); err != nil {
  347. return err
  348. }
  349. }
  350. }
  351. return nil
  352. }
  353. func serviceNameForAPIServerProxy(pg *tsapi.ProxyGroup) tailcfg.ServiceName {
  354. if pg.Spec.KubeAPIServer != nil && pg.Spec.KubeAPIServer.Hostname != "" {
  355. return tailcfg.ServiceName("svc:" + pg.Spec.KubeAPIServer.Hostname)
  356. }
  357. return tailcfg.ServiceName("svc:" + pg.Name)
  358. }
  359. // exclusiveOwnerAnnotations returns the updated annotations required to ensure this
  360. // instance of the operator is the exclusive owner. If the Tailscale Service is not
  361. // nil, but does not contain an owner reference we return an error as this likely means
  362. // that the Service was created by something other than a Tailscale Kubernetes operator.
  363. // We also error if it is already owned by another operator instance, as we do not
  364. // want to load balance a kube-apiserver ProxyGroup across multiple clusters.
  365. func exclusiveOwnerAnnotations(pg *tsapi.ProxyGroup, operatorID string, svc *tailscale.VIPService) (map[string]string, error) {
  366. ref := OwnerRef{
  367. OperatorID: operatorID,
  368. Resource: &Resource{
  369. Kind: "ProxyGroup",
  370. Name: pg.Name,
  371. UID: string(pg.UID),
  372. },
  373. }
  374. if svc == nil {
  375. c := ownerAnnotationValue{OwnerRefs: []OwnerRef{ref}}
  376. json, err := json.Marshal(c)
  377. if err != nil {
  378. return nil, fmt.Errorf("[unexpected] unable to marshal Tailscale Service's owner annotation contents: %w, please report this", err)
  379. }
  380. return map[string]string{
  381. ownerAnnotation: string(json),
  382. }, nil
  383. }
  384. o, err := parseOwnerAnnotation(svc)
  385. if err != nil {
  386. return nil, err
  387. }
  388. if o == nil || len(o.OwnerRefs) == 0 {
  389. 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)
  390. }
  391. if len(o.OwnerRefs) > 1 || o.OwnerRefs[0].OperatorID != operatorID {
  392. return nil, fmt.Errorf("Tailscale Service %s is already owned by other operator(s) and cannot be shared across multiple clusters; configure a difference Service name to continue", svc.Name)
  393. }
  394. if o.OwnerRefs[0].Resource == nil {
  395. return nil, fmt.Errorf("Tailscale Service %s exists, but does not reference an owning resource; not proceeding as this is likely a Service already owned by an Ingress", svc.Name)
  396. }
  397. if o.OwnerRefs[0].Resource.Kind != "ProxyGroup" || o.OwnerRefs[0].Resource.UID != string(pg.UID) {
  398. return nil, fmt.Errorf("Tailscale Service %s is already owned by another resource: %#v; configure a difference Service name to continue", svc.Name, o.OwnerRefs[0].Resource)
  399. }
  400. if o.OwnerRefs[0].Resource.Name != pg.Name {
  401. // ProxyGroup name can be updated in place.
  402. o.OwnerRefs[0].Resource.Name = pg.Name
  403. }
  404. oBytes, err := json.Marshal(o)
  405. if err != nil {
  406. return nil, err
  407. }
  408. newAnnots := make(map[string]string, len(svc.Annotations)+1)
  409. maps.Copy(newAnnots, svc.Annotations)
  410. newAnnots[ownerAnnotation] = string(oBytes)
  411. return newAnnots, nil
  412. }