proxyclass.go 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  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. "fmt"
  8. "slices"
  9. "strings"
  10. "sync"
  11. dockerref "github.com/distribution/reference"
  12. "go.uber.org/zap"
  13. corev1 "k8s.io/api/core/v1"
  14. apiequality "k8s.io/apimachinery/pkg/api/equality"
  15. apierrors "k8s.io/apimachinery/pkg/api/errors"
  16. apivalidation "k8s.io/apimachinery/pkg/api/validation"
  17. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  18. metavalidation "k8s.io/apimachinery/pkg/apis/meta/v1/validation"
  19. "k8s.io/apimachinery/pkg/types"
  20. "k8s.io/apimachinery/pkg/util/validation/field"
  21. "k8s.io/client-go/tools/record"
  22. "sigs.k8s.io/controller-runtime/pkg/client"
  23. "sigs.k8s.io/controller-runtime/pkg/reconcile"
  24. tsoperator "tailscale.com/k8s-operator"
  25. tsapi "tailscale.com/k8s-operator/apis/v1alpha1"
  26. "tailscale.com/tstime"
  27. "tailscale.com/util/clientmetric"
  28. "tailscale.com/util/set"
  29. )
  30. const (
  31. reasonProxyClassInvalid = "ProxyClassInvalid"
  32. reasonProxyClassValid = "ProxyClassValid"
  33. reasonCustomTSEnvVar = "CustomTSEnvVar"
  34. messageProxyClassInvalid = "ProxyClass is not valid: %v"
  35. messageCustomTSEnvVar = "ProxyClass overrides the default value for %s env var for %s container. Running with custom values for Tailscale env vars is not recommended and might break in the future."
  36. )
  37. type ProxyClassReconciler struct {
  38. client.Client
  39. recorder record.EventRecorder
  40. logger *zap.SugaredLogger
  41. clock tstime.Clock
  42. mu sync.Mutex // protects following
  43. // managedProxyClasses is a set of all ProxyClass resources that we're currently
  44. // managing. This is only used for metrics.
  45. managedProxyClasses set.Slice[types.UID]
  46. }
  47. var (
  48. // gaugeProxyClassResources tracks the number of ProxyClass resources
  49. // that we're currently managing.
  50. gaugeProxyClassResources = clientmetric.NewGauge("k8s_proxyclass_resources")
  51. )
  52. func (pcr *ProxyClassReconciler) Reconcile(ctx context.Context, req reconcile.Request) (res reconcile.Result, err error) {
  53. logger := pcr.logger.With("ProxyClass", req.Name)
  54. logger.Debugf("starting reconcile")
  55. defer logger.Debugf("reconcile finished")
  56. pc := new(tsapi.ProxyClass)
  57. err = pcr.Get(ctx, req.NamespacedName, pc)
  58. if apierrors.IsNotFound(err) {
  59. logger.Debugf("ProxyClass not found, assuming it was deleted")
  60. return reconcile.Result{}, nil
  61. } else if err != nil {
  62. return reconcile.Result{}, fmt.Errorf("failed to get tailscale.com ProxyClass: %w", err)
  63. }
  64. if !pc.DeletionTimestamp.IsZero() {
  65. logger.Debugf("ProxyClass is being deleted")
  66. return reconcile.Result{}, pcr.maybeCleanup(ctx, logger, pc)
  67. }
  68. // Add a finalizer so that we can ensure that metrics get updated when
  69. // this ProxyClass is deleted.
  70. if !slices.Contains(pc.Finalizers, FinalizerName) {
  71. logger.Debugf("updating ProxyClass finalizers")
  72. pc.Finalizers = append(pc.Finalizers, FinalizerName)
  73. if err := pcr.Update(ctx, pc); err != nil {
  74. return res, fmt.Errorf("failed to add finalizer: %w", err)
  75. }
  76. }
  77. // Ensure this ProxyClass is tracked in metrics.
  78. pcr.mu.Lock()
  79. pcr.managedProxyClasses.Add(pc.UID)
  80. gaugeProxyClassResources.Set(int64(pcr.managedProxyClasses.Len()))
  81. pcr.mu.Unlock()
  82. oldPCStatus := pc.Status.DeepCopy()
  83. if errs := pcr.validate(pc); errs != nil {
  84. msg := fmt.Sprintf(messageProxyClassInvalid, errs.ToAggregate().Error())
  85. pcr.recorder.Event(pc, corev1.EventTypeWarning, reasonProxyClassInvalid, msg)
  86. tsoperator.SetProxyClassCondition(pc, tsapi.ProxyClassReady, metav1.ConditionFalse, reasonProxyClassInvalid, msg, pc.Generation, pcr.clock, logger)
  87. } else {
  88. tsoperator.SetProxyClassCondition(pc, tsapi.ProxyClassReady, metav1.ConditionTrue, reasonProxyClassValid, reasonProxyClassValid, pc.Generation, pcr.clock, logger)
  89. }
  90. if !apiequality.Semantic.DeepEqual(oldPCStatus, pc.Status) {
  91. if err := pcr.Client.Status().Update(ctx, pc); err != nil {
  92. logger.Errorf("error updating ProxyClass status: %v", err)
  93. return reconcile.Result{}, err
  94. }
  95. }
  96. return reconcile.Result{}, nil
  97. }
  98. func (pcr *ProxyClassReconciler) validate(pc *tsapi.ProxyClass) (violations field.ErrorList) {
  99. if sts := pc.Spec.StatefulSet; sts != nil {
  100. if len(sts.Labels) > 0 {
  101. if errs := metavalidation.ValidateLabels(sts.Labels, field.NewPath(".spec.statefulSet.labels")); errs != nil {
  102. violations = append(violations, errs...)
  103. }
  104. }
  105. if len(sts.Annotations) > 0 {
  106. if errs := apivalidation.ValidateAnnotations(sts.Annotations, field.NewPath(".spec.statefulSet.annotations")); errs != nil {
  107. violations = append(violations, errs...)
  108. }
  109. }
  110. if pod := sts.Pod; pod != nil {
  111. if len(pod.Labels) > 0 {
  112. if errs := metavalidation.ValidateLabels(pod.Labels, field.NewPath(".spec.statefulSet.pod.labels")); errs != nil {
  113. violations = append(violations, errs...)
  114. }
  115. }
  116. if len(pod.Annotations) > 0 {
  117. if errs := apivalidation.ValidateAnnotations(pod.Annotations, field.NewPath(".spec.statefulSet.pod.annotations")); errs != nil {
  118. violations = append(violations, errs...)
  119. }
  120. }
  121. if tc := pod.TailscaleContainer; tc != nil {
  122. for _, e := range tc.Env {
  123. if strings.HasPrefix(string(e.Name), "TS_") {
  124. pcr.recorder.Event(pc, corev1.EventTypeWarning, reasonCustomTSEnvVar, fmt.Sprintf(messageCustomTSEnvVar, string(e.Name), "tailscale"))
  125. }
  126. if strings.EqualFold(string(e.Name), "EXPERIMENTAL_TS_CONFIGFILE_PATH") {
  127. pcr.recorder.Event(pc, corev1.EventTypeWarning, reasonCustomTSEnvVar, fmt.Sprintf(messageCustomTSEnvVar, string(e.Name), "tailscale"))
  128. }
  129. if strings.EqualFold(string(e.Name), "EXPERIMENTAL_ALLOW_PROXYING_CLUSTER_TRAFFIC_VIA_INGRESS") {
  130. pcr.recorder.Event(pc, corev1.EventTypeWarning, reasonCustomTSEnvVar, fmt.Sprintf(messageCustomTSEnvVar, string(e.Name), "tailscale"))
  131. }
  132. }
  133. if tc.Image != "" {
  134. // Same validation as used by kubelet https://github.com/kubernetes/kubernetes/blob/release-1.30/pkg/kubelet/images/image_manager.go#L212
  135. if _, err := dockerref.ParseNormalizedNamed(tc.Image); err != nil {
  136. violations = append(violations, field.TypeInvalid(field.NewPath("spec", "statefulSet", "pod", "tailscaleContainer", "image"), tc.Image, err.Error()))
  137. }
  138. }
  139. }
  140. if tc := pod.TailscaleInitContainer; tc != nil {
  141. if tc.Image != "" {
  142. // Same validation as used by kubelet https://github.com/kubernetes/kubernetes/blob/release-1.30/pkg/kubelet/images/image_manager.go#L212
  143. if _, err := dockerref.ParseNormalizedNamed(tc.Image); err != nil {
  144. violations = append(violations, field.TypeInvalid(field.NewPath("spec", "statefulSet", "pod", "tailscaleInitContainer", "image"), tc.Image, err.Error()))
  145. }
  146. }
  147. if tc.Debug != nil {
  148. violations = append(violations, field.TypeInvalid(field.NewPath("spec", "statefulSet", "pod", "tailscaleInitContainer", "debug"), tc.Debug, "debug settings cannot be configured on the init container"))
  149. }
  150. }
  151. }
  152. }
  153. // We do not validate embedded fields (security context, resource
  154. // requirements etc) as we inherit upstream validation for those fields.
  155. // Invalid values would get rejected by upstream validations at apply
  156. // time.
  157. return violations
  158. }
  159. // maybeCleanup removes tailscale.com finalizer and ensures that the ProxyClass
  160. // is no longer counted towards k8s_proxyclass_resources.
  161. func (pcr *ProxyClassReconciler) maybeCleanup(ctx context.Context, logger *zap.SugaredLogger, pc *tsapi.ProxyClass) error {
  162. ix := slices.Index(pc.Finalizers, FinalizerName)
  163. if ix < 0 {
  164. logger.Debugf("no finalizer, nothing to do")
  165. pcr.mu.Lock()
  166. defer pcr.mu.Unlock()
  167. pcr.managedProxyClasses.Remove(pc.UID)
  168. gaugeProxyClassResources.Set(int64(pcr.managedProxyClasses.Len()))
  169. return nil
  170. }
  171. pc.Finalizers = append(pc.Finalizers[:ix], pc.Finalizers[ix+1:]...)
  172. if err := pcr.Update(ctx, pc); err != nil {
  173. return fmt.Errorf("failed to remove finalizer: %w", err)
  174. }
  175. pcr.mu.Lock()
  176. defer pcr.mu.Unlock()
  177. pcr.managedProxyClasses.Remove(pc.UID)
  178. gaugeProxyClassResources.Set(int64(pcr.managedProxyClasses.Len()))
  179. logger.Infof("ProxyClass resources have been cleaned up")
  180. return nil
  181. }