proxygroup_specs.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609
  1. // Copyright (c) Tailscale Inc & AUTHORS
  2. // SPDX-License-Identifier: BSD-3-Clause
  3. //go:build !plan9
  4. package main
  5. import (
  6. "fmt"
  7. "slices"
  8. "strconv"
  9. "strings"
  10. appsv1 "k8s.io/api/apps/v1"
  11. corev1 "k8s.io/api/core/v1"
  12. rbacv1 "k8s.io/api/rbac/v1"
  13. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  14. "k8s.io/apimachinery/pkg/types"
  15. "k8s.io/apimachinery/pkg/util/intstr"
  16. "sigs.k8s.io/yaml"
  17. tsapi "tailscale.com/k8s-operator/apis/v1alpha1"
  18. "tailscale.com/kube/egressservices"
  19. "tailscale.com/kube/ingressservices"
  20. "tailscale.com/kube/kubetypes"
  21. "tailscale.com/types/ptr"
  22. )
  23. const (
  24. // deletionGracePeriodSeconds is set to 6 minutes to ensure that the pre-stop hook of these proxies have enough chance to terminate gracefully.
  25. deletionGracePeriodSeconds int64 = 360
  26. staticEndpointPortName = "static-endpoint-port"
  27. // authAPIServerProxySAName is the ServiceAccount deployed by the helm chart
  28. // if apiServerProxy.authEnabled is true.
  29. authAPIServerProxySAName = "kube-apiserver-auth-proxy"
  30. )
  31. func pgNodePortServiceName(proxyGroupName string, replica int32) string {
  32. return fmt.Sprintf("%s-%d-nodeport", proxyGroupName, replica)
  33. }
  34. func pgNodePortService(pg *tsapi.ProxyGroup, name string, namespace string) *corev1.Service {
  35. return &corev1.Service{
  36. ObjectMeta: metav1.ObjectMeta{
  37. Name: name,
  38. Namespace: namespace,
  39. Labels: pgLabels(pg.Name, nil),
  40. OwnerReferences: pgOwnerReference(pg),
  41. },
  42. Spec: corev1.ServiceSpec{
  43. Type: corev1.ServiceTypeNodePort,
  44. Ports: []corev1.ServicePort{
  45. // NOTE(ChaosInTheCRD): we set the ports once we've iterated over every svc and found any old configuration we want to persist.
  46. {
  47. Name: staticEndpointPortName,
  48. Protocol: corev1.ProtocolUDP,
  49. },
  50. },
  51. Selector: map[string]string{
  52. appsv1.StatefulSetPodNameLabel: strings.TrimSuffix(name, "-nodeport"),
  53. },
  54. },
  55. }
  56. }
  57. // Returns the base StatefulSet definition for a ProxyGroup. A ProxyClass may be
  58. // applied over the top after.
  59. func pgStatefulSet(pg *tsapi.ProxyGroup, namespace, image, tsFirewallMode string, port *uint16, proxyClass *tsapi.ProxyClass) (*appsv1.StatefulSet, error) {
  60. if pg.Spec.Type == tsapi.ProxyGroupTypeKubernetesAPIServer {
  61. return kubeAPIServerStatefulSet(pg, namespace, image, port)
  62. }
  63. ss := new(appsv1.StatefulSet)
  64. if err := yaml.Unmarshal(proxyYaml, &ss); err != nil {
  65. return nil, fmt.Errorf("failed to unmarshal proxy spec: %w", err)
  66. }
  67. // Validate some base assumptions.
  68. if len(ss.Spec.Template.Spec.InitContainers) != 1 {
  69. return nil, fmt.Errorf("[unexpected] base proxy config had %d init containers instead of 1", len(ss.Spec.Template.Spec.InitContainers))
  70. }
  71. if len(ss.Spec.Template.Spec.Containers) != 1 {
  72. return nil, fmt.Errorf("[unexpected] base proxy config had %d containers instead of 1", len(ss.Spec.Template.Spec.Containers))
  73. }
  74. // StatefulSet config.
  75. ss.ObjectMeta = metav1.ObjectMeta{
  76. Name: pg.Name,
  77. Namespace: namespace,
  78. Labels: pgLabels(pg.Name, nil),
  79. OwnerReferences: pgOwnerReference(pg),
  80. }
  81. ss.Spec.Replicas = ptr.To(pgReplicas(pg))
  82. ss.Spec.Selector = &metav1.LabelSelector{
  83. MatchLabels: pgLabels(pg.Name, nil),
  84. }
  85. // Template config.
  86. tmpl := &ss.Spec.Template
  87. tmpl.ObjectMeta = metav1.ObjectMeta{
  88. Name: pg.Name,
  89. Namespace: namespace,
  90. Labels: pgLabels(pg.Name, nil),
  91. DeletionGracePeriodSeconds: ptr.To[int64](10),
  92. }
  93. tmpl.Spec.ServiceAccountName = pg.Name
  94. tmpl.Spec.InitContainers[0].Image = image
  95. proxyConfigVolName := pgEgressCMName(pg.Name)
  96. if pg.Spec.Type == tsapi.ProxyGroupTypeIngress {
  97. proxyConfigVolName = pgIngressCMName(pg.Name)
  98. }
  99. tmpl.Spec.Volumes = func() []corev1.Volume {
  100. var volumes []corev1.Volume
  101. for i := range pgReplicas(pg) {
  102. volumes = append(volumes, corev1.Volume{
  103. Name: fmt.Sprintf("tailscaledconfig-%d", i),
  104. VolumeSource: corev1.VolumeSource{
  105. Secret: &corev1.SecretVolumeSource{
  106. SecretName: pgConfigSecretName(pg.Name, i),
  107. },
  108. },
  109. })
  110. }
  111. volumes = append(volumes, corev1.Volume{
  112. Name: proxyConfigVolName,
  113. VolumeSource: corev1.VolumeSource{
  114. ConfigMap: &corev1.ConfigMapVolumeSource{
  115. LocalObjectReference: corev1.LocalObjectReference{
  116. Name: proxyConfigVolName,
  117. },
  118. },
  119. },
  120. })
  121. return volumes
  122. }()
  123. // Main container config.
  124. c := &ss.Spec.Template.Spec.Containers[0]
  125. c.Image = image
  126. c.VolumeMounts = func() []corev1.VolumeMount {
  127. var mounts []corev1.VolumeMount
  128. // TODO(tomhjp): Read config directly from the secret instead. The
  129. // mounts change on scaling up/down which causes unnecessary restarts
  130. // for pods that haven't meaningfully changed.
  131. for i := range pgReplicas(pg) {
  132. mounts = append(mounts, corev1.VolumeMount{
  133. Name: fmt.Sprintf("tailscaledconfig-%d", i),
  134. ReadOnly: true,
  135. MountPath: fmt.Sprintf("/etc/tsconfig/%s-%d", pg.Name, i),
  136. })
  137. }
  138. mounts = append(mounts, corev1.VolumeMount{
  139. Name: proxyConfigVolName,
  140. MountPath: "/etc/proxies",
  141. ReadOnly: true,
  142. })
  143. return mounts
  144. }()
  145. c.Env = func() []corev1.EnvVar {
  146. envs := []corev1.EnvVar{
  147. {
  148. // TODO(irbekrm): verify that .status.podIPs are always set, else read in .status.podIP as well.
  149. Name: "POD_IPS", // this will be a comma separate list i.e 10.136.0.6,2600:1900:4011:161:0:e:0:6
  150. ValueFrom: &corev1.EnvVarSource{
  151. FieldRef: &corev1.ObjectFieldSelector{
  152. FieldPath: "status.podIPs",
  153. },
  154. },
  155. },
  156. {
  157. Name: "TS_KUBE_SECRET",
  158. Value: "$(POD_NAME)",
  159. },
  160. {
  161. // TODO(tomhjp): This is tsrecorder-specific and does nothing. Delete.
  162. Name: "TS_STATE",
  163. Value: "kube:$(POD_NAME)",
  164. },
  165. {
  166. Name: "TS_EXPERIMENTAL_VERSIONED_CONFIG_DIR",
  167. Value: "/etc/tsconfig/$(POD_NAME)",
  168. },
  169. {
  170. // This ensures that cert renewals can succeed if ACME account
  171. // keys have changed since issuance. We cannot guarantee or
  172. // validate that the account key has not changed, see
  173. // https://github.com/tailscale/tailscale/issues/18251
  174. Name: "TS_DEBUG_ACME_FORCE_RENEWAL",
  175. Value: "true",
  176. },
  177. }
  178. if port != nil {
  179. envs = append(envs, corev1.EnvVar{
  180. Name: "PORT",
  181. Value: strconv.Itoa(int(*port)),
  182. })
  183. }
  184. if tsFirewallMode != "" {
  185. envs = append(envs, corev1.EnvVar{
  186. Name: "TS_DEBUG_FIREWALL_MODE",
  187. Value: tsFirewallMode,
  188. })
  189. }
  190. if pg.Spec.Type == tsapi.ProxyGroupTypeEgress {
  191. envs = append(envs,
  192. // TODO(irbekrm): in 1.80 we deprecated TS_EGRESS_SERVICES_CONFIG_PATH in favour of
  193. // TS_EGRESS_PROXIES_CONFIG_PATH. Remove it in 1.84.
  194. corev1.EnvVar{
  195. Name: "TS_EGRESS_SERVICES_CONFIG_PATH",
  196. Value: fmt.Sprintf("/etc/proxies/%s", egressservices.KeyEgressServices),
  197. },
  198. corev1.EnvVar{
  199. Name: "TS_EGRESS_PROXIES_CONFIG_PATH",
  200. Value: "/etc/proxies",
  201. },
  202. corev1.EnvVar{
  203. Name: "TS_INTERNAL_APP",
  204. Value: kubetypes.AppProxyGroupEgress,
  205. },
  206. corev1.EnvVar{
  207. Name: "TS_ENABLE_HEALTH_CHECK",
  208. Value: "true",
  209. })
  210. } else { // ingress
  211. envs = append(envs, corev1.EnvVar{
  212. Name: "TS_INTERNAL_APP",
  213. Value: kubetypes.AppProxyGroupIngress,
  214. },
  215. corev1.EnvVar{
  216. Name: "TS_INGRESS_PROXIES_CONFIG_PATH",
  217. Value: fmt.Sprintf("/etc/proxies/%s", ingressservices.IngressConfigKey),
  218. },
  219. corev1.EnvVar{
  220. Name: "TS_SERVE_CONFIG",
  221. Value: fmt.Sprintf("/etc/proxies/%s", serveConfigKey),
  222. },
  223. corev1.EnvVar{
  224. // Run proxies in cert share mode to
  225. // ensure that only one TLS cert is
  226. // issued for an HA Ingress.
  227. Name: "TS_EXPERIMENTAL_CERT_SHARE",
  228. Value: "true",
  229. },
  230. )
  231. }
  232. return append(c.Env, envs...)
  233. }()
  234. // The pre-stop hook is used to ensure that a replica does not get terminated while cluster traffic for egress
  235. // services is still being routed to it.
  236. //
  237. // This mechanism currently (2025-01-26) rely on the local health check being accessible on the Pod's
  238. // IP, so they are not supported for ProxyGroups where users have configured TS_LOCAL_ADDR_PORT to a custom
  239. // value.
  240. //
  241. // NB: For _Ingress_ ProxyGroups, we run shutdown logic within containerboot
  242. // in reaction to a SIGTERM signal instead of using a pre-stop hook. This is
  243. // because Ingress pods need to unadvertise services, and it's preferable to
  244. // avoid triggering those side-effects from a GET request that would be
  245. // accessible to the whole cluster network (in the absence of NetworkPolicy
  246. // rules).
  247. //
  248. // TODO(tomhjp): add a readiness probe or gate to Ingress Pods. There is a
  249. // small window where the Pod is marked ready but routing can still fail.
  250. if pg.Spec.Type == tsapi.ProxyGroupTypeEgress && !hasLocalAddrPortSet(proxyClass) {
  251. c.Lifecycle = &corev1.Lifecycle{
  252. PreStop: &corev1.LifecycleHandler{
  253. HTTPGet: &corev1.HTTPGetAction{
  254. Path: kubetypes.EgessServicesPreshutdownEP,
  255. Port: intstr.FromInt(defaultLocalAddrPort),
  256. },
  257. },
  258. }
  259. // Set the deletion grace period to 6 minutes to ensure that the pre-stop hook has enough time to terminate
  260. // gracefully.
  261. ss.Spec.Template.DeletionGracePeriodSeconds = ptr.To(deletionGracePeriodSeconds)
  262. }
  263. return ss, nil
  264. }
  265. func kubeAPIServerStatefulSet(pg *tsapi.ProxyGroup, namespace, image string, port *uint16) (*appsv1.StatefulSet, error) {
  266. sts := &appsv1.StatefulSet{
  267. ObjectMeta: metav1.ObjectMeta{
  268. Name: pg.Name,
  269. Namespace: namespace,
  270. Labels: pgLabels(pg.Name, nil),
  271. OwnerReferences: pgOwnerReference(pg),
  272. },
  273. Spec: appsv1.StatefulSetSpec{
  274. Replicas: ptr.To(pgReplicas(pg)),
  275. Selector: &metav1.LabelSelector{
  276. MatchLabels: pgLabels(pg.Name, nil),
  277. },
  278. Template: corev1.PodTemplateSpec{
  279. ObjectMeta: metav1.ObjectMeta{
  280. Name: pg.Name,
  281. Namespace: namespace,
  282. Labels: pgLabels(pg.Name, nil),
  283. DeletionGracePeriodSeconds: ptr.To[int64](10),
  284. },
  285. Spec: corev1.PodSpec{
  286. ServiceAccountName: pgServiceAccountName(pg),
  287. Containers: []corev1.Container{
  288. {
  289. Name: mainContainerName,
  290. Image: image,
  291. Env: func() []corev1.EnvVar {
  292. envs := []corev1.EnvVar{
  293. {
  294. // Used as default hostname and in Secret names.
  295. Name: "POD_NAME",
  296. ValueFrom: &corev1.EnvVarSource{
  297. FieldRef: &corev1.ObjectFieldSelector{
  298. FieldPath: "metadata.name",
  299. },
  300. },
  301. },
  302. {
  303. // Used by kubeclient to post Events about the Pod's lifecycle.
  304. Name: "POD_UID",
  305. ValueFrom: &corev1.EnvVarSource{
  306. FieldRef: &corev1.ObjectFieldSelector{
  307. FieldPath: "metadata.uid",
  308. },
  309. },
  310. },
  311. {
  312. // Used in an interpolated env var if metrics enabled.
  313. Name: "POD_IP",
  314. ValueFrom: &corev1.EnvVarSource{
  315. FieldRef: &corev1.ObjectFieldSelector{
  316. FieldPath: "status.podIP",
  317. },
  318. },
  319. },
  320. {
  321. // Included for completeness with POD_IP and easier backwards compatibility in future.
  322. Name: "POD_IPS",
  323. ValueFrom: &corev1.EnvVarSource{
  324. FieldRef: &corev1.ObjectFieldSelector{
  325. FieldPath: "status.podIPs",
  326. },
  327. },
  328. },
  329. {
  330. Name: "TS_K8S_PROXY_CONFIG",
  331. Value: "kube:" + types.NamespacedName{
  332. Namespace: namespace,
  333. Name: "$(POD_NAME)-config",
  334. }.String(),
  335. },
  336. {
  337. // This ensures that cert renewals can succeed if ACME account
  338. // keys have changed since issuance. We cannot guarantee or
  339. // validate that the account key has not changed, see
  340. // https://github.com/tailscale/tailscale/issues/18251
  341. Name: "TS_DEBUG_ACME_FORCE_RENEWAL",
  342. Value: "true",
  343. },
  344. }
  345. if port != nil {
  346. envs = append(envs, corev1.EnvVar{
  347. Name: "PORT",
  348. Value: strconv.Itoa(int(*port)),
  349. })
  350. }
  351. return envs
  352. }(),
  353. Ports: []corev1.ContainerPort{
  354. {
  355. Name: "k8s-proxy",
  356. ContainerPort: 443,
  357. Protocol: corev1.ProtocolTCP,
  358. },
  359. },
  360. },
  361. },
  362. },
  363. },
  364. },
  365. }
  366. return sts, nil
  367. }
  368. func pgServiceAccount(pg *tsapi.ProxyGroup, namespace string) *corev1.ServiceAccount {
  369. return &corev1.ServiceAccount{
  370. ObjectMeta: metav1.ObjectMeta{
  371. Name: pg.Name,
  372. Namespace: namespace,
  373. Labels: pgLabels(pg.Name, nil),
  374. OwnerReferences: pgOwnerReference(pg),
  375. },
  376. }
  377. }
  378. func pgRole(pg *tsapi.ProxyGroup, namespace string) *rbacv1.Role {
  379. return &rbacv1.Role{
  380. ObjectMeta: metav1.ObjectMeta{
  381. Name: pg.Name,
  382. Namespace: namespace,
  383. Labels: pgLabels(pg.Name, nil),
  384. OwnerReferences: pgOwnerReference(pg),
  385. },
  386. Rules: []rbacv1.PolicyRule{
  387. {
  388. APIGroups: []string{""},
  389. Resources: []string{"secrets"},
  390. Verbs: []string{
  391. "list",
  392. "watch", // For k8s-proxy.
  393. },
  394. },
  395. {
  396. APIGroups: []string{""},
  397. Resources: []string{"secrets"},
  398. Verbs: []string{
  399. "get",
  400. "patch",
  401. "update",
  402. },
  403. ResourceNames: func() (secrets []string) {
  404. for i := range pgReplicas(pg) {
  405. secrets = append(secrets,
  406. pgConfigSecretName(pg.Name, i), // Config with auth key.
  407. pgPodName(pg.Name, i), // State.
  408. )
  409. }
  410. return secrets
  411. }(),
  412. },
  413. {
  414. APIGroups: []string{""},
  415. Resources: []string{"events"},
  416. Verbs: []string{
  417. "create",
  418. "patch",
  419. "get",
  420. },
  421. },
  422. },
  423. }
  424. }
  425. func pgRoleBinding(pg *tsapi.ProxyGroup, namespace string) *rbacv1.RoleBinding {
  426. return &rbacv1.RoleBinding{
  427. ObjectMeta: metav1.ObjectMeta{
  428. Name: pg.Name,
  429. Namespace: namespace,
  430. Labels: pgLabels(pg.Name, nil),
  431. OwnerReferences: pgOwnerReference(pg),
  432. },
  433. Subjects: []rbacv1.Subject{
  434. {
  435. Kind: "ServiceAccount",
  436. Name: pgServiceAccountName(pg),
  437. Namespace: namespace,
  438. },
  439. },
  440. RoleRef: rbacv1.RoleRef{
  441. Kind: "Role",
  442. Name: pg.Name,
  443. },
  444. }
  445. }
  446. // kube-apiserver proxies in auth mode use a static ServiceAccount. Everything
  447. // else uses a per-ProxyGroup ServiceAccount.
  448. func pgServiceAccountName(pg *tsapi.ProxyGroup) string {
  449. if isAuthAPIServerProxy(pg) {
  450. return authAPIServerProxySAName
  451. }
  452. return pg.Name
  453. }
  454. func isAuthAPIServerProxy(pg *tsapi.ProxyGroup) bool {
  455. if pg.Spec.Type != tsapi.ProxyGroupTypeKubernetesAPIServer {
  456. return false
  457. }
  458. // The default is auth mode.
  459. return pg.Spec.KubeAPIServer == nil ||
  460. pg.Spec.KubeAPIServer.Mode == nil ||
  461. *pg.Spec.KubeAPIServer.Mode == tsapi.APIServerProxyModeAuth
  462. }
  463. func pgStateSecrets(pg *tsapi.ProxyGroup, namespace string) (secrets []*corev1.Secret) {
  464. for i := range pgReplicas(pg) {
  465. secrets = append(secrets, &corev1.Secret{
  466. ObjectMeta: metav1.ObjectMeta{
  467. Name: pgStateSecretName(pg.Name, i),
  468. Namespace: namespace,
  469. Labels: pgSecretLabels(pg.Name, kubetypes.LabelSecretTypeState),
  470. OwnerReferences: pgOwnerReference(pg),
  471. },
  472. })
  473. }
  474. return secrets
  475. }
  476. func pgEgressCM(pg *tsapi.ProxyGroup, namespace string) (*corev1.ConfigMap, []byte) {
  477. hp := hepPings(pg)
  478. hpBs := []byte(strconv.Itoa(hp))
  479. return &corev1.ConfigMap{
  480. ObjectMeta: metav1.ObjectMeta{
  481. Name: pgEgressCMName(pg.Name),
  482. Namespace: namespace,
  483. Labels: pgLabels(pg.Name, nil),
  484. OwnerReferences: pgOwnerReference(pg),
  485. },
  486. BinaryData: map[string][]byte{egressservices.KeyHEPPings: hpBs},
  487. }, hpBs
  488. }
  489. func pgIngressCM(pg *tsapi.ProxyGroup, namespace string) *corev1.ConfigMap {
  490. return &corev1.ConfigMap{
  491. ObjectMeta: metav1.ObjectMeta{
  492. Name: pgIngressCMName(pg.Name),
  493. Namespace: namespace,
  494. Labels: pgLabels(pg.Name, nil),
  495. OwnerReferences: pgOwnerReference(pg),
  496. },
  497. }
  498. }
  499. func pgSecretLabels(pgName, secretType string) map[string]string {
  500. return pgLabels(pgName, map[string]string{
  501. kubetypes.LabelSecretType: secretType, // "config" or "state".
  502. })
  503. }
  504. func pgLabels(pgName string, customLabels map[string]string) map[string]string {
  505. labels := make(map[string]string, len(customLabels)+3)
  506. for k, v := range customLabels {
  507. labels[k] = v
  508. }
  509. labels[kubetypes.LabelManaged] = "true"
  510. labels[LabelParentType] = "proxygroup"
  511. labels[LabelParentName] = pgName
  512. return labels
  513. }
  514. func pgOwnerReference(owner *tsapi.ProxyGroup) []metav1.OwnerReference {
  515. return []metav1.OwnerReference{*metav1.NewControllerRef(owner, tsapi.SchemeGroupVersion.WithKind("ProxyGroup"))}
  516. }
  517. func pgReplicas(pg *tsapi.ProxyGroup) int32 {
  518. if pg.Spec.Replicas != nil {
  519. return *pg.Spec.Replicas
  520. }
  521. return 2
  522. }
  523. func pgPodName(pgName string, i int32) string {
  524. return fmt.Sprintf("%s-%d", pgName, i)
  525. }
  526. func pgHostname(pg *tsapi.ProxyGroup, i int32) string {
  527. if pg.Spec.HostnamePrefix != "" {
  528. return fmt.Sprintf("%s-%d", pg.Spec.HostnamePrefix, i)
  529. }
  530. return fmt.Sprintf("%s-%d", pg.Name, i)
  531. }
  532. func pgConfigSecretName(pgName string, i int32) string {
  533. return fmt.Sprintf("%s-%d-config", pgName, i)
  534. }
  535. func pgStateSecretName(pgName string, i int32) string {
  536. return fmt.Sprintf("%s-%d", pgName, i)
  537. }
  538. func pgEgressCMName(pg string) string {
  539. return fmt.Sprintf("%s-egress-config", pg)
  540. }
  541. // hasLocalAddrPortSet returns true if the proxyclass has the TS_LOCAL_ADDR_PORT env var set. For egress ProxyGroups,
  542. // currently (2025-01-26) this means that the ProxyGroup does not support graceful failover.
  543. func hasLocalAddrPortSet(proxyClass *tsapi.ProxyClass) bool {
  544. if proxyClass == nil || proxyClass.Spec.StatefulSet == nil || proxyClass.Spec.StatefulSet.Pod == nil || proxyClass.Spec.StatefulSet.Pod.TailscaleContainer == nil {
  545. return false
  546. }
  547. return slices.ContainsFunc(proxyClass.Spec.StatefulSet.Pod.TailscaleContainer.Env, func(env tsapi.Env) bool {
  548. return env.Name == envVarTSLocalAddrPort
  549. })
  550. }
  551. // hepPings returns the number of times a health check endpoint exposed by a Service fronting ProxyGroup replicas should
  552. // be pinged to ensure that all currently configured backend replicas are hit.
  553. func hepPings(pg *tsapi.ProxyGroup) int {
  554. rc := pgReplicas(pg)
  555. // Assuming a Service implemented using round robin load balancing, number-of-replica-times should be enough, but in
  556. // practice, we cannot assume that the requests will be load balanced perfectly.
  557. return int(rc) * 3
  558. }