testutils_test.go 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988
  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. "fmt"
  9. "net/http"
  10. "net/netip"
  11. "path"
  12. "reflect"
  13. "strings"
  14. "sync"
  15. "testing"
  16. "time"
  17. "github.com/google/go-cmp/cmp"
  18. "go.uber.org/zap"
  19. appsv1 "k8s.io/api/apps/v1"
  20. corev1 "k8s.io/api/core/v1"
  21. apierrors "k8s.io/apimachinery/pkg/api/errors"
  22. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  23. "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
  24. "k8s.io/apimachinery/pkg/types"
  25. "k8s.io/client-go/tools/record"
  26. "sigs.k8s.io/controller-runtime/pkg/client"
  27. "sigs.k8s.io/controller-runtime/pkg/reconcile"
  28. "tailscale.com/internal/client/tailscale"
  29. "tailscale.com/ipn"
  30. "tailscale.com/ipn/ipnstate"
  31. tsapi "tailscale.com/k8s-operator/apis/v1alpha1"
  32. "tailscale.com/kube/kubetypes"
  33. "tailscale.com/tailcfg"
  34. "tailscale.com/types/ptr"
  35. "tailscale.com/util/mak"
  36. )
  37. const (
  38. vipTestIP = "5.6.7.8"
  39. )
  40. // confgOpts contains configuration options for creating cluster resources for
  41. // Tailscale proxies.
  42. type configOpts struct {
  43. stsName string
  44. secretName string
  45. hostname string
  46. namespace string
  47. tailscaleNamespace string
  48. namespaced bool
  49. parentType string
  50. proxyType string
  51. priorityClassName string
  52. firewallMode string
  53. tailnetTargetIP string
  54. tailnetTargetFQDN string
  55. clusterTargetIP string
  56. clusterTargetDNS string
  57. subnetRoutes string
  58. isExitNode bool
  59. isAppConnector bool
  60. serveConfig *ipn.ServeConfig
  61. shouldEnableForwardingClusterTrafficViaIngress bool
  62. proxyClass string // configuration from the named ProxyClass should be applied to proxy resources
  63. app string
  64. shouldRemoveAuthKey bool
  65. secretExtraData map[string][]byte
  66. resourceVersion string
  67. replicas *int32
  68. enableMetrics bool
  69. serviceMonitorLabels tsapi.Labels
  70. }
  71. func expectedSTS(t *testing.T, cl client.Client, opts configOpts) *appsv1.StatefulSet {
  72. t.Helper()
  73. zl, err := zap.NewDevelopment()
  74. if err != nil {
  75. t.Fatal(err)
  76. }
  77. tsContainer := corev1.Container{
  78. Name: "tailscale",
  79. Image: "tailscale/tailscale",
  80. Env: []corev1.EnvVar{
  81. {Name: "TS_USERSPACE", Value: "false"},
  82. {Name: "POD_IP", ValueFrom: &corev1.EnvVarSource{FieldRef: &corev1.ObjectFieldSelector{APIVersion: "", FieldPath: "status.podIP"}, ResourceFieldRef: nil, ConfigMapKeyRef: nil, SecretKeyRef: nil}},
  83. {Name: "POD_NAME", ValueFrom: &corev1.EnvVarSource{FieldRef: &corev1.ObjectFieldSelector{APIVersion: "", FieldPath: "metadata.name"}, ResourceFieldRef: nil, ConfigMapKeyRef: nil, SecretKeyRef: nil}},
  84. {Name: "POD_UID", ValueFrom: &corev1.EnvVarSource{FieldRef: &corev1.ObjectFieldSelector{APIVersion: "", FieldPath: "metadata.uid"}, ResourceFieldRef: nil, ConfigMapKeyRef: nil, SecretKeyRef: nil}},
  85. {Name: "TS_KUBE_SECRET", Value: "$(POD_NAME)"},
  86. {Name: "TS_EXPERIMENTAL_VERSIONED_CONFIG_DIR", Value: "/etc/tsconfig/$(POD_NAME)"},
  87. },
  88. SecurityContext: &corev1.SecurityContext{
  89. Privileged: ptr.To(true),
  90. },
  91. ImagePullPolicy: "Always",
  92. }
  93. if opts.shouldEnableForwardingClusterTrafficViaIngress {
  94. tsContainer.Env = append(tsContainer.Env, corev1.EnvVar{
  95. Name: "EXPERIMENTAL_ALLOW_PROXYING_CLUSTER_TRAFFIC_VIA_INGRESS",
  96. Value: "true",
  97. })
  98. }
  99. var annots map[string]string
  100. var volumes []corev1.Volume
  101. volumes = []corev1.Volume{
  102. {
  103. Name: "tailscaledconfig-0",
  104. VolumeSource: corev1.VolumeSource{
  105. Secret: &corev1.SecretVolumeSource{
  106. SecretName: opts.secretName,
  107. },
  108. },
  109. },
  110. }
  111. tsContainer.VolumeMounts = []corev1.VolumeMount{{
  112. Name: "tailscaledconfig-0",
  113. ReadOnly: true,
  114. MountPath: "/etc/tsconfig/" + opts.secretName,
  115. }}
  116. if opts.firewallMode != "" {
  117. tsContainer.Env = append(tsContainer.Env, corev1.EnvVar{
  118. Name: "TS_DEBUG_FIREWALL_MODE",
  119. Value: opts.firewallMode,
  120. })
  121. }
  122. if opts.tailnetTargetIP != "" {
  123. mak.Set(&annots, "tailscale.com/operator-last-set-ts-tailnet-target-ip", opts.tailnetTargetIP)
  124. tsContainer.Env = append(tsContainer.Env, corev1.EnvVar{
  125. Name: "TS_TAILNET_TARGET_IP",
  126. Value: opts.tailnetTargetIP,
  127. })
  128. } else if opts.tailnetTargetFQDN != "" {
  129. mak.Set(&annots, "tailscale.com/operator-last-set-ts-tailnet-target-fqdn", opts.tailnetTargetFQDN)
  130. tsContainer.Env = append(tsContainer.Env, corev1.EnvVar{
  131. Name: "TS_TAILNET_TARGET_FQDN",
  132. Value: opts.tailnetTargetFQDN,
  133. })
  134. } else if opts.clusterTargetIP != "" {
  135. tsContainer.Env = append(tsContainer.Env, corev1.EnvVar{
  136. Name: "TS_DEST_IP",
  137. Value: opts.clusterTargetIP,
  138. })
  139. mak.Set(&annots, "tailscale.com/operator-last-set-cluster-ip", opts.clusterTargetIP)
  140. } else if opts.clusterTargetDNS != "" {
  141. tsContainer.Env = append(tsContainer.Env, corev1.EnvVar{
  142. Name: "TS_EXPERIMENTAL_DEST_DNS_NAME",
  143. Value: opts.clusterTargetDNS,
  144. })
  145. mak.Set(&annots, "tailscale.com/operator-last-set-cluster-dns-name", opts.clusterTargetDNS)
  146. }
  147. if opts.serveConfig != nil {
  148. tsContainer.Env = append(tsContainer.Env, corev1.EnvVar{
  149. Name: "TS_SERVE_CONFIG",
  150. Value: "/etc/tailscaled/$(POD_NAME)/serve-config",
  151. })
  152. volumes = append(volumes, corev1.Volume{
  153. Name: "serve-config-0",
  154. VolumeSource: corev1.VolumeSource{
  155. Secret: &corev1.SecretVolumeSource{
  156. SecretName: opts.secretName,
  157. Items: []corev1.KeyToPath{{
  158. Key: "serve-config",
  159. Path: "serve-config",
  160. }},
  161. },
  162. },
  163. })
  164. tsContainer.VolumeMounts = append(tsContainer.VolumeMounts, corev1.VolumeMount{Name: "serve-config-0", ReadOnly: true, MountPath: path.Join("/etc/tailscaled", opts.secretName)})
  165. }
  166. tsContainer.Env = append(tsContainer.Env, corev1.EnvVar{
  167. Name: "TS_INTERNAL_APP",
  168. Value: opts.app,
  169. })
  170. if opts.enableMetrics {
  171. tsContainer.Env = append(tsContainer.Env,
  172. corev1.EnvVar{
  173. Name: "TS_DEBUG_ADDR_PORT",
  174. Value: "$(POD_IP):9001"},
  175. corev1.EnvVar{
  176. Name: "TS_TAILSCALED_EXTRA_ARGS",
  177. Value: "--debug=$(TS_DEBUG_ADDR_PORT)",
  178. },
  179. corev1.EnvVar{
  180. Name: "TS_LOCAL_ADDR_PORT",
  181. Value: "$(POD_IP):9002",
  182. },
  183. corev1.EnvVar{
  184. Name: "TS_ENABLE_METRICS",
  185. Value: "true",
  186. },
  187. )
  188. tsContainer.Ports = append(tsContainer.Ports,
  189. corev1.ContainerPort{Name: "debug", ContainerPort: 9001, Protocol: "TCP"},
  190. corev1.ContainerPort{Name: "metrics", ContainerPort: 9002, Protocol: "TCP"},
  191. )
  192. }
  193. ss := &appsv1.StatefulSet{
  194. TypeMeta: metav1.TypeMeta{
  195. Kind: "StatefulSet",
  196. APIVersion: "apps/v1",
  197. },
  198. ObjectMeta: metav1.ObjectMeta{
  199. Name: opts.stsName,
  200. Namespace: "operator-ns",
  201. Labels: map[string]string{
  202. "tailscale.com/managed": "true",
  203. "tailscale.com/parent-resource": "test",
  204. "tailscale.com/parent-resource-ns": opts.namespace,
  205. "tailscale.com/parent-resource-type": opts.parentType,
  206. },
  207. },
  208. Spec: appsv1.StatefulSetSpec{
  209. Replicas: opts.replicas,
  210. Selector: &metav1.LabelSelector{
  211. MatchLabels: map[string]string{"app": "1234-UID"},
  212. },
  213. ServiceName: opts.stsName,
  214. Template: corev1.PodTemplateSpec{
  215. ObjectMeta: metav1.ObjectMeta{
  216. Annotations: annots,
  217. DeletionGracePeriodSeconds: ptr.To[int64](10),
  218. Labels: map[string]string{
  219. "tailscale.com/managed": "true",
  220. "tailscale.com/parent-resource": "test",
  221. "tailscale.com/parent-resource-ns": opts.namespace,
  222. "tailscale.com/parent-resource-type": opts.parentType,
  223. "app": "1234-UID",
  224. },
  225. },
  226. Spec: corev1.PodSpec{
  227. ServiceAccountName: "proxies",
  228. PriorityClassName: opts.priorityClassName,
  229. InitContainers: []corev1.Container{
  230. {
  231. Name: "sysctler",
  232. Image: "tailscale/tailscale",
  233. Command: []string{"/bin/sh", "-c"},
  234. Args: []string{"sysctl -w net.ipv4.ip_forward=1 && if sysctl net.ipv6.conf.all.forwarding; then sysctl -w net.ipv6.conf.all.forwarding=1; fi"},
  235. SecurityContext: &corev1.SecurityContext{
  236. Privileged: ptr.To(true),
  237. },
  238. },
  239. },
  240. Containers: []corev1.Container{tsContainer},
  241. Volumes: volumes,
  242. },
  243. },
  244. },
  245. }
  246. // If opts.proxyClass is set, retrieve the ProxyClass and apply
  247. // configuration from that to the StatefulSet.
  248. if opts.proxyClass != "" {
  249. t.Logf("applying configuration from ProxyClass %s", opts.proxyClass)
  250. proxyClass := new(tsapi.ProxyClass)
  251. if err := cl.Get(context.Background(), types.NamespacedName{Name: opts.proxyClass}, proxyClass); err != nil {
  252. t.Fatalf("error getting ProxyClass: %v", err)
  253. }
  254. return applyProxyClassToStatefulSet(proxyClass, ss, new(tailscaleSTSConfig), zl.Sugar())
  255. }
  256. return ss
  257. }
  258. func expectedSTSUserspace(t *testing.T, cl client.Client, opts configOpts) *appsv1.StatefulSet {
  259. t.Helper()
  260. zl, err := zap.NewDevelopment()
  261. if err != nil {
  262. t.Fatal(err)
  263. }
  264. tsContainer := corev1.Container{
  265. Name: "tailscale",
  266. Image: "tailscale/tailscale",
  267. Env: []corev1.EnvVar{
  268. {Name: "TS_USERSPACE", Value: "true"},
  269. {Name: "POD_IP", ValueFrom: &corev1.EnvVarSource{FieldRef: &corev1.ObjectFieldSelector{APIVersion: "", FieldPath: "status.podIP"}, ResourceFieldRef: nil, ConfigMapKeyRef: nil, SecretKeyRef: nil}},
  270. {Name: "POD_NAME", ValueFrom: &corev1.EnvVarSource{FieldRef: &corev1.ObjectFieldSelector{APIVersion: "", FieldPath: "metadata.name"}, ResourceFieldRef: nil, ConfigMapKeyRef: nil, SecretKeyRef: nil}},
  271. {Name: "POD_UID", ValueFrom: &corev1.EnvVarSource{FieldRef: &corev1.ObjectFieldSelector{APIVersion: "", FieldPath: "metadata.uid"}, ResourceFieldRef: nil, ConfigMapKeyRef: nil, SecretKeyRef: nil}},
  272. {Name: "TS_KUBE_SECRET", Value: "$(POD_NAME)"},
  273. {Name: "TS_EXPERIMENTAL_VERSIONED_CONFIG_DIR", Value: "/etc/tsconfig/$(POD_NAME)"},
  274. {Name: "TS_SERVE_CONFIG", Value: "/etc/tailscaled/$(POD_NAME)/serve-config"},
  275. {Name: "TS_INTERNAL_APP", Value: opts.app},
  276. },
  277. ImagePullPolicy: "Always",
  278. VolumeMounts: []corev1.VolumeMount{
  279. {Name: "tailscaledconfig-0", ReadOnly: true, MountPath: path.Join("/etc/tsconfig", opts.secretName)},
  280. {Name: "serve-config-0", ReadOnly: true, MountPath: path.Join("/etc/tailscaled", opts.secretName)},
  281. },
  282. }
  283. if opts.enableMetrics {
  284. tsContainer.Env = append(tsContainer.Env,
  285. corev1.EnvVar{
  286. Name: "TS_DEBUG_ADDR_PORT",
  287. Value: "$(POD_IP):9001"},
  288. corev1.EnvVar{
  289. Name: "TS_TAILSCALED_EXTRA_ARGS",
  290. Value: "--debug=$(TS_DEBUG_ADDR_PORT)",
  291. },
  292. corev1.EnvVar{
  293. Name: "TS_LOCAL_ADDR_PORT",
  294. Value: "$(POD_IP):9002",
  295. },
  296. corev1.EnvVar{
  297. Name: "TS_ENABLE_METRICS",
  298. Value: "true",
  299. },
  300. )
  301. tsContainer.Ports = append(tsContainer.Ports, corev1.ContainerPort{
  302. Name: "debug", ContainerPort: 9001, Protocol: "TCP"},
  303. corev1.ContainerPort{Name: "metrics", ContainerPort: 9002, Protocol: "TCP"},
  304. )
  305. }
  306. volumes := []corev1.Volume{
  307. {
  308. Name: "tailscaledconfig-0",
  309. VolumeSource: corev1.VolumeSource{
  310. Secret: &corev1.SecretVolumeSource{
  311. SecretName: opts.secretName,
  312. },
  313. },
  314. },
  315. {
  316. Name: "serve-config-0",
  317. VolumeSource: corev1.VolumeSource{
  318. Secret: &corev1.SecretVolumeSource{
  319. SecretName: opts.secretName,
  320. Items: []corev1.KeyToPath{{Key: "serve-config", Path: "serve-config"}},
  321. },
  322. },
  323. },
  324. }
  325. ss := &appsv1.StatefulSet{
  326. TypeMeta: metav1.TypeMeta{
  327. Kind: "StatefulSet",
  328. APIVersion: "apps/v1",
  329. },
  330. ObjectMeta: metav1.ObjectMeta{
  331. Name: opts.stsName,
  332. Namespace: "operator-ns",
  333. Labels: map[string]string{
  334. "tailscale.com/managed": "true",
  335. "tailscale.com/parent-resource": "test",
  336. "tailscale.com/parent-resource-ns": opts.namespace,
  337. "tailscale.com/parent-resource-type": opts.parentType,
  338. },
  339. },
  340. Spec: appsv1.StatefulSetSpec{
  341. Replicas: ptr.To[int32](1),
  342. Selector: &metav1.LabelSelector{
  343. MatchLabels: map[string]string{"app": "1234-UID"},
  344. },
  345. ServiceName: opts.stsName,
  346. Template: corev1.PodTemplateSpec{
  347. ObjectMeta: metav1.ObjectMeta{
  348. DeletionGracePeriodSeconds: ptr.To[int64](10),
  349. Labels: map[string]string{
  350. "tailscale.com/managed": "true",
  351. "tailscale.com/parent-resource": "test",
  352. "tailscale.com/parent-resource-ns": opts.namespace,
  353. "tailscale.com/parent-resource-type": opts.parentType,
  354. "app": "1234-UID",
  355. },
  356. },
  357. Spec: corev1.PodSpec{
  358. ServiceAccountName: "proxies",
  359. PriorityClassName: opts.priorityClassName,
  360. Containers: []corev1.Container{tsContainer},
  361. Volumes: volumes,
  362. },
  363. },
  364. },
  365. }
  366. // If opts.proxyClass is set, retrieve the ProxyClass and apply
  367. // configuration from that to the StatefulSet.
  368. if opts.proxyClass != "" {
  369. t.Logf("applying configuration from ProxyClass %s", opts.proxyClass)
  370. proxyClass := new(tsapi.ProxyClass)
  371. if err := cl.Get(context.Background(), types.NamespacedName{Name: opts.proxyClass}, proxyClass); err != nil {
  372. t.Fatalf("error getting ProxyClass: %v", err)
  373. }
  374. return applyProxyClassToStatefulSet(proxyClass, ss, new(tailscaleSTSConfig), zl.Sugar())
  375. }
  376. return ss
  377. }
  378. func expectedHeadlessService(name string, parentType string) *corev1.Service {
  379. return &corev1.Service{
  380. ObjectMeta: metav1.ObjectMeta{
  381. Name: name,
  382. GenerateName: "ts-test-",
  383. Namespace: "operator-ns",
  384. Labels: map[string]string{
  385. "tailscale.com/managed": "true",
  386. "tailscale.com/parent-resource": "test",
  387. "tailscale.com/parent-resource-ns": "default",
  388. "tailscale.com/parent-resource-type": parentType,
  389. },
  390. },
  391. Spec: corev1.ServiceSpec{
  392. Selector: map[string]string{
  393. "app": "1234-UID",
  394. },
  395. ClusterIP: "None",
  396. IPFamilyPolicy: ptr.To(corev1.IPFamilyPolicyPreferDualStack),
  397. },
  398. }
  399. }
  400. func expectedMetricsService(opts configOpts) *corev1.Service {
  401. labels := metricsLabels(opts)
  402. selector := map[string]string{
  403. "tailscale.com/managed": "true",
  404. "tailscale.com/parent-resource": "test",
  405. "tailscale.com/parent-resource-type": opts.parentType,
  406. }
  407. if opts.namespaced {
  408. selector["tailscale.com/parent-resource-ns"] = opts.namespace
  409. }
  410. return &corev1.Service{
  411. ObjectMeta: metav1.ObjectMeta{
  412. Name: metricsResourceName(opts.stsName),
  413. Namespace: opts.tailscaleNamespace,
  414. Labels: labels,
  415. },
  416. Spec: corev1.ServiceSpec{
  417. Selector: selector,
  418. Type: corev1.ServiceTypeClusterIP,
  419. Ports: []corev1.ServicePort{{Protocol: "TCP", Port: 9002, Name: "metrics"}},
  420. },
  421. }
  422. }
  423. func metricsLabels(opts configOpts) map[string]string {
  424. promJob := fmt.Sprintf("ts_%s_default_test", opts.proxyType)
  425. if !opts.namespaced {
  426. promJob = fmt.Sprintf("ts_%s_test", opts.proxyType)
  427. }
  428. labels := map[string]string{
  429. "tailscale.com/managed": "true",
  430. "tailscale.com/metrics-target": opts.stsName,
  431. "ts_prom_job": promJob,
  432. "ts_proxy_type": opts.proxyType,
  433. "ts_proxy_parent_name": "test",
  434. }
  435. if opts.namespaced {
  436. labels["ts_proxy_parent_namespace"] = "default"
  437. }
  438. return labels
  439. }
  440. func expectedServiceMonitor(t *testing.T, opts configOpts) *unstructured.Unstructured {
  441. t.Helper()
  442. smLabels := metricsLabels(opts)
  443. if len(opts.serviceMonitorLabels) != 0 {
  444. smLabels = mergeMapKeys(smLabels, opts.serviceMonitorLabels.Parse())
  445. }
  446. name := metricsResourceName(opts.stsName)
  447. sm := &ServiceMonitor{
  448. ObjectMeta: metav1.ObjectMeta{
  449. Name: name,
  450. Namespace: opts.tailscaleNamespace,
  451. Labels: smLabels,
  452. ResourceVersion: opts.resourceVersion,
  453. OwnerReferences: []metav1.OwnerReference{{APIVersion: "v1", Kind: "Service", Name: name, BlockOwnerDeletion: ptr.To(true), Controller: ptr.To(true)}},
  454. },
  455. TypeMeta: metav1.TypeMeta{
  456. Kind: "ServiceMonitor",
  457. APIVersion: "monitoring.coreos.com/v1",
  458. },
  459. Spec: ServiceMonitorSpec{
  460. Selector: metav1.LabelSelector{MatchLabels: metricsLabels(opts)},
  461. Endpoints: []ServiceMonitorEndpoint{{
  462. Port: "metrics",
  463. }},
  464. NamespaceSelector: ServiceMonitorNamespaceSelector{
  465. MatchNames: []string{opts.tailscaleNamespace},
  466. },
  467. JobLabel: "ts_prom_job",
  468. TargetLabels: []string{
  469. "ts_proxy_parent_name",
  470. "ts_proxy_parent_namespace",
  471. "ts_proxy_type",
  472. },
  473. },
  474. }
  475. u, err := serviceMonitorToUnstructured(sm)
  476. if err != nil {
  477. t.Fatalf("error converting ServiceMonitor to unstructured: %v", err)
  478. }
  479. return u
  480. }
  481. func expectedSecret(t *testing.T, cl client.Client, opts configOpts) *corev1.Secret {
  482. t.Helper()
  483. s := &corev1.Secret{
  484. ObjectMeta: metav1.ObjectMeta{
  485. Name: opts.secretName,
  486. Namespace: "operator-ns",
  487. },
  488. }
  489. if opts.serveConfig != nil {
  490. serveConfigBs, err := json.Marshal(opts.serveConfig)
  491. if err != nil {
  492. t.Fatalf("error marshalling serve config: %v", err)
  493. }
  494. mak.Set(&s.StringData, "serve-config", string(serveConfigBs))
  495. }
  496. conf := &ipn.ConfigVAlpha{
  497. Version: "alpha0",
  498. AcceptDNS: "false",
  499. Hostname: &opts.hostname,
  500. Locked: "false",
  501. AuthKey: ptr.To("secret-authkey"),
  502. AcceptRoutes: "false",
  503. AppConnector: &ipn.AppConnectorPrefs{Advertise: false},
  504. NoStatefulFiltering: "true",
  505. }
  506. if opts.proxyClass != "" {
  507. t.Logf("applying configuration from ProxyClass %s", opts.proxyClass)
  508. proxyClass := new(tsapi.ProxyClass)
  509. if err := cl.Get(context.Background(), types.NamespacedName{Name: opts.proxyClass}, proxyClass); err != nil {
  510. t.Fatalf("error getting ProxyClass: %v", err)
  511. }
  512. if proxyClass.Spec.TailscaleConfig != nil && proxyClass.Spec.TailscaleConfig.AcceptRoutes {
  513. conf.AcceptRoutes = "true"
  514. }
  515. }
  516. if opts.shouldRemoveAuthKey {
  517. conf.AuthKey = nil
  518. }
  519. if opts.isAppConnector {
  520. conf.AppConnector = &ipn.AppConnectorPrefs{Advertise: true}
  521. }
  522. var routes []netip.Prefix
  523. if opts.subnetRoutes != "" || opts.isExitNode {
  524. r := opts.subnetRoutes
  525. if opts.isExitNode {
  526. r = "0.0.0.0/0,::/0," + r
  527. }
  528. for _, rr := range strings.Split(r, ",") {
  529. prefix, err := netip.ParsePrefix(rr)
  530. if err != nil {
  531. t.Fatal(err)
  532. }
  533. routes = append(routes, prefix)
  534. }
  535. }
  536. conf.AdvertiseRoutes = routes
  537. bnn, err := json.Marshal(conf)
  538. if err != nil {
  539. t.Fatalf("error marshalling tailscaled config")
  540. }
  541. conf.AppConnector = nil
  542. bn, err := json.Marshal(conf)
  543. if err != nil {
  544. t.Fatalf("error marshalling tailscaled config")
  545. }
  546. mak.Set(&s.StringData, "cap-95.hujson", string(bn))
  547. mak.Set(&s.StringData, "cap-107.hujson", string(bnn))
  548. labels := map[string]string{
  549. "tailscale.com/managed": "true",
  550. "tailscale.com/parent-resource": "test",
  551. "tailscale.com/parent-resource-ns": "default",
  552. "tailscale.com/parent-resource-type": opts.parentType,
  553. }
  554. if opts.parentType == "connector" {
  555. labels["tailscale.com/parent-resource-ns"] = "" // Connector is cluster scoped
  556. }
  557. s.Labels = labels
  558. for key, val := range opts.secretExtraData {
  559. mak.Set(&s.Data, key, val)
  560. }
  561. return s
  562. }
  563. func findNoGenName(t *testing.T, client client.Client, ns, name, typ string) {
  564. t.Helper()
  565. labels := map[string]string{
  566. kubetypes.LabelManaged: "true",
  567. LabelParentName: name,
  568. LabelParentNamespace: ns,
  569. LabelParentType: typ,
  570. }
  571. s, err := getSingleObject[corev1.Secret](context.Background(), client, "operator-ns", labels)
  572. if err != nil {
  573. t.Fatalf("finding secrets for %q: %v", name, err)
  574. }
  575. if s != nil {
  576. t.Fatalf("found unexpected secret with name %q", s.GetName())
  577. }
  578. }
  579. func findGenName(t *testing.T, client client.Client, ns, name, typ string) (full, noSuffix string) {
  580. t.Helper()
  581. labels := map[string]string{
  582. kubetypes.LabelManaged: "true",
  583. LabelParentName: name,
  584. LabelParentNamespace: ns,
  585. LabelParentType: typ,
  586. }
  587. s, err := getSingleObject[corev1.Secret](context.Background(), client, "operator-ns", labels)
  588. if err != nil {
  589. t.Fatalf("finding secret for %q: %v", name, err)
  590. }
  591. if s == nil {
  592. t.Fatalf("no secret found for %q %s %+#v", name, ns, labels)
  593. }
  594. return s.GetName(), strings.TrimSuffix(s.GetName(), "-0")
  595. }
  596. func findGenNames(t *testing.T, cl client.Client, ns, name, typ string) []string {
  597. t.Helper()
  598. labels := map[string]string{
  599. kubetypes.LabelManaged: "true",
  600. LabelParentName: name,
  601. LabelParentNamespace: ns,
  602. LabelParentType: typ,
  603. }
  604. var list corev1.SecretList
  605. if err := cl.List(t.Context(), &list, client.InNamespace(ns), client.MatchingLabels(labels)); err != nil {
  606. t.Fatalf("finding secrets for %q: %v", name, err)
  607. }
  608. if len(list.Items) == 0 {
  609. t.Fatalf("no secrets found for %q %s %+#v", name, ns, labels)
  610. }
  611. names := make([]string, len(list.Items))
  612. for i, secret := range list.Items {
  613. names[i] = secret.GetName()
  614. }
  615. return names
  616. }
  617. func mustCreate(t *testing.T, client client.Client, obj client.Object) {
  618. t.Helper()
  619. if err := client.Create(context.Background(), obj); err != nil {
  620. t.Fatalf("creating %q: %v", obj.GetName(), err)
  621. }
  622. }
  623. func mustCreateAll(t *testing.T, client client.Client, objs ...client.Object) {
  624. t.Helper()
  625. for _, obj := range objs {
  626. mustCreate(t, client, obj)
  627. }
  628. }
  629. func mustDeleteAll(t *testing.T, client client.Client, objs ...client.Object) {
  630. t.Helper()
  631. for _, obj := range objs {
  632. if err := client.Delete(context.Background(), obj); err != nil {
  633. t.Fatalf("deleting %q: %v", obj.GetName(), err)
  634. }
  635. }
  636. }
  637. func mustUpdate[T any, O ptrObject[T]](t *testing.T, client client.Client, ns, name string, update func(O)) {
  638. t.Helper()
  639. obj := O(new(T))
  640. if err := client.Get(context.Background(), types.NamespacedName{
  641. Name: name,
  642. Namespace: ns,
  643. }, obj); err != nil {
  644. t.Fatalf("getting %q: %v", name, err)
  645. }
  646. update(obj)
  647. if err := client.Update(context.Background(), obj); err != nil {
  648. t.Fatalf("updating %q: %v", name, err)
  649. }
  650. }
  651. func mustUpdateStatus[T any, O ptrObject[T]](t *testing.T, client client.Client, ns, name string, update func(O)) {
  652. t.Helper()
  653. obj := O(new(T))
  654. if err := client.Get(context.Background(), types.NamespacedName{
  655. Name: name,
  656. Namespace: ns,
  657. }, obj); err != nil {
  658. t.Fatalf("getting %q: %v", name, err)
  659. }
  660. update(obj)
  661. if err := client.Status().Update(context.Background(), obj); err != nil {
  662. t.Fatalf("updating %q: %v", name, err)
  663. }
  664. }
  665. // expectEqual accepts a Kubernetes object and a Kubernetes client. It tests
  666. // whether an object with equivalent contents can be retrieved by the passed
  667. // client. If you want to NOT test some object fields for equality, use the
  668. // modify func to ensure that they are removed from the cluster object and the
  669. // object passed as 'want'. If no such modifications are needed, you can pass
  670. // nil in place of the modify function.
  671. func expectEqual[T any, O ptrObject[T]](t *testing.T, client client.Client, want O, modifiers ...func(O)) {
  672. t.Helper()
  673. got := O(new(T))
  674. if err := client.Get(context.Background(), types.NamespacedName{
  675. Name: want.GetName(),
  676. Namespace: want.GetNamespace(),
  677. }, got); err != nil {
  678. t.Fatalf("getting %q: %v", want.GetName(), err)
  679. }
  680. // The resource version changes eagerly whenever the operator does even a
  681. // no-op update. Asserting a specific value leads to overly brittle tests,
  682. // so just remove it from both got and want.
  683. got.SetResourceVersion("")
  684. want.SetResourceVersion("")
  685. for _, modifier := range modifiers {
  686. modifier(want)
  687. modifier(got)
  688. }
  689. if diff := cmp.Diff(got, want); diff != "" {
  690. t.Fatalf("unexpected %s (-got +want):\n%s", reflect.TypeOf(want).Elem().Name(), diff)
  691. }
  692. }
  693. func expectEqualUnstructured(t *testing.T, client client.Client, want *unstructured.Unstructured) {
  694. t.Helper()
  695. got := &unstructured.Unstructured{}
  696. got.SetGroupVersionKind(want.GroupVersionKind())
  697. if err := client.Get(context.Background(), types.NamespacedName{
  698. Name: want.GetName(),
  699. Namespace: want.GetNamespace(),
  700. }, got); err != nil {
  701. t.Fatalf("getting %q: %v", want.GetName(), err)
  702. }
  703. if diff := cmp.Diff(got, want); diff != "" {
  704. t.Fatalf("unexpected contents of Unstructured (-got +want):\n%s", diff)
  705. }
  706. }
  707. func expectMissing[T any, O ptrObject[T]](t *testing.T, client client.Client, ns, name string) {
  708. t.Helper()
  709. obj := O(new(T))
  710. err := client.Get(context.Background(), types.NamespacedName{
  711. Name: name,
  712. Namespace: ns,
  713. }, obj)
  714. if !apierrors.IsNotFound(err) {
  715. t.Fatalf("%s %s/%s unexpectedly present, wanted missing", reflect.TypeOf(obj).Elem().Name(), ns, name)
  716. }
  717. }
  718. func expectReconciled(t *testing.T, sr reconcile.Reconciler, ns, name string) {
  719. t.Helper()
  720. req := reconcile.Request{
  721. NamespacedName: types.NamespacedName{
  722. Namespace: ns,
  723. Name: name,
  724. },
  725. }
  726. res, err := sr.Reconcile(context.Background(), req)
  727. if err != nil {
  728. t.Fatalf("Reconcile: unexpected error: %v", err)
  729. }
  730. if res.Requeue {
  731. t.Fatalf("unexpected immediate requeue")
  732. }
  733. if res.RequeueAfter != 0 {
  734. t.Fatalf("unexpected timed requeue (%v)", res.RequeueAfter)
  735. }
  736. }
  737. func expectRequeue(t *testing.T, sr reconcile.Reconciler, ns, name string) {
  738. t.Helper()
  739. req := reconcile.Request{
  740. NamespacedName: types.NamespacedName{
  741. Name: name,
  742. Namespace: ns,
  743. },
  744. }
  745. res, err := sr.Reconcile(context.Background(), req)
  746. if err != nil {
  747. t.Fatalf("Reconcile: unexpected error: %v", err)
  748. }
  749. if res.RequeueAfter == 0 {
  750. t.Fatalf("expected timed requeue, got success")
  751. }
  752. }
  753. func expectError(t *testing.T, sr reconcile.Reconciler, ns, name string) {
  754. t.Helper()
  755. req := reconcile.Request{
  756. NamespacedName: types.NamespacedName{
  757. Name: name,
  758. Namespace: ns,
  759. },
  760. }
  761. _, err := sr.Reconcile(context.Background(), req)
  762. if err == nil {
  763. t.Error("Reconcile: expected error but did not get one")
  764. }
  765. }
  766. // expectEvents accepts a test recorder and a list of events, tests that expected
  767. // events are sent down the recorder's channel. Waits for 5s for each event.
  768. func expectEvents(t *testing.T, rec *record.FakeRecorder, wantsEvents []string) {
  769. t.Helper()
  770. // Events are not expected to arrive in order.
  771. seenEvents := make([]string, 0)
  772. for range len(wantsEvents) {
  773. timer := time.NewTimer(time.Second * 5)
  774. defer timer.Stop()
  775. select {
  776. case gotEvent := <-rec.Events:
  777. found := false
  778. for _, wantEvent := range wantsEvents {
  779. if wantEvent == gotEvent {
  780. found = true
  781. seenEvents = append(seenEvents, gotEvent)
  782. break
  783. }
  784. }
  785. if !found {
  786. t.Errorf("got unexpected event %q, expected events: %+#v", gotEvent, wantsEvents)
  787. }
  788. case <-timer.C:
  789. t.Errorf("timeout waiting for an event, wants events %#+v, got events %+#v", wantsEvents, seenEvents)
  790. }
  791. }
  792. }
  793. type fakeTSClient struct {
  794. sync.Mutex
  795. keyRequests []tailscale.KeyCapabilities
  796. deleted []string
  797. vipServices map[tailcfg.ServiceName]*tailscale.VIPService
  798. }
  799. type fakeTSNetServer struct {
  800. certDomains []string
  801. }
  802. func (f *fakeTSNetServer) CertDomains() []string {
  803. return f.certDomains
  804. }
  805. func (c *fakeTSClient) CreateKey(ctx context.Context, caps tailscale.KeyCapabilities) (string, *tailscale.Key, error) {
  806. c.Lock()
  807. defer c.Unlock()
  808. c.keyRequests = append(c.keyRequests, caps)
  809. k := &tailscale.Key{
  810. ID: "key",
  811. Created: time.Now(),
  812. Capabilities: caps,
  813. }
  814. return "secret-authkey", k, nil
  815. }
  816. func (c *fakeTSClient) Device(ctx context.Context, deviceID string, fields *tailscale.DeviceFieldsOpts) (*tailscale.Device, error) {
  817. return &tailscale.Device{
  818. DeviceID: deviceID,
  819. Hostname: "hostname-" + deviceID,
  820. Addresses: []string{
  821. "1.2.3.4",
  822. "::1",
  823. },
  824. }, nil
  825. }
  826. func (c *fakeTSClient) DeleteDevice(ctx context.Context, deviceID string) error {
  827. c.Lock()
  828. defer c.Unlock()
  829. c.deleted = append(c.deleted, deviceID)
  830. return nil
  831. }
  832. func (c *fakeTSClient) KeyRequests() []tailscale.KeyCapabilities {
  833. c.Lock()
  834. defer c.Unlock()
  835. return c.keyRequests
  836. }
  837. func (c *fakeTSClient) Deleted() []string {
  838. c.Lock()
  839. defer c.Unlock()
  840. return c.deleted
  841. }
  842. func removeResourceReqs(sts *appsv1.StatefulSet) {
  843. if sts != nil {
  844. sts.Spec.Template.Spec.Resources = nil
  845. }
  846. }
  847. func removeTargetPortsFromSvc(svc *corev1.Service) {
  848. newPorts := make([]corev1.ServicePort, 0)
  849. for _, p := range svc.Spec.Ports {
  850. newPorts = append(newPorts, corev1.ServicePort{Protocol: p.Protocol, Port: p.Port, Name: p.Name})
  851. }
  852. svc.Spec.Ports = newPorts
  853. }
  854. func removeAuthKeyIfExistsModifier(t *testing.T) func(s *corev1.Secret) {
  855. return func(secret *corev1.Secret) {
  856. t.Helper()
  857. if len(secret.StringData["cap-95.hujson"]) != 0 {
  858. conf := &ipn.ConfigVAlpha{}
  859. if err := json.Unmarshal([]byte(secret.StringData["cap-95.hujson"]), conf); err != nil {
  860. t.Fatalf("error umarshalling 'cap-95.hujson' contents: %v", err)
  861. }
  862. conf.AuthKey = nil
  863. b, err := json.Marshal(conf)
  864. if err != nil {
  865. t.Fatalf("error marshalling 'cap-95.huson' contents: %v", err)
  866. }
  867. mak.Set(&secret.StringData, "cap-95.hujson", string(b))
  868. }
  869. if len(secret.StringData["cap-107.hujson"]) != 0 {
  870. conf := &ipn.ConfigVAlpha{}
  871. if err := json.Unmarshal([]byte(secret.StringData["cap-107.hujson"]), conf); err != nil {
  872. t.Fatalf("error umarshalling 'cap-107.hujson' contents: %v", err)
  873. }
  874. conf.AuthKey = nil
  875. b, err := json.Marshal(conf)
  876. if err != nil {
  877. t.Fatalf("error marshalling 'cap-107.huson' contents: %v", err)
  878. }
  879. mak.Set(&secret.StringData, "cap-107.hujson", string(b))
  880. }
  881. }
  882. }
  883. func (c *fakeTSClient) GetVIPService(ctx context.Context, name tailcfg.ServiceName) (*tailscale.VIPService, error) {
  884. c.Lock()
  885. defer c.Unlock()
  886. if c.vipServices == nil {
  887. return nil, tailscale.ErrResponse{Status: http.StatusNotFound}
  888. }
  889. svc, ok := c.vipServices[name]
  890. if !ok {
  891. return nil, tailscale.ErrResponse{Status: http.StatusNotFound}
  892. }
  893. return svc, nil
  894. }
  895. func (c *fakeTSClient) ListVIPServices(ctx context.Context) (*tailscale.VIPServiceList, error) {
  896. c.Lock()
  897. defer c.Unlock()
  898. if c.vipServices == nil {
  899. return nil, &tailscale.ErrResponse{Status: http.StatusNotFound}
  900. }
  901. result := &tailscale.VIPServiceList{}
  902. for _, svc := range c.vipServices {
  903. result.VIPServices = append(result.VIPServices, *svc)
  904. }
  905. return result, nil
  906. }
  907. func (c *fakeTSClient) CreateOrUpdateVIPService(ctx context.Context, svc *tailscale.VIPService) error {
  908. c.Lock()
  909. defer c.Unlock()
  910. if c.vipServices == nil {
  911. c.vipServices = make(map[tailcfg.ServiceName]*tailscale.VIPService)
  912. }
  913. if svc.Addrs == nil {
  914. svc.Addrs = []string{vipTestIP}
  915. }
  916. c.vipServices[svc.Name] = svc
  917. return nil
  918. }
  919. func (c *fakeTSClient) DeleteVIPService(ctx context.Context, name tailcfg.ServiceName) error {
  920. c.Lock()
  921. defer c.Unlock()
  922. if c.vipServices != nil {
  923. delete(c.vipServices, name)
  924. }
  925. return nil
  926. }
  927. type fakeLocalClient struct {
  928. status *ipnstate.Status
  929. }
  930. func (f *fakeLocalClient) StatusWithoutPeers(ctx context.Context) (*ipnstate.Status, error) {
  931. if f.status == nil {
  932. return &ipnstate.Status{
  933. Self: &ipnstate.PeerStatus{
  934. DNSName: "test-node.test.ts.net.",
  935. },
  936. }, nil
  937. }
  938. return f.status, nil
  939. }