operator_test.go 65 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115
  1. // Copyright (c) Tailscale Inc & contributors
  2. // SPDX-License-Identifier: BSD-3-Clause
  3. //go:build !plan9
  4. package main
  5. import (
  6. "context"
  7. "encoding/json"
  8. "fmt"
  9. "testing"
  10. "time"
  11. "github.com/google/go-cmp/cmp"
  12. "go.uber.org/zap"
  13. appsv1 "k8s.io/api/apps/v1"
  14. corev1 "k8s.io/api/core/v1"
  15. networkingv1 "k8s.io/api/networking/v1"
  16. apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
  17. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  18. "k8s.io/apimachinery/pkg/types"
  19. "k8s.io/client-go/tools/record"
  20. "sigs.k8s.io/controller-runtime/pkg/client"
  21. "sigs.k8s.io/controller-runtime/pkg/client/fake"
  22. "sigs.k8s.io/controller-runtime/pkg/reconcile"
  23. "tailscale.com/k8s-operator/apis/v1alpha1"
  24. tsapi "tailscale.com/k8s-operator/apis/v1alpha1"
  25. "tailscale.com/kube/kubetypes"
  26. "tailscale.com/net/dns/resolvconffile"
  27. "tailscale.com/tstest"
  28. "tailscale.com/tstime"
  29. "tailscale.com/types/ptr"
  30. "tailscale.com/util/dnsname"
  31. "tailscale.com/util/mak"
  32. )
  33. func TestLoadBalancerClass(t *testing.T) {
  34. fc := fake.NewFakeClient()
  35. ft := &fakeTSClient{}
  36. zl := zap.Must(zap.NewDevelopment())
  37. clock := tstest.NewClock(tstest.ClockOpts{})
  38. sr := &ServiceReconciler{
  39. Client: fc,
  40. ssr: &tailscaleSTSReconciler{
  41. Client: fc,
  42. tsClient: ft,
  43. defaultTags: []string{"tag:k8s"},
  44. operatorNamespace: "operator-ns",
  45. proxyImage: "tailscale/tailscale",
  46. },
  47. logger: zl.Sugar(),
  48. clock: clock,
  49. recorder: record.NewFakeRecorder(100),
  50. }
  51. // Create a service that we should manage, but start with a miconfiguration
  52. // in the annotations.
  53. mustCreate(t, fc, &corev1.Service{
  54. ObjectMeta: metav1.ObjectMeta{
  55. Name: "test",
  56. Namespace: "default",
  57. // The apiserver is supposed to set the UID, but the fake client
  58. // doesn't. So, set it explicitly because other code later depends
  59. // on it being set.
  60. UID: types.UID("1234-UID"),
  61. Annotations: map[string]string{
  62. AnnotationTailnetTargetFQDN: "invalid.example.com",
  63. },
  64. },
  65. Spec: corev1.ServiceSpec{
  66. ClusterIP: "10.20.30.40",
  67. Type: corev1.ServiceTypeLoadBalancer,
  68. LoadBalancerClass: ptr.To("tailscale"),
  69. },
  70. })
  71. expectReconciled(t, sr, "default", "test")
  72. // The expected value of .status.conditions[0].LastTransitionTime until the
  73. // proxy becomes ready.
  74. t0 := conditionTime(clock)
  75. // Should have an error about invalid config.
  76. want := &corev1.Service{
  77. ObjectMeta: metav1.ObjectMeta{
  78. Name: "test",
  79. Namespace: "default",
  80. UID: types.UID("1234-UID"),
  81. Annotations: map[string]string{
  82. AnnotationTailnetTargetFQDN: "invalid.example.com",
  83. },
  84. },
  85. Spec: corev1.ServiceSpec{
  86. ClusterIP: "10.20.30.40",
  87. Type: corev1.ServiceTypeLoadBalancer,
  88. LoadBalancerClass: ptr.To("tailscale"),
  89. },
  90. Status: corev1.ServiceStatus{
  91. Conditions: []metav1.Condition{{
  92. Type: string(tsapi.ProxyReady),
  93. Status: metav1.ConditionFalse,
  94. LastTransitionTime: t0,
  95. Reason: reasonProxyInvalid,
  96. Message: `unable to provision proxy resources: invalid Service: invalid value of annotation tailscale.com/tailnet-fqdn: "invalid.example.com" does not appear to be a valid MagicDNS name`,
  97. }},
  98. },
  99. }
  100. expectEqual(t, fc, want)
  101. // Delete the misconfiguration so the proxy starts getting created on the
  102. // next reconcile.
  103. mustUpdate(t, fc, "default", "test", func(s *corev1.Service) {
  104. s.ObjectMeta.Annotations = nil
  105. })
  106. clock.Advance(time.Second)
  107. expectReconciled(t, sr, "default", "test")
  108. fullName, shortName := findGenName(t, fc, "default", "test", "svc")
  109. opts := configOpts{
  110. replicas: ptr.To[int32](1),
  111. stsName: shortName,
  112. secretName: fullName,
  113. namespace: "default",
  114. parentType: "svc",
  115. hostname: "default-test",
  116. clusterTargetIP: "10.20.30.40",
  117. app: kubetypes.AppIngressProxy,
  118. }
  119. expectEqual(t, fc, expectedSecret(t, fc, opts))
  120. expectEqual(t, fc, expectedHeadlessService(shortName, "svc"))
  121. expectEqual(t, fc, expectedSTS(t, fc, opts), removeResourceReqs)
  122. want.Annotations = nil
  123. want.ObjectMeta.Finalizers = []string{"tailscale.com/finalizer"}
  124. want.Status = corev1.ServiceStatus{
  125. Conditions: []metav1.Condition{{
  126. Type: string(tsapi.ProxyReady),
  127. Status: metav1.ConditionFalse,
  128. LastTransitionTime: t0, // Status is still false, no update to transition time
  129. Reason: reasonProxyPending,
  130. Message: "no Tailscale hostname known yet, waiting for proxy pod to finish auth",
  131. }},
  132. }
  133. expectEqual(t, fc, want)
  134. // Normally the Tailscale proxy pod would come up here and write its info
  135. // into the secret. Simulate that, then verify reconcile again and verify
  136. // that we get to the end.
  137. mustUpdate(t, fc, "operator-ns", fullName, func(s *corev1.Secret) {
  138. if s.Data == nil {
  139. s.Data = map[string][]byte{}
  140. }
  141. s.Data["device_id"] = []byte("ts-id-1234")
  142. s.Data["device_fqdn"] = []byte("tailscale.device.name.")
  143. s.Data["device_ips"] = []byte(`["100.99.98.97", "2c0a:8083:94d4:2012:3165:34a5:3616:5fdf"]`)
  144. })
  145. clock.Advance(time.Second)
  146. expectReconciled(t, sr, "default", "test")
  147. want.Status.Conditions = proxyCreatedCondition(clock)
  148. want.Status.LoadBalancer = corev1.LoadBalancerStatus{
  149. Ingress: []corev1.LoadBalancerIngress{
  150. {
  151. Hostname: "tailscale.device.name",
  152. },
  153. {
  154. IP: "100.99.98.97",
  155. },
  156. },
  157. }
  158. // Perform an additional reconciliation loop here to ensure resources don't change through side effects. Mainly
  159. // to prevent infinite reconciliation
  160. expectReconciled(t, sr, "default", "test")
  161. expectEqual(t, fc, want)
  162. // Turn the service back into a ClusterIP service, which should make the
  163. // operator clean up.
  164. mustUpdate(t, fc, "default", "test", func(s *corev1.Service) {
  165. s.Spec.Type = corev1.ServiceTypeClusterIP
  166. s.Spec.LoadBalancerClass = nil
  167. })
  168. mustUpdateStatus(t, fc, "default", "test", func(s *corev1.Service) {
  169. // Fake client doesn't automatically delete the LoadBalancer status when
  170. // changing away from the LoadBalancer type, we have to do
  171. // controller-manager's work by hand.
  172. s.Status = corev1.ServiceStatus{}
  173. })
  174. // synchronous StatefulSet deletion triggers a requeue. But, the StatefulSet
  175. // didn't create any child resources since this is all faked, so the
  176. // deletion goes through immediately.
  177. expectReconciled(t, sr, "default", "test")
  178. expectMissing[appsv1.StatefulSet](t, fc, "operator-ns", shortName)
  179. // The deletion triggers another reconcile, to finish the cleanup.
  180. expectReconciled(t, sr, "default", "test")
  181. expectMissing[appsv1.StatefulSet](t, fc, "operator-ns", shortName)
  182. expectMissing[corev1.Service](t, fc, "operator-ns", shortName)
  183. expectMissing[corev1.Secret](t, fc, "operator-ns", fullName)
  184. // Note that the Tailscale-specific condition status should be gone now.
  185. want = &corev1.Service{
  186. ObjectMeta: metav1.ObjectMeta{
  187. Name: "test",
  188. Namespace: "default",
  189. UID: types.UID("1234-UID"),
  190. },
  191. Spec: corev1.ServiceSpec{
  192. ClusterIP: "10.20.30.40",
  193. Type: corev1.ServiceTypeClusterIP,
  194. },
  195. }
  196. expectEqual(t, fc, want)
  197. }
  198. func TestTailnetTargetFQDNAnnotation(t *testing.T) {
  199. fc := fake.NewFakeClient()
  200. ft := &fakeTSClient{}
  201. zl := zap.Must(zap.NewDevelopment())
  202. tailnetTargetFQDN := "foo.bar.ts.net."
  203. clock := tstest.NewClock(tstest.ClockOpts{})
  204. sr := &ServiceReconciler{
  205. Client: fc,
  206. ssr: &tailscaleSTSReconciler{
  207. Client: fc,
  208. tsClient: ft,
  209. defaultTags: []string{"tag:k8s"},
  210. operatorNamespace: "operator-ns",
  211. proxyImage: "tailscale/tailscale",
  212. },
  213. logger: zl.Sugar(),
  214. clock: clock,
  215. }
  216. // Create a service that we should manage, and check that the initial round
  217. // of objects looks right.
  218. mustCreate(t, fc, &corev1.Service{
  219. ObjectMeta: metav1.ObjectMeta{
  220. Name: "test",
  221. Namespace: "default",
  222. // The apiserver is supposed to set the UID, but the fake client
  223. // doesn't. So, set it explicitly because other code later depends
  224. // on it being set.
  225. UID: types.UID("1234-UID"),
  226. Annotations: map[string]string{
  227. AnnotationTailnetTargetFQDN: tailnetTargetFQDN,
  228. },
  229. },
  230. Spec: corev1.ServiceSpec{
  231. Type: corev1.ServiceTypeClusterIP,
  232. Selector: map[string]string{
  233. "foo": "bar",
  234. },
  235. },
  236. })
  237. expectReconciled(t, sr, "default", "test")
  238. fullName, shortName := findGenName(t, fc, "default", "test", "svc")
  239. o := configOpts{
  240. replicas: ptr.To[int32](1),
  241. stsName: shortName,
  242. secretName: fullName,
  243. namespace: "default",
  244. parentType: "svc",
  245. tailnetTargetFQDN: tailnetTargetFQDN,
  246. hostname: "default-test",
  247. app: kubetypes.AppEgressProxy,
  248. }
  249. expectEqual(t, fc, expectedSecret(t, fc, o))
  250. expectEqual(t, fc, expectedHeadlessService(shortName, "svc"))
  251. expectEqual(t, fc, expectedSTS(t, fc, o), removeResourceReqs)
  252. want := &corev1.Service{
  253. ObjectMeta: metav1.ObjectMeta{
  254. Name: "test",
  255. Namespace: "default",
  256. Finalizers: []string{"tailscale.com/finalizer"},
  257. UID: types.UID("1234-UID"),
  258. Annotations: map[string]string{
  259. AnnotationTailnetTargetFQDN: tailnetTargetFQDN,
  260. },
  261. },
  262. Spec: corev1.ServiceSpec{
  263. ExternalName: fmt.Sprintf("%s.operator-ns.svc.cluster.local", shortName),
  264. Type: corev1.ServiceTypeExternalName,
  265. Selector: nil,
  266. },
  267. Status: corev1.ServiceStatus{
  268. Conditions: proxyCreatedCondition(clock),
  269. },
  270. }
  271. expectEqual(t, fc, want)
  272. expectEqual(t, fc, expectedSecret(t, fc, o))
  273. expectEqual(t, fc, expectedHeadlessService(shortName, "svc"))
  274. expectEqual(t, fc, expectedSTS(t, fc, o), removeResourceReqs)
  275. // Change the tailscale-target-fqdn annotation which should update the
  276. // StatefulSet
  277. tailnetTargetFQDN = "bar.baz.ts.net"
  278. mustUpdate(t, fc, "default", "test", func(s *corev1.Service) {
  279. s.ObjectMeta.Annotations = map[string]string{
  280. AnnotationTailnetTargetFQDN: tailnetTargetFQDN,
  281. }
  282. })
  283. // Remove the tailscale-target-fqdn annotation which should make the
  284. // operator clean up
  285. mustUpdate(t, fc, "default", "test", func(s *corev1.Service) {
  286. s.ObjectMeta.Annotations = map[string]string{}
  287. })
  288. expectReconciled(t, sr, "default", "test")
  289. // // synchronous StatefulSet deletion triggers a requeue. But, the StatefulSet
  290. // // didn't create any child resources since this is all faked, so the
  291. // // deletion goes through immediately.
  292. expectReconciled(t, sr, "default", "test")
  293. expectMissing[appsv1.StatefulSet](t, fc, "operator-ns", shortName)
  294. // // The deletion triggers another reconcile, to finish the cleanup.
  295. expectReconciled(t, sr, "default", "test")
  296. expectMissing[appsv1.StatefulSet](t, fc, "operator-ns", shortName)
  297. expectMissing[corev1.Service](t, fc, "operator-ns", shortName)
  298. expectMissing[corev1.Secret](t, fc, "operator-ns", fullName)
  299. }
  300. func TestTailnetTargetIPAnnotation(t *testing.T) {
  301. fc := fake.NewFakeClient()
  302. ft := &fakeTSClient{}
  303. zl := zap.Must(zap.NewDevelopment())
  304. tailnetTargetIP := "100.66.66.66"
  305. clock := tstest.NewClock(tstest.ClockOpts{})
  306. sr := &ServiceReconciler{
  307. Client: fc,
  308. ssr: &tailscaleSTSReconciler{
  309. Client: fc,
  310. tsClient: ft,
  311. defaultTags: []string{"tag:k8s"},
  312. operatorNamespace: "operator-ns",
  313. proxyImage: "tailscale/tailscale",
  314. },
  315. logger: zl.Sugar(),
  316. clock: clock,
  317. }
  318. // Create a service that we should manage, and check that the initial round
  319. // of objects looks right.
  320. mustCreate(t, fc, &corev1.Service{
  321. ObjectMeta: metav1.ObjectMeta{
  322. Name: "test",
  323. Namespace: "default",
  324. // The apiserver is supposed to set the UID, but the fake client
  325. // doesn't. So, set it explicitly because other code later depends
  326. // on it being set.
  327. UID: types.UID("1234-UID"),
  328. Annotations: map[string]string{
  329. AnnotationTailnetTargetIP: tailnetTargetIP,
  330. },
  331. },
  332. Spec: corev1.ServiceSpec{
  333. Type: corev1.ServiceTypeClusterIP,
  334. Selector: map[string]string{
  335. "foo": "bar",
  336. },
  337. },
  338. })
  339. expectReconciled(t, sr, "default", "test")
  340. fullName, shortName := findGenName(t, fc, "default", "test", "svc")
  341. o := configOpts{
  342. replicas: ptr.To[int32](1),
  343. stsName: shortName,
  344. secretName: fullName,
  345. namespace: "default",
  346. parentType: "svc",
  347. tailnetTargetIP: tailnetTargetIP,
  348. hostname: "default-test",
  349. app: kubetypes.AppEgressProxy,
  350. }
  351. expectEqual(t, fc, expectedSecret(t, fc, o))
  352. expectEqual(t, fc, expectedHeadlessService(shortName, "svc"))
  353. expectEqual(t, fc, expectedSTS(t, fc, o), removeResourceReqs)
  354. want := &corev1.Service{
  355. ObjectMeta: metav1.ObjectMeta{
  356. Name: "test",
  357. Namespace: "default",
  358. Finalizers: []string{"tailscale.com/finalizer"},
  359. UID: types.UID("1234-UID"),
  360. Annotations: map[string]string{
  361. AnnotationTailnetTargetIP: tailnetTargetIP,
  362. },
  363. },
  364. Spec: corev1.ServiceSpec{
  365. ExternalName: fmt.Sprintf("%s.operator-ns.svc.cluster.local", shortName),
  366. Type: corev1.ServiceTypeExternalName,
  367. Selector: nil,
  368. },
  369. Status: corev1.ServiceStatus{
  370. Conditions: proxyCreatedCondition(clock),
  371. },
  372. }
  373. expectEqual(t, fc, want)
  374. expectEqual(t, fc, expectedSecret(t, fc, o))
  375. expectEqual(t, fc, expectedHeadlessService(shortName, "svc"))
  376. expectEqual(t, fc, expectedSTS(t, fc, o), removeResourceReqs)
  377. // Change the tailscale-target-ip annotation which should update the
  378. // StatefulSet
  379. tailnetTargetIP = "100.77.77.77"
  380. mustUpdate(t, fc, "default", "test", func(s *corev1.Service) {
  381. s.ObjectMeta.Annotations = map[string]string{
  382. AnnotationTailnetTargetIP: tailnetTargetIP,
  383. }
  384. })
  385. // Remove the tailscale-target-ip annotation which should make the
  386. // operator clean up
  387. mustUpdate(t, fc, "default", "test", func(s *corev1.Service) {
  388. s.ObjectMeta.Annotations = map[string]string{}
  389. })
  390. expectReconciled(t, sr, "default", "test")
  391. // synchronous StatefulSet deletion triggers a requeue. But, the StatefulSet
  392. // didn't create any child resources since this is all faked, so the
  393. // deletion goes through immediately.
  394. expectReconciled(t, sr, "default", "test")
  395. expectMissing[appsv1.StatefulSet](t, fc, "operator-ns", shortName)
  396. // The deletion triggers another reconcile, to finish the cleanup.
  397. expectReconciled(t, sr, "default", "test")
  398. expectMissing[appsv1.StatefulSet](t, fc, "operator-ns", shortName)
  399. expectMissing[corev1.Service](t, fc, "operator-ns", shortName)
  400. expectMissing[corev1.Secret](t, fc, "operator-ns", fullName)
  401. }
  402. func TestTailnetTargetIPAnnotation_IPCouldNotBeParsed(t *testing.T) {
  403. fc := fake.NewFakeClient()
  404. ft := &fakeTSClient{}
  405. zl := zap.Must(zap.NewDevelopment())
  406. clock := tstest.NewClock(tstest.ClockOpts{})
  407. sr := &ServiceReconciler{
  408. Client: fc,
  409. ssr: &tailscaleSTSReconciler{
  410. Client: fc,
  411. tsClient: ft,
  412. defaultTags: []string{"tag:k8s"},
  413. operatorNamespace: "operator-ns",
  414. proxyImage: "tailscale/tailscale",
  415. },
  416. logger: zl.Sugar(),
  417. clock: clock,
  418. recorder: record.NewFakeRecorder(100),
  419. }
  420. tailnetTargetIP := "invalid-ip"
  421. mustCreate(t, fc, &corev1.Service{
  422. ObjectMeta: metav1.ObjectMeta{
  423. Name: "test",
  424. Namespace: "default",
  425. UID: types.UID("1234-UID"),
  426. Annotations: map[string]string{
  427. AnnotationTailnetTargetIP: tailnetTargetIP,
  428. },
  429. },
  430. Spec: corev1.ServiceSpec{
  431. ClusterIP: "10.20.30.40",
  432. Type: corev1.ServiceTypeLoadBalancer,
  433. LoadBalancerClass: ptr.To("tailscale"),
  434. },
  435. })
  436. expectReconciled(t, sr, "default", "test")
  437. t0 := conditionTime(clock)
  438. want := &corev1.Service{
  439. ObjectMeta: metav1.ObjectMeta{
  440. Name: "test",
  441. Namespace: "default",
  442. UID: types.UID("1234-UID"),
  443. Annotations: map[string]string{
  444. AnnotationTailnetTargetIP: tailnetTargetIP,
  445. },
  446. },
  447. Spec: corev1.ServiceSpec{
  448. ClusterIP: "10.20.30.40",
  449. Type: corev1.ServiceTypeLoadBalancer,
  450. LoadBalancerClass: ptr.To("tailscale"),
  451. },
  452. Status: corev1.ServiceStatus{
  453. Conditions: []metav1.Condition{{
  454. Type: string(tsapi.ProxyReady),
  455. Status: metav1.ConditionFalse,
  456. LastTransitionTime: t0,
  457. Reason: reasonProxyInvalid,
  458. Message: `unable to provision proxy resources: invalid Service: invalid value of annotation tailscale.com/tailnet-ip: "invalid-ip" could not be parsed as a valid IP Address, error: ParseAddr("invalid-ip"): unable to parse IP`,
  459. }},
  460. },
  461. }
  462. expectEqual(t, fc, want)
  463. }
  464. func TestTailnetTargetIPAnnotation_InvalidIP(t *testing.T) {
  465. fc := fake.NewFakeClient()
  466. ft := &fakeTSClient{}
  467. zl := zap.Must(zap.NewDevelopment())
  468. clock := tstest.NewClock(tstest.ClockOpts{})
  469. sr := &ServiceReconciler{
  470. Client: fc,
  471. ssr: &tailscaleSTSReconciler{
  472. Client: fc,
  473. tsClient: ft,
  474. defaultTags: []string{"tag:k8s"},
  475. operatorNamespace: "operator-ns",
  476. proxyImage: "tailscale/tailscale",
  477. },
  478. logger: zl.Sugar(),
  479. clock: clock,
  480. recorder: record.NewFakeRecorder(100),
  481. }
  482. tailnetTargetIP := "999.999.999.999"
  483. mustCreate(t, fc, &corev1.Service{
  484. ObjectMeta: metav1.ObjectMeta{
  485. Name: "test",
  486. Namespace: "default",
  487. UID: types.UID("1234-UID"),
  488. Annotations: map[string]string{
  489. AnnotationTailnetTargetIP: tailnetTargetIP,
  490. },
  491. },
  492. Spec: corev1.ServiceSpec{
  493. ClusterIP: "10.20.30.40",
  494. Type: corev1.ServiceTypeLoadBalancer,
  495. LoadBalancerClass: ptr.To("tailscale"),
  496. },
  497. })
  498. expectReconciled(t, sr, "default", "test")
  499. t0 := conditionTime(clock)
  500. want := &corev1.Service{
  501. ObjectMeta: metav1.ObjectMeta{
  502. Name: "test",
  503. Namespace: "default",
  504. UID: types.UID("1234-UID"),
  505. Annotations: map[string]string{
  506. AnnotationTailnetTargetIP: tailnetTargetIP,
  507. },
  508. },
  509. Spec: corev1.ServiceSpec{
  510. ClusterIP: "10.20.30.40",
  511. Type: corev1.ServiceTypeLoadBalancer,
  512. LoadBalancerClass: ptr.To("tailscale"),
  513. },
  514. Status: corev1.ServiceStatus{
  515. Conditions: []metav1.Condition{{
  516. Type: string(tsapi.ProxyReady),
  517. Status: metav1.ConditionFalse,
  518. LastTransitionTime: t0,
  519. Reason: reasonProxyInvalid,
  520. Message: `unable to provision proxy resources: invalid Service: invalid value of annotation tailscale.com/tailnet-ip: "999.999.999.999" could not be parsed as a valid IP Address, error: ParseAddr("999.999.999.999"): IPv4 field has value >255`,
  521. }},
  522. },
  523. }
  524. expectEqual(t, fc, want)
  525. }
  526. func TestAnnotations(t *testing.T) {
  527. fc := fake.NewFakeClient()
  528. ft := &fakeTSClient{}
  529. zl := zap.Must(zap.NewDevelopment())
  530. clock := tstest.NewClock(tstest.ClockOpts{})
  531. sr := &ServiceReconciler{
  532. Client: fc,
  533. ssr: &tailscaleSTSReconciler{
  534. Client: fc,
  535. tsClient: ft,
  536. defaultTags: []string{"tag:k8s"},
  537. operatorNamespace: "operator-ns",
  538. proxyImage: "tailscale/tailscale",
  539. },
  540. logger: zl.Sugar(),
  541. clock: clock,
  542. }
  543. // Create a service that we should manage, and check that the initial round
  544. // of objects looks right.
  545. mustCreate(t, fc, &corev1.Service{
  546. ObjectMeta: metav1.ObjectMeta{
  547. Name: "test",
  548. Namespace: "default",
  549. // The apiserver is supposed to set the UID, but the fake client
  550. // doesn't. So, set it explicitly because other code later depends
  551. // on it being set.
  552. UID: types.UID("1234-UID"),
  553. Annotations: map[string]string{
  554. "tailscale.com/expose": "true",
  555. },
  556. },
  557. Spec: corev1.ServiceSpec{
  558. ClusterIP: "10.20.30.40",
  559. Type: corev1.ServiceTypeClusterIP,
  560. },
  561. })
  562. expectReconciled(t, sr, "default", "test")
  563. fullName, shortName := findGenName(t, fc, "default", "test", "svc")
  564. o := configOpts{
  565. replicas: ptr.To[int32](1),
  566. stsName: shortName,
  567. secretName: fullName,
  568. namespace: "default",
  569. parentType: "svc",
  570. hostname: "default-test",
  571. clusterTargetIP: "10.20.30.40",
  572. app: kubetypes.AppIngressProxy,
  573. }
  574. expectEqual(t, fc, expectedSecret(t, fc, o))
  575. expectEqual(t, fc, expectedHeadlessService(shortName, "svc"))
  576. expectEqual(t, fc, expectedSTS(t, fc, o), removeResourceReqs)
  577. want := &corev1.Service{
  578. ObjectMeta: metav1.ObjectMeta{
  579. Name: "test",
  580. Namespace: "default",
  581. Finalizers: []string{"tailscale.com/finalizer"},
  582. UID: types.UID("1234-UID"),
  583. Annotations: map[string]string{
  584. "tailscale.com/expose": "true",
  585. },
  586. },
  587. Spec: corev1.ServiceSpec{
  588. ClusterIP: "10.20.30.40",
  589. Type: corev1.ServiceTypeClusterIP,
  590. },
  591. Status: corev1.ServiceStatus{
  592. Conditions: proxyCreatedCondition(clock),
  593. },
  594. }
  595. expectEqual(t, fc, want)
  596. // Turn the service back into a ClusterIP service, which should make the
  597. // operator clean up.
  598. mustUpdate(t, fc, "default", "test", func(s *corev1.Service) {
  599. delete(s.ObjectMeta.Annotations, "tailscale.com/expose")
  600. })
  601. // synchronous StatefulSet deletion triggers a requeue. But, the StatefulSet
  602. // didn't create any child resources since this is all faked, so the
  603. // deletion goes through immediately.
  604. expectReconciled(t, sr, "default", "test")
  605. expectMissing[appsv1.StatefulSet](t, fc, "operator-ns", shortName)
  606. // Second time around, the rest of cleanup happens.
  607. expectReconciled(t, sr, "default", "test")
  608. expectMissing[appsv1.StatefulSet](t, fc, "operator-ns", shortName)
  609. expectMissing[corev1.Service](t, fc, "operator-ns", shortName)
  610. expectMissing[corev1.Secret](t, fc, "operator-ns", fullName)
  611. want = &corev1.Service{
  612. ObjectMeta: metav1.ObjectMeta{
  613. Name: "test",
  614. Namespace: "default",
  615. UID: types.UID("1234-UID"),
  616. },
  617. Spec: corev1.ServiceSpec{
  618. ClusterIP: "10.20.30.40",
  619. Type: corev1.ServiceTypeClusterIP,
  620. },
  621. }
  622. expectEqual(t, fc, want)
  623. }
  624. func TestAnnotationIntoLB(t *testing.T) {
  625. fc := fake.NewFakeClient()
  626. ft := &fakeTSClient{}
  627. zl := zap.Must(zap.NewDevelopment())
  628. clock := tstest.NewClock(tstest.ClockOpts{})
  629. sr := &ServiceReconciler{
  630. Client: fc,
  631. ssr: &tailscaleSTSReconciler{
  632. Client: fc,
  633. tsClient: ft,
  634. defaultTags: []string{"tag:k8s"},
  635. operatorNamespace: "operator-ns",
  636. proxyImage: "tailscale/tailscale",
  637. },
  638. logger: zl.Sugar(),
  639. clock: clock,
  640. }
  641. // Create a service that we should manage, and check that the initial round
  642. // of objects looks right.
  643. mustCreate(t, fc, &corev1.Service{
  644. ObjectMeta: metav1.ObjectMeta{
  645. Name: "test",
  646. Namespace: "default",
  647. // The apiserver is supposed to set the UID, but the fake client
  648. // doesn't. So, set it explicitly because other code later depends
  649. // on it being set.
  650. UID: types.UID("1234-UID"),
  651. Annotations: map[string]string{
  652. "tailscale.com/expose": "true",
  653. },
  654. },
  655. Spec: corev1.ServiceSpec{
  656. ClusterIP: "10.20.30.40",
  657. Type: corev1.ServiceTypeClusterIP,
  658. },
  659. })
  660. expectReconciled(t, sr, "default", "test")
  661. fullName, shortName := findGenName(t, fc, "default", "test", "svc")
  662. o := configOpts{
  663. replicas: ptr.To[int32](1),
  664. stsName: shortName,
  665. secretName: fullName,
  666. namespace: "default",
  667. parentType: "svc",
  668. hostname: "default-test",
  669. clusterTargetIP: "10.20.30.40",
  670. app: kubetypes.AppIngressProxy,
  671. }
  672. expectEqual(t, fc, expectedSecret(t, fc, o))
  673. expectEqual(t, fc, expectedHeadlessService(shortName, "svc"))
  674. expectEqual(t, fc, expectedSTS(t, fc, o), removeResourceReqs)
  675. // Normally the Tailscale proxy pod would come up here and write its info
  676. // into the secret. Simulate that, since it would have normally happened at
  677. // this point and the LoadBalancer is going to expect this.
  678. mustUpdate(t, fc, "operator-ns", fullName, func(s *corev1.Secret) {
  679. if s.Data == nil {
  680. s.Data = map[string][]byte{}
  681. }
  682. s.Data["device_id"] = []byte("ts-id-1234")
  683. s.Data["device_fqdn"] = []byte("tailscale.device.name.")
  684. s.Data["device_ips"] = []byte(`["100.99.98.97", "2c0a:8083:94d4:2012:3165:34a5:3616:5fdf"]`)
  685. })
  686. expectReconciled(t, sr, "default", "test")
  687. want := &corev1.Service{
  688. ObjectMeta: metav1.ObjectMeta{
  689. Name: "test",
  690. Namespace: "default",
  691. Finalizers: []string{"tailscale.com/finalizer"},
  692. UID: types.UID("1234-UID"),
  693. Annotations: map[string]string{
  694. "tailscale.com/expose": "true",
  695. },
  696. },
  697. Spec: corev1.ServiceSpec{
  698. ClusterIP: "10.20.30.40",
  699. Type: corev1.ServiceTypeClusterIP,
  700. },
  701. Status: corev1.ServiceStatus{
  702. Conditions: proxyCreatedCondition(clock),
  703. },
  704. }
  705. expectEqual(t, fc, want)
  706. // Remove Tailscale's annotation, and at the same time convert the service
  707. // into a tailscale LoadBalancer.
  708. mustUpdate(t, fc, "default", "test", func(s *corev1.Service) {
  709. delete(s.ObjectMeta.Annotations, "tailscale.com/expose")
  710. s.Spec.Type = corev1.ServiceTypeLoadBalancer
  711. s.Spec.LoadBalancerClass = ptr.To("tailscale")
  712. })
  713. expectReconciled(t, sr, "default", "test")
  714. // None of the proxy machinery should have changed...
  715. expectEqual(t, fc, expectedHeadlessService(shortName, "svc"))
  716. expectEqual(t, fc, expectedSTS(t, fc, o), removeResourceReqs)
  717. // ... but the service should have a LoadBalancer status.
  718. want = &corev1.Service{
  719. ObjectMeta: metav1.ObjectMeta{
  720. Name: "test",
  721. Namespace: "default",
  722. Finalizers: []string{"tailscale.com/finalizer"},
  723. UID: types.UID("1234-UID"),
  724. },
  725. Spec: corev1.ServiceSpec{
  726. ClusterIP: "10.20.30.40",
  727. Type: corev1.ServiceTypeLoadBalancer,
  728. LoadBalancerClass: ptr.To("tailscale"),
  729. },
  730. Status: corev1.ServiceStatus{
  731. LoadBalancer: corev1.LoadBalancerStatus{
  732. Ingress: []corev1.LoadBalancerIngress{
  733. {
  734. Hostname: "tailscale.device.name",
  735. },
  736. {
  737. IP: "100.99.98.97",
  738. },
  739. },
  740. },
  741. Conditions: proxyCreatedCondition(clock),
  742. },
  743. }
  744. expectEqual(t, fc, want)
  745. }
  746. func TestLBIntoAnnotation(t *testing.T) {
  747. fc := fake.NewFakeClient()
  748. ft := &fakeTSClient{}
  749. zl := zap.Must(zap.NewDevelopment())
  750. clock := tstest.NewClock(tstest.ClockOpts{})
  751. sr := &ServiceReconciler{
  752. Client: fc,
  753. ssr: &tailscaleSTSReconciler{
  754. Client: fc,
  755. tsClient: ft,
  756. defaultTags: []string{"tag:k8s"},
  757. operatorNamespace: "operator-ns",
  758. proxyImage: "tailscale/tailscale",
  759. },
  760. logger: zl.Sugar(),
  761. clock: clock,
  762. }
  763. // Create a service that we should manage, and check that the initial round
  764. // of objects looks right.
  765. mustCreate(t, fc, &corev1.Service{
  766. ObjectMeta: metav1.ObjectMeta{
  767. Name: "test",
  768. Namespace: "default",
  769. // The apiserver is supposed to set the UID, but the fake client
  770. // doesn't. So, set it explicitly because other code later depends
  771. // on it being set.
  772. UID: types.UID("1234-UID"),
  773. },
  774. Spec: corev1.ServiceSpec{
  775. ClusterIP: "10.20.30.40",
  776. Type: corev1.ServiceTypeLoadBalancer,
  777. LoadBalancerClass: ptr.To("tailscale"),
  778. },
  779. })
  780. expectReconciled(t, sr, "default", "test")
  781. fullName, shortName := findGenName(t, fc, "default", "test", "svc")
  782. o := configOpts{
  783. replicas: ptr.To[int32](1),
  784. stsName: shortName,
  785. secretName: fullName,
  786. namespace: "default",
  787. parentType: "svc",
  788. hostname: "default-test",
  789. clusterTargetIP: "10.20.30.40",
  790. app: kubetypes.AppIngressProxy,
  791. }
  792. expectEqual(t, fc, expectedSecret(t, fc, o))
  793. expectEqual(t, fc, expectedHeadlessService(shortName, "svc"))
  794. expectEqual(t, fc, expectedSTS(t, fc, o), removeResourceReqs)
  795. // Normally the Tailscale proxy pod would come up here and write its info
  796. // into the secret. Simulate that, then verify reconcile again and verify
  797. // that we get to the end.
  798. mustUpdate(t, fc, "operator-ns", fullName, func(s *corev1.Secret) {
  799. if s.Data == nil {
  800. s.Data = map[string][]byte{}
  801. }
  802. s.Data["device_id"] = []byte("ts-id-1234")
  803. s.Data["device_fqdn"] = []byte("tailscale.device.name.")
  804. s.Data["device_ips"] = []byte(`["100.99.98.97", "2c0a:8083:94d4:2012:3165:34a5:3616:5fdf"]`)
  805. })
  806. expectReconciled(t, sr, "default", "test")
  807. want := &corev1.Service{
  808. ObjectMeta: metav1.ObjectMeta{
  809. Name: "test",
  810. Namespace: "default",
  811. Finalizers: []string{"tailscale.com/finalizer"},
  812. UID: types.UID("1234-UID"),
  813. },
  814. Spec: corev1.ServiceSpec{
  815. ClusterIP: "10.20.30.40",
  816. Type: corev1.ServiceTypeLoadBalancer,
  817. LoadBalancerClass: ptr.To("tailscale"),
  818. },
  819. Status: corev1.ServiceStatus{
  820. LoadBalancer: corev1.LoadBalancerStatus{
  821. Ingress: []corev1.LoadBalancerIngress{
  822. {
  823. Hostname: "tailscale.device.name",
  824. },
  825. {
  826. IP: "100.99.98.97",
  827. },
  828. },
  829. },
  830. Conditions: proxyCreatedCondition(clock),
  831. },
  832. }
  833. expectEqual(t, fc, want)
  834. // Turn the service back into a ClusterIP service, but also add the
  835. // tailscale annotation.
  836. mustUpdate(t, fc, "default", "test", func(s *corev1.Service) {
  837. s.ObjectMeta.Annotations = map[string]string{
  838. "tailscale.com/expose": "true",
  839. }
  840. s.Spec.Type = corev1.ServiceTypeClusterIP
  841. s.Spec.LoadBalancerClass = nil
  842. })
  843. mustUpdateStatus(t, fc, "default", "test", func(s *corev1.Service) {
  844. // Fake client doesn't automatically delete the LoadBalancer status when
  845. // changing away from the LoadBalancer type, we have to do
  846. // controller-manager's work by hand.
  847. s.Status = corev1.ServiceStatus{}
  848. })
  849. expectReconciled(t, sr, "default", "test")
  850. expectEqual(t, fc, expectedHeadlessService(shortName, "svc"))
  851. expectEqual(t, fc, expectedSTS(t, fc, o), removeResourceReqs)
  852. want = &corev1.Service{
  853. ObjectMeta: metav1.ObjectMeta{
  854. Name: "test",
  855. Namespace: "default",
  856. Finalizers: []string{"tailscale.com/finalizer"},
  857. Annotations: map[string]string{
  858. "tailscale.com/expose": "true",
  859. },
  860. UID: types.UID("1234-UID"),
  861. },
  862. Spec: corev1.ServiceSpec{
  863. ClusterIP: "10.20.30.40",
  864. Type: corev1.ServiceTypeClusterIP,
  865. },
  866. Status: corev1.ServiceStatus{
  867. Conditions: proxyCreatedCondition(clock),
  868. },
  869. }
  870. expectEqual(t, fc, want)
  871. }
  872. func TestCustomHostname(t *testing.T) {
  873. fc := fake.NewFakeClient()
  874. ft := &fakeTSClient{}
  875. zl := zap.Must(zap.NewDevelopment())
  876. clock := tstest.NewClock(tstest.ClockOpts{})
  877. sr := &ServiceReconciler{
  878. Client: fc,
  879. ssr: &tailscaleSTSReconciler{
  880. Client: fc,
  881. tsClient: ft,
  882. defaultTags: []string{"tag:k8s"},
  883. operatorNamespace: "operator-ns",
  884. proxyImage: "tailscale/tailscale",
  885. },
  886. logger: zl.Sugar(),
  887. clock: clock,
  888. }
  889. // Create a service that we should manage, and check that the initial round
  890. // of objects looks right.
  891. mustCreate(t, fc, &corev1.Service{
  892. ObjectMeta: metav1.ObjectMeta{
  893. Name: "test",
  894. Namespace: "default",
  895. // The apiserver is supposed to set the UID, but the fake client
  896. // doesn't. So, set it explicitly because other code later depends
  897. // on it being set.
  898. UID: types.UID("1234-UID"),
  899. Annotations: map[string]string{
  900. "tailscale.com/expose": "true",
  901. "tailscale.com/hostname": "reindeer-flotilla",
  902. },
  903. },
  904. Spec: corev1.ServiceSpec{
  905. ClusterIP: "10.20.30.40",
  906. Type: corev1.ServiceTypeClusterIP,
  907. },
  908. })
  909. expectReconciled(t, sr, "default", "test")
  910. fullName, shortName := findGenName(t, fc, "default", "test", "svc")
  911. o := configOpts{
  912. replicas: ptr.To[int32](1),
  913. stsName: shortName,
  914. secretName: fullName,
  915. namespace: "default",
  916. parentType: "svc",
  917. hostname: "reindeer-flotilla",
  918. clusterTargetIP: "10.20.30.40",
  919. app: kubetypes.AppIngressProxy,
  920. }
  921. expectEqual(t, fc, expectedSecret(t, fc, o))
  922. expectEqual(t, fc, expectedHeadlessService(shortName, "svc"))
  923. expectEqual(t, fc, expectedSTS(t, fc, o), removeResourceReqs)
  924. want := &corev1.Service{
  925. ObjectMeta: metav1.ObjectMeta{
  926. Name: "test",
  927. Namespace: "default",
  928. Finalizers: []string{"tailscale.com/finalizer"},
  929. UID: types.UID("1234-UID"),
  930. Annotations: map[string]string{
  931. "tailscale.com/expose": "true",
  932. "tailscale.com/hostname": "reindeer-flotilla",
  933. },
  934. },
  935. Spec: corev1.ServiceSpec{
  936. ClusterIP: "10.20.30.40",
  937. Type: corev1.ServiceTypeClusterIP,
  938. },
  939. Status: corev1.ServiceStatus{
  940. Conditions: proxyCreatedCondition(clock),
  941. },
  942. }
  943. expectEqual(t, fc, want)
  944. // Turn the service back into a ClusterIP service, which should make the
  945. // operator clean up.
  946. mustUpdate(t, fc, "default", "test", func(s *corev1.Service) {
  947. delete(s.ObjectMeta.Annotations, "tailscale.com/expose")
  948. })
  949. // synchronous StatefulSet deletion triggers a requeue. But, the StatefulSet
  950. // didn't create any child resources since this is all faked, so the
  951. // deletion goes through immediately.
  952. expectReconciled(t, sr, "default", "test")
  953. expectMissing[appsv1.StatefulSet](t, fc, "operator-ns", shortName)
  954. // Second time around, the rest of cleanup happens.
  955. expectReconciled(t, sr, "default", "test")
  956. expectMissing[appsv1.StatefulSet](t, fc, "operator-ns", shortName)
  957. expectMissing[corev1.Service](t, fc, "operator-ns", shortName)
  958. expectMissing[corev1.Secret](t, fc, "operator-ns", fullName)
  959. want = &corev1.Service{
  960. ObjectMeta: metav1.ObjectMeta{
  961. Name: "test",
  962. Namespace: "default",
  963. UID: types.UID("1234-UID"),
  964. Annotations: map[string]string{
  965. "tailscale.com/hostname": "reindeer-flotilla",
  966. },
  967. },
  968. Spec: corev1.ServiceSpec{
  969. ClusterIP: "10.20.30.40",
  970. Type: corev1.ServiceTypeClusterIP,
  971. },
  972. }
  973. expectEqual(t, fc, want)
  974. }
  975. func TestCustomPriorityClassName(t *testing.T) {
  976. fc := fake.NewFakeClient()
  977. ft := &fakeTSClient{}
  978. zl := zap.Must(zap.NewDevelopment())
  979. clock := tstest.NewClock(tstest.ClockOpts{})
  980. sr := &ServiceReconciler{
  981. Client: fc,
  982. ssr: &tailscaleSTSReconciler{
  983. Client: fc,
  984. tsClient: ft,
  985. defaultTags: []string{"tag:k8s"},
  986. operatorNamespace: "operator-ns",
  987. proxyImage: "tailscale/tailscale",
  988. proxyPriorityClassName: "custom-priority-class-name",
  989. },
  990. logger: zl.Sugar(),
  991. clock: clock,
  992. }
  993. // Create a service that we should manage, and check that the initial round
  994. // of objects looks right.
  995. mustCreate(t, fc, &corev1.Service{
  996. ObjectMeta: metav1.ObjectMeta{
  997. Name: "test",
  998. Namespace: "default",
  999. // The apiserver is supposed to set the UID, but the fake client
  1000. // doesn't. So, set it explicitly because other code later depends
  1001. // on it being set.
  1002. UID: types.UID("1234-UID"),
  1003. Annotations: map[string]string{
  1004. "tailscale.com/expose": "true",
  1005. "tailscale.com/hostname": "tailscale-critical",
  1006. },
  1007. },
  1008. Spec: corev1.ServiceSpec{
  1009. ClusterIP: "10.20.30.40",
  1010. Type: corev1.ServiceTypeClusterIP,
  1011. },
  1012. })
  1013. expectReconciled(t, sr, "default", "test")
  1014. fullName, shortName := findGenName(t, fc, "default", "test", "svc")
  1015. o := configOpts{
  1016. replicas: ptr.To[int32](1),
  1017. stsName: shortName,
  1018. secretName: fullName,
  1019. namespace: "default",
  1020. parentType: "svc",
  1021. hostname: "tailscale-critical",
  1022. priorityClassName: "custom-priority-class-name",
  1023. clusterTargetIP: "10.20.30.40",
  1024. app: kubetypes.AppIngressProxy,
  1025. }
  1026. expectEqual(t, fc, expectedSTS(t, fc, o), removeResourceReqs)
  1027. }
  1028. func TestServiceProxyClassAnnotation(t *testing.T) {
  1029. cl := tstest.NewClock(tstest.ClockOpts{})
  1030. zl := zap.Must(zap.NewDevelopment())
  1031. pcIfNotPresent := &tsapi.ProxyClass{
  1032. ObjectMeta: metav1.ObjectMeta{
  1033. Name: "if-not-present",
  1034. },
  1035. Spec: tsapi.ProxyClassSpec{
  1036. StatefulSet: &tsapi.StatefulSet{
  1037. Pod: &tsapi.Pod{
  1038. TailscaleContainer: &v1alpha1.Container{
  1039. ImagePullPolicy: corev1.PullIfNotPresent,
  1040. },
  1041. },
  1042. },
  1043. },
  1044. }
  1045. pcAlways := &tsapi.ProxyClass{
  1046. ObjectMeta: metav1.ObjectMeta{
  1047. Name: "always",
  1048. },
  1049. Spec: tsapi.ProxyClassSpec{
  1050. StatefulSet: &tsapi.StatefulSet{
  1051. Pod: &tsapi.Pod{
  1052. TailscaleContainer: &v1alpha1.Container{
  1053. ImagePullPolicy: corev1.PullAlways,
  1054. },
  1055. },
  1056. },
  1057. },
  1058. }
  1059. builder := fake.NewClientBuilder().
  1060. WithScheme(tsapi.GlobalScheme)
  1061. builder = builder.WithObjects(pcIfNotPresent, pcAlways).
  1062. WithStatusSubresource(pcIfNotPresent, pcAlways)
  1063. fc := builder.Build()
  1064. svc := &corev1.Service{
  1065. ObjectMeta: metav1.ObjectMeta{
  1066. Name: "test",
  1067. Namespace: "default",
  1068. // The apiserver is supposed to set the UID, but the fake client
  1069. // doesn't. So, set it explicitly because other code later depends
  1070. // on it being set.
  1071. UID: types.UID("1234-UID"),
  1072. },
  1073. Spec: corev1.ServiceSpec{
  1074. ClusterIP: "10.20.30.40",
  1075. Type: corev1.ServiceTypeLoadBalancer,
  1076. },
  1077. }
  1078. mustCreate(t, fc, svc)
  1079. testCases := []struct {
  1080. name string
  1081. proxyClassAnnotation string
  1082. proxyClassLabel string
  1083. proxyClassDefault string
  1084. expectedProxyClass string
  1085. expectEvents []string
  1086. }{
  1087. {
  1088. name: "via_label",
  1089. proxyClassLabel: pcIfNotPresent.Name,
  1090. expectedProxyClass: pcIfNotPresent.Name,
  1091. },
  1092. {
  1093. name: "via_annotation",
  1094. proxyClassAnnotation: pcIfNotPresent.Name,
  1095. expectedProxyClass: pcIfNotPresent.Name,
  1096. },
  1097. {
  1098. name: "via_default",
  1099. proxyClassDefault: pcIfNotPresent.Name,
  1100. expectedProxyClass: pcIfNotPresent.Name,
  1101. },
  1102. {
  1103. name: "via_label_override_annotation",
  1104. proxyClassLabel: pcIfNotPresent.Name,
  1105. proxyClassAnnotation: pcAlways.Name,
  1106. expectedProxyClass: pcIfNotPresent.Name,
  1107. },
  1108. }
  1109. for _, tt := range testCases {
  1110. t.Run(tt.name, func(t *testing.T) {
  1111. ft := &fakeTSClient{}
  1112. if tt.proxyClassAnnotation != "" || tt.proxyClassLabel != "" || tt.proxyClassDefault != "" {
  1113. name := tt.proxyClassDefault
  1114. if name == "" {
  1115. name = tt.proxyClassLabel
  1116. if name == "" {
  1117. name = tt.proxyClassAnnotation
  1118. }
  1119. }
  1120. setProxyClassReady(t, fc, cl, name)
  1121. }
  1122. sr := &ServiceReconciler{
  1123. Client: fc,
  1124. ssr: &tailscaleSTSReconciler{
  1125. Client: fc,
  1126. tsClient: ft,
  1127. defaultTags: []string{"tag:k8s"},
  1128. operatorNamespace: "operator-ns",
  1129. proxyImage: "tailscale/tailscale",
  1130. },
  1131. defaultProxyClass: tt.proxyClassDefault,
  1132. logger: zl.Sugar(),
  1133. clock: cl,
  1134. isDefaultLoadBalancer: true,
  1135. }
  1136. if tt.proxyClassLabel != "" {
  1137. svc.Labels = map[string]string{
  1138. LabelAnnotationProxyClass: tt.proxyClassLabel,
  1139. }
  1140. }
  1141. if tt.proxyClassAnnotation != "" {
  1142. svc.Annotations = map[string]string{
  1143. LabelAnnotationProxyClass: tt.proxyClassAnnotation,
  1144. }
  1145. }
  1146. mustUpdate(t, fc, svc.Namespace, svc.Name, func(s *corev1.Service) {
  1147. s.Labels = svc.Labels
  1148. s.Annotations = svc.Annotations
  1149. })
  1150. expectReconciled(t, sr, "default", "test")
  1151. list := &corev1.ServiceList{}
  1152. fc.List(context.Background(), list, client.InNamespace("default"))
  1153. for _, i := range list.Items {
  1154. t.Logf("found service %s", i.Name)
  1155. }
  1156. slist := &corev1.SecretList{}
  1157. fc.List(context.Background(), slist, client.InNamespace("operator-ns"))
  1158. for _, i := range slist.Items {
  1159. labels, _ := json.Marshal(i.Labels)
  1160. t.Logf("found secret %q with labels %q ", i.Name, string(labels))
  1161. }
  1162. _, shortName := findGenName(t, fc, "default", "test", "svc")
  1163. sts := &appsv1.StatefulSet{}
  1164. if err := fc.Get(context.Background(), client.ObjectKey{Namespace: "operator-ns", Name: shortName}, sts); err != nil {
  1165. t.Fatalf("failed to get StatefulSet: %v", err)
  1166. }
  1167. switch tt.expectedProxyClass {
  1168. case pcIfNotPresent.Name:
  1169. for _, cont := range sts.Spec.Template.Spec.Containers {
  1170. if cont.Name == "tailscale" && cont.ImagePullPolicy != corev1.PullIfNotPresent {
  1171. t.Fatalf("ImagePullPolicy %q does not match ProxyClass %q with value %q", cont.ImagePullPolicy, pcIfNotPresent.Name, pcIfNotPresent.Spec.StatefulSet.Pod.TailscaleContainer.ImagePullPolicy)
  1172. }
  1173. }
  1174. case pcAlways.Name:
  1175. for _, cont := range sts.Spec.Template.Spec.Containers {
  1176. if cont.Name == "tailscale" && cont.ImagePullPolicy != corev1.PullAlways {
  1177. t.Fatalf("ImagePullPolicy %q does not match ProxyClass %q with value %q", cont.ImagePullPolicy, pcAlways.Name, pcAlways.Spec.StatefulSet.Pod.TailscaleContainer.ImagePullPolicy)
  1178. }
  1179. }
  1180. default:
  1181. t.Fatalf("unexpected expected ProxyClass %q", tt.expectedProxyClass)
  1182. }
  1183. })
  1184. }
  1185. }
  1186. func TestProxyClassForService(t *testing.T) {
  1187. // Setup
  1188. pc := &tsapi.ProxyClass{
  1189. ObjectMeta: metav1.ObjectMeta{Name: "custom-metadata"},
  1190. Spec: tsapi.ProxyClassSpec{
  1191. TailscaleConfig: &tsapi.TailscaleConfig{
  1192. AcceptRoutes: true,
  1193. },
  1194. StatefulSet: &tsapi.StatefulSet{
  1195. Labels: tsapi.Labels{"foo": "bar"},
  1196. Annotations: map[string]string{"bar.io/foo": "some-val"},
  1197. Pod: &tsapi.Pod{Annotations: map[string]string{"foo.io/bar": "some-val"}},
  1198. },
  1199. },
  1200. }
  1201. fc := fake.NewClientBuilder().
  1202. WithScheme(tsapi.GlobalScheme).
  1203. WithObjects(pc).
  1204. WithStatusSubresource(pc).
  1205. Build()
  1206. ft := &fakeTSClient{}
  1207. zl := zap.Must(zap.NewDevelopment())
  1208. clock := tstest.NewClock(tstest.ClockOpts{})
  1209. sr := &ServiceReconciler{
  1210. Client: fc,
  1211. ssr: &tailscaleSTSReconciler{
  1212. Client: fc,
  1213. tsClient: ft,
  1214. defaultTags: []string{"tag:k8s"},
  1215. operatorNamespace: "operator-ns",
  1216. proxyImage: "tailscale/tailscale",
  1217. },
  1218. logger: zl.Sugar(),
  1219. clock: clock,
  1220. }
  1221. // 1. A new tailscale LoadBalancer Service is created without any
  1222. // ProxyClass. Resources get created for it as usual.
  1223. mustCreate(t, fc, &corev1.Service{
  1224. ObjectMeta: metav1.ObjectMeta{
  1225. Name: "test",
  1226. Namespace: "default",
  1227. // The apiserver is supposed to set the UID, but the fake client
  1228. // doesn't. So, set it explicitly because other code later depends
  1229. // on it being set.
  1230. UID: types.UID("1234-UID"),
  1231. },
  1232. Spec: corev1.ServiceSpec{
  1233. ClusterIP: "10.20.30.40",
  1234. Type: corev1.ServiceTypeLoadBalancer,
  1235. LoadBalancerClass: ptr.To("tailscale"),
  1236. },
  1237. })
  1238. expectReconciled(t, sr, "default", "test")
  1239. fullName, shortName := findGenName(t, fc, "default", "test", "svc")
  1240. opts := configOpts{
  1241. replicas: ptr.To[int32](1),
  1242. stsName: shortName,
  1243. secretName: fullName,
  1244. namespace: "default",
  1245. parentType: "svc",
  1246. hostname: "default-test",
  1247. clusterTargetIP: "10.20.30.40",
  1248. app: kubetypes.AppIngressProxy,
  1249. }
  1250. expectEqual(t, fc, expectedSecret(t, fc, opts))
  1251. expectEqual(t, fc, expectedHeadlessService(shortName, "svc"))
  1252. expectEqual(t, fc, expectedSTS(t, fc, opts), removeResourceReqs)
  1253. // 2. The Service gets updated with tailscale.com/proxy-class label
  1254. // pointing at the 'custom-metadata' ProxyClass. The ProxyClass is not
  1255. // yet ready, so no changes are actually applied to the proxy resources.
  1256. mustUpdate(t, fc, "default", "test", func(svc *corev1.Service) {
  1257. mak.Set(&svc.Labels, LabelAnnotationProxyClass, "custom-metadata")
  1258. })
  1259. expectReconciled(t, sr, "default", "test")
  1260. expectEqual(t, fc, expectedSTS(t, fc, opts), removeResourceReqs)
  1261. expectEqual(t, fc, expectedSecret(t, fc, opts))
  1262. // 3. ProxyClass is set to Ready, the Service gets reconciled by the
  1263. // services-reconciler and the customization from the ProxyClass is
  1264. // applied to the proxy resources.
  1265. mustUpdateStatus(t, fc, "", "custom-metadata", func(pc *tsapi.ProxyClass) {
  1266. pc.Status = tsapi.ProxyClassStatus{
  1267. Conditions: []metav1.Condition{{
  1268. Status: metav1.ConditionTrue,
  1269. Type: string(tsapi.ProxyClassReady),
  1270. ObservedGeneration: pc.Generation,
  1271. }},
  1272. }
  1273. })
  1274. opts.proxyClass = pc.Name
  1275. expectReconciled(t, sr, "default", "test")
  1276. expectEqual(t, fc, expectedSTS(t, fc, opts), removeResourceReqs)
  1277. expectEqual(t, fc, expectedSecret(t, fc, opts), removeAuthKeyIfExistsModifier(t))
  1278. // 4. tailscale.com/proxy-class label is removed from the Service, the
  1279. // configuration from the ProxyClass is removed from the cluster
  1280. // resources.
  1281. mustUpdate(t, fc, "default", "test", func(svc *corev1.Service) {
  1282. delete(svc.Labels, LabelAnnotationProxyClass)
  1283. })
  1284. opts.proxyClass = ""
  1285. expectReconciled(t, sr, "default", "test")
  1286. expectEqual(t, fc, expectedSTS(t, fc, opts), removeResourceReqs)
  1287. }
  1288. func TestDefaultLoadBalancer(t *testing.T) {
  1289. fc := fake.NewFakeClient()
  1290. ft := &fakeTSClient{}
  1291. zl := zap.Must(zap.NewDevelopment())
  1292. clock := tstest.NewClock(tstest.ClockOpts{})
  1293. sr := &ServiceReconciler{
  1294. Client: fc,
  1295. ssr: &tailscaleSTSReconciler{
  1296. Client: fc,
  1297. tsClient: ft,
  1298. defaultTags: []string{"tag:k8s"},
  1299. operatorNamespace: "operator-ns",
  1300. proxyImage: "tailscale/tailscale",
  1301. },
  1302. logger: zl.Sugar(),
  1303. clock: clock,
  1304. isDefaultLoadBalancer: true,
  1305. }
  1306. // Create a service that we should manage, and check that the initial round
  1307. // of objects looks right.
  1308. mustCreate(t, fc, &corev1.Service{
  1309. ObjectMeta: metav1.ObjectMeta{
  1310. Name: "test",
  1311. Namespace: "default",
  1312. // The apiserver is supposed to set the UID, but the fake client
  1313. // doesn't. So, set it explicitly because other code later depends
  1314. // on it being set.
  1315. UID: types.UID("1234-UID"),
  1316. },
  1317. Spec: corev1.ServiceSpec{
  1318. ClusterIP: "10.20.30.40",
  1319. Type: corev1.ServiceTypeLoadBalancer,
  1320. },
  1321. })
  1322. expectReconciled(t, sr, "default", "test")
  1323. fullName, shortName := findGenName(t, fc, "default", "test", "svc")
  1324. expectEqual(t, fc, expectedHeadlessService(shortName, "svc"))
  1325. o := configOpts{
  1326. replicas: ptr.To[int32](1),
  1327. stsName: shortName,
  1328. secretName: fullName,
  1329. namespace: "default",
  1330. parentType: "svc",
  1331. hostname: "default-test",
  1332. clusterTargetIP: "10.20.30.40",
  1333. app: kubetypes.AppIngressProxy,
  1334. }
  1335. expectEqual(t, fc, expectedSTS(t, fc, o), removeResourceReqs)
  1336. }
  1337. func TestProxyFirewallMode(t *testing.T) {
  1338. fc := fake.NewFakeClient()
  1339. ft := &fakeTSClient{}
  1340. zl := zap.Must(zap.NewDevelopment())
  1341. clock := tstest.NewClock(tstest.ClockOpts{})
  1342. sr := &ServiceReconciler{
  1343. Client: fc,
  1344. ssr: &tailscaleSTSReconciler{
  1345. Client: fc,
  1346. tsClient: ft,
  1347. defaultTags: []string{"tag:k8s"},
  1348. operatorNamespace: "operator-ns",
  1349. proxyImage: "tailscale/tailscale",
  1350. tsFirewallMode: "nftables",
  1351. },
  1352. logger: zl.Sugar(),
  1353. clock: clock,
  1354. isDefaultLoadBalancer: true,
  1355. }
  1356. // Create a service that we should manage, and check that the initial round
  1357. // of objects looks right.
  1358. mustCreate(t, fc, &corev1.Service{
  1359. ObjectMeta: metav1.ObjectMeta{
  1360. Name: "test",
  1361. Namespace: "default",
  1362. // The apiserver is supposed to set the UID, but the fake client
  1363. // doesn't. So, set it explicitly because other code later depends
  1364. // on it being set.
  1365. UID: types.UID("1234-UID"),
  1366. },
  1367. Spec: corev1.ServiceSpec{
  1368. ClusterIP: "10.20.30.40",
  1369. Type: corev1.ServiceTypeLoadBalancer,
  1370. },
  1371. })
  1372. expectReconciled(t, sr, "default", "test")
  1373. fullName, shortName := findGenName(t, fc, "default", "test", "svc")
  1374. o := configOpts{
  1375. replicas: ptr.To[int32](1),
  1376. stsName: shortName,
  1377. secretName: fullName,
  1378. namespace: "default",
  1379. parentType: "svc",
  1380. hostname: "default-test",
  1381. firewallMode: "nftables",
  1382. clusterTargetIP: "10.20.30.40",
  1383. app: kubetypes.AppIngressProxy,
  1384. }
  1385. expectEqual(t, fc, expectedSTS(t, fc, o), removeResourceReqs)
  1386. }
  1387. func Test_isMagicDNSName(t *testing.T) {
  1388. tests := []struct {
  1389. in string
  1390. want bool
  1391. }{
  1392. {
  1393. in: "foo.tail4567.ts.net",
  1394. want: true,
  1395. },
  1396. {
  1397. in: "foo.tail4567.ts.net.",
  1398. want: true,
  1399. },
  1400. {
  1401. in: "foo.tail4567",
  1402. want: false,
  1403. },
  1404. }
  1405. for _, tt := range tests {
  1406. t.Run(tt.in, func(t *testing.T) {
  1407. if got := isMagicDNSName(tt.in); got != tt.want {
  1408. t.Errorf("isMagicDNSName(%q) = %v, want %v", tt.in, got, tt.want)
  1409. }
  1410. })
  1411. }
  1412. }
  1413. func Test_HeadlessService(t *testing.T) {
  1414. fc := fake.NewFakeClient()
  1415. zl := zap.Must(zap.NewDevelopment())
  1416. clock := tstest.NewClock(tstest.ClockOpts{})
  1417. sr := &ServiceReconciler{
  1418. Client: fc,
  1419. ssr: &tailscaleSTSReconciler{
  1420. Client: fc,
  1421. },
  1422. logger: zl.Sugar(),
  1423. clock: clock,
  1424. recorder: record.NewFakeRecorder(100),
  1425. }
  1426. mustCreate(t, fc, &corev1.Service{
  1427. ObjectMeta: metav1.ObjectMeta{
  1428. Name: "test",
  1429. Namespace: "default",
  1430. UID: types.UID("1234-UID"),
  1431. Annotations: map[string]string{
  1432. AnnotationExpose: "true",
  1433. },
  1434. },
  1435. Spec: corev1.ServiceSpec{
  1436. ClusterIP: "None",
  1437. Type: corev1.ServiceTypeClusterIP,
  1438. },
  1439. })
  1440. expectReconciled(t, sr, "default", "test")
  1441. t0 := conditionTime(clock)
  1442. want := &corev1.Service{
  1443. ObjectMeta: metav1.ObjectMeta{
  1444. Name: "test",
  1445. Namespace: "default",
  1446. UID: types.UID("1234-UID"),
  1447. Annotations: map[string]string{
  1448. AnnotationExpose: "true",
  1449. },
  1450. },
  1451. Spec: corev1.ServiceSpec{
  1452. ClusterIP: "None",
  1453. Type: corev1.ServiceTypeClusterIP,
  1454. },
  1455. Status: corev1.ServiceStatus{
  1456. Conditions: []metav1.Condition{{
  1457. Type: string(tsapi.ProxyReady),
  1458. Status: metav1.ConditionFalse,
  1459. LastTransitionTime: t0,
  1460. Reason: reasonProxyInvalid,
  1461. Message: `unable to provision proxy resources: invalid Service: headless Services are not supported.`,
  1462. }},
  1463. },
  1464. }
  1465. expectEqual(t, fc, want)
  1466. }
  1467. func Test_serviceHandlerForIngress(t *testing.T) {
  1468. const tailscaleIngressClassName = "tailscale"
  1469. fc := fake.NewFakeClient()
  1470. zl := zap.Must(zap.NewDevelopment())
  1471. // 1. An event on a headless Service for a tailscale Ingress results in
  1472. // the Ingress being reconciled.
  1473. mustCreate(t, fc, &networkingv1.Ingress{
  1474. ObjectMeta: metav1.ObjectMeta{
  1475. Name: "ing-1",
  1476. Namespace: "ns-1",
  1477. },
  1478. Spec: networkingv1.IngressSpec{IngressClassName: ptr.To(tailscaleIngressClassName)},
  1479. })
  1480. svc1 := &corev1.Service{
  1481. ObjectMeta: metav1.ObjectMeta{
  1482. Name: "headless-1",
  1483. Namespace: "tailscale",
  1484. Labels: map[string]string{
  1485. kubetypes.LabelManaged: "true",
  1486. LabelParentName: "ing-1",
  1487. LabelParentNamespace: "ns-1",
  1488. LabelParentType: "ingress",
  1489. },
  1490. },
  1491. }
  1492. mustCreate(t, fc, svc1)
  1493. wantReqs := []reconcile.Request{{NamespacedName: types.NamespacedName{Namespace: "ns-1", Name: "ing-1"}}}
  1494. gotReqs := serviceHandlerForIngress(fc, zl.Sugar(), tailscaleIngressClassName)(context.Background(), svc1)
  1495. if diff := cmp.Diff(gotReqs, wantReqs); diff != "" {
  1496. t.Fatalf("unexpected reconcile requests (-got +want):\n%s", diff)
  1497. }
  1498. // 2. An event on a Service that is the default backend for a tailscale
  1499. // Ingress results in the Ingress being reconciled.
  1500. mustCreate(t, fc, &networkingv1.Ingress{
  1501. ObjectMeta: metav1.ObjectMeta{
  1502. Name: "ing-2",
  1503. Namespace: "ns-2",
  1504. },
  1505. Spec: networkingv1.IngressSpec{
  1506. DefaultBackend: &networkingv1.IngressBackend{
  1507. Service: &networkingv1.IngressServiceBackend{Name: "def-backend"},
  1508. },
  1509. IngressClassName: ptr.To(tailscaleIngressClassName),
  1510. },
  1511. })
  1512. backendSvc := &corev1.Service{
  1513. ObjectMeta: metav1.ObjectMeta{
  1514. Name: "def-backend",
  1515. Namespace: "ns-2",
  1516. },
  1517. }
  1518. mustCreate(t, fc, backendSvc)
  1519. wantReqs = []reconcile.Request{{NamespacedName: types.NamespacedName{Namespace: "ns-2", Name: "ing-2"}}}
  1520. gotReqs = serviceHandlerForIngress(fc, zl.Sugar(), tailscaleIngressClassName)(context.Background(), backendSvc)
  1521. if diff := cmp.Diff(gotReqs, wantReqs); diff != "" {
  1522. t.Fatalf("unexpected reconcile requests (-got +want):\n%s", diff)
  1523. }
  1524. // 3. An event on a Service that is one of the non-default backends for
  1525. // a tailscale Ingress results in the Ingress being reconciled.
  1526. mustCreate(t, fc, &networkingv1.Ingress{
  1527. ObjectMeta: metav1.ObjectMeta{
  1528. Name: "ing-3",
  1529. Namespace: "ns-3",
  1530. },
  1531. Spec: networkingv1.IngressSpec{
  1532. IngressClassName: ptr.To(tailscaleIngressClassName),
  1533. Rules: []networkingv1.IngressRule{{IngressRuleValue: networkingv1.IngressRuleValue{HTTP: &networkingv1.HTTPIngressRuleValue{
  1534. Paths: []networkingv1.HTTPIngressPath{
  1535. {Backend: networkingv1.IngressBackend{Service: &networkingv1.IngressServiceBackend{Name: "backend"}}},
  1536. },
  1537. }}}},
  1538. },
  1539. })
  1540. backendSvc2 := &corev1.Service{
  1541. ObjectMeta: metav1.ObjectMeta{
  1542. Name: "backend",
  1543. Namespace: "ns-3",
  1544. },
  1545. }
  1546. mustCreate(t, fc, backendSvc2)
  1547. wantReqs = []reconcile.Request{{NamespacedName: types.NamespacedName{Namespace: "ns-3", Name: "ing-3"}}}
  1548. gotReqs = serviceHandlerForIngress(fc, zl.Sugar(), tailscaleIngressClassName)(context.Background(), backendSvc2)
  1549. if diff := cmp.Diff(gotReqs, wantReqs); diff != "" {
  1550. t.Fatalf("unexpected reconcile requests (-got +want):\n%s", diff)
  1551. }
  1552. // 4. An event on a Service that is a backend for an Ingress that is not
  1553. // tailscale Ingress does not result in an Ingress reconcile.
  1554. mustCreate(t, fc, &networkingv1.Ingress{
  1555. ObjectMeta: metav1.ObjectMeta{
  1556. Name: "ing-4",
  1557. Namespace: "ns-4",
  1558. },
  1559. Spec: networkingv1.IngressSpec{
  1560. Rules: []networkingv1.IngressRule{{IngressRuleValue: networkingv1.IngressRuleValue{HTTP: &networkingv1.HTTPIngressRuleValue{
  1561. Paths: []networkingv1.HTTPIngressPath{
  1562. {Backend: networkingv1.IngressBackend{Service: &networkingv1.IngressServiceBackend{Name: "non-ts-backend"}}},
  1563. },
  1564. }}}},
  1565. },
  1566. })
  1567. nonTSBackend := &corev1.Service{
  1568. ObjectMeta: metav1.ObjectMeta{
  1569. Name: "non-ts-backend",
  1570. Namespace: "ns-4",
  1571. },
  1572. }
  1573. mustCreate(t, fc, nonTSBackend)
  1574. gotReqs = serviceHandlerForIngress(fc, zl.Sugar(), tailscaleIngressClassName)(context.Background(), nonTSBackend)
  1575. if len(gotReqs) > 0 {
  1576. t.Errorf("unexpected reconcile request for a Service that does not belong to a Tailscale Ingress: %#+v\n", gotReqs)
  1577. }
  1578. // 5. An event on a Service not related to any Ingress does not result
  1579. // in an Ingress reconcile.
  1580. someSvc := &corev1.Service{
  1581. ObjectMeta: metav1.ObjectMeta{
  1582. Name: "some-svc",
  1583. Namespace: "ns-4",
  1584. },
  1585. }
  1586. mustCreate(t, fc, someSvc)
  1587. gotReqs = serviceHandlerForIngress(fc, zl.Sugar(), tailscaleIngressClassName)(context.Background(), someSvc)
  1588. if len(gotReqs) > 0 {
  1589. t.Errorf("unexpected reconcile request for a Service that does not belong to any Ingress: %#+v\n", gotReqs)
  1590. }
  1591. }
  1592. func Test_serviceHandlerForIngress_multipleIngressClasses(t *testing.T) {
  1593. fc := fake.NewFakeClient()
  1594. zl := zap.Must(zap.NewDevelopment())
  1595. svc := &corev1.Service{
  1596. ObjectMeta: metav1.ObjectMeta{Name: "backend", Namespace: "default"},
  1597. }
  1598. mustCreate(t, fc, svc)
  1599. mustCreate(t, fc, &networkingv1.Ingress{
  1600. ObjectMeta: metav1.ObjectMeta{Name: "nginx-ing", Namespace: "default"},
  1601. Spec: networkingv1.IngressSpec{
  1602. IngressClassName: ptr.To("nginx"),
  1603. DefaultBackend: &networkingv1.IngressBackend{Service: &networkingv1.IngressServiceBackend{Name: "backend"}},
  1604. },
  1605. })
  1606. mustCreate(t, fc, &networkingv1.Ingress{
  1607. ObjectMeta: metav1.ObjectMeta{Name: "ts-ing", Namespace: "default"},
  1608. Spec: networkingv1.IngressSpec{
  1609. IngressClassName: ptr.To("tailscale"),
  1610. DefaultBackend: &networkingv1.IngressBackend{Service: &networkingv1.IngressServiceBackend{Name: "backend"}},
  1611. },
  1612. })
  1613. got := serviceHandlerForIngress(fc, zl.Sugar(), "tailscale")(context.Background(), svc)
  1614. want := []reconcile.Request{{NamespacedName: types.NamespacedName{Namespace: "default", Name: "ts-ing"}}}
  1615. if diff := cmp.Diff(got, want); diff != "" {
  1616. t.Fatalf("unexpected reconcile requests (-got +want):\n%s", diff)
  1617. }
  1618. }
  1619. func Test_clusterDomainFromResolverConf(t *testing.T) {
  1620. zl := zap.Must(zap.NewDevelopment())
  1621. tests := []struct {
  1622. name string
  1623. conf *resolvconffile.Config
  1624. namespace string
  1625. want string
  1626. }{
  1627. {
  1628. name: "success- custom domain",
  1629. conf: &resolvconffile.Config{
  1630. SearchDomains: []dnsname.FQDN{toFQDN(t, "foo.svc.department.org.io"), toFQDN(t, "svc.department.org.io"), toFQDN(t, "department.org.io")},
  1631. },
  1632. namespace: "foo",
  1633. want: "department.org.io",
  1634. },
  1635. {
  1636. name: "success- default domain",
  1637. conf: &resolvconffile.Config{
  1638. SearchDomains: []dnsname.FQDN{toFQDN(t, "foo.svc.cluster.local."), toFQDN(t, "svc.cluster.local."), toFQDN(t, "cluster.local.")},
  1639. },
  1640. namespace: "foo",
  1641. want: "cluster.local",
  1642. },
  1643. {
  1644. name: "only two search domains found",
  1645. conf: &resolvconffile.Config{
  1646. SearchDomains: []dnsname.FQDN{toFQDN(t, "svc.department.org.io"), toFQDN(t, "department.org.io")},
  1647. },
  1648. namespace: "foo",
  1649. want: "cluster.local",
  1650. },
  1651. {
  1652. name: "first search domain does not match the expected structure",
  1653. conf: &resolvconffile.Config{
  1654. SearchDomains: []dnsname.FQDN{toFQDN(t, "foo.bar.department.org.io"), toFQDN(t, "svc.department.org.io"), toFQDN(t, "some.other.fqdn")},
  1655. },
  1656. namespace: "foo",
  1657. want: "cluster.local",
  1658. },
  1659. {
  1660. name: "second search domain does not match the expected structure",
  1661. conf: &resolvconffile.Config{
  1662. SearchDomains: []dnsname.FQDN{toFQDN(t, "foo.svc.department.org.io"), toFQDN(t, "foo.department.org.io"), toFQDN(t, "some.other.fqdn")},
  1663. },
  1664. namespace: "foo",
  1665. want: "cluster.local",
  1666. },
  1667. {
  1668. name: "third search domain does not match the expected structure",
  1669. conf: &resolvconffile.Config{
  1670. SearchDomains: []dnsname.FQDN{toFQDN(t, "foo.svc.department.org.io"), toFQDN(t, "svc.department.org.io"), toFQDN(t, "some.other.fqdn")},
  1671. },
  1672. namespace: "foo",
  1673. want: "cluster.local",
  1674. },
  1675. }
  1676. for _, tt := range tests {
  1677. t.Run(tt.name, func(t *testing.T) {
  1678. if got := clusterDomainFromResolverConf(tt.conf, tt.namespace, zl.Sugar()); got != tt.want {
  1679. t.Errorf("clusterDomainFromResolverConf() = %v, want %v", got, tt.want)
  1680. }
  1681. })
  1682. }
  1683. }
  1684. func Test_authKeyRemoval(t *testing.T) {
  1685. fc := fake.NewFakeClient()
  1686. ft := &fakeTSClient{}
  1687. zl := zap.Must(zap.NewDevelopment())
  1688. // 1. A new Service that should be exposed via Tailscale gets created, a Secret with a config that contains auth
  1689. // key is generated.
  1690. clock := tstest.NewClock(tstest.ClockOpts{})
  1691. sr := &ServiceReconciler{
  1692. Client: fc,
  1693. ssr: &tailscaleSTSReconciler{
  1694. Client: fc,
  1695. tsClient: ft,
  1696. defaultTags: []string{"tag:k8s"},
  1697. operatorNamespace: "operator-ns",
  1698. proxyImage: "tailscale/tailscale",
  1699. },
  1700. logger: zl.Sugar(),
  1701. clock: clock,
  1702. }
  1703. mustCreate(t, fc, &corev1.Service{
  1704. ObjectMeta: metav1.ObjectMeta{
  1705. Name: "test",
  1706. Namespace: "default",
  1707. UID: types.UID("1234-UID"),
  1708. },
  1709. Spec: corev1.ServiceSpec{
  1710. ClusterIP: "10.20.30.40",
  1711. Type: corev1.ServiceTypeLoadBalancer,
  1712. LoadBalancerClass: ptr.To("tailscale"),
  1713. },
  1714. })
  1715. expectReconciled(t, sr, "default", "test")
  1716. fullName, shortName := findGenName(t, fc, "default", "test", "svc")
  1717. opts := configOpts{
  1718. stsName: shortName,
  1719. secretName: fullName,
  1720. namespace: "default",
  1721. parentType: "svc",
  1722. hostname: "default-test",
  1723. clusterTargetIP: "10.20.30.40",
  1724. app: kubetypes.AppIngressProxy,
  1725. replicas: ptr.To[int32](1),
  1726. }
  1727. expectEqual(t, fc, expectedSecret(t, fc, opts))
  1728. expectEqual(t, fc, expectedHeadlessService(shortName, "svc"))
  1729. expectEqual(t, fc, expectedSTS(t, fc, opts), removeResourceReqs)
  1730. // 2. Apply update to the Secret that imitates the proxy setting device_id.
  1731. s := expectedSecret(t, fc, opts)
  1732. mustUpdate(t, fc, s.Namespace, s.Name, func(s *corev1.Secret) {
  1733. mak.Set(&s.Data, "device_id", []byte("dkkdi4CNTRL"))
  1734. })
  1735. // 3. Config should no longer contain auth key
  1736. expectReconciled(t, sr, "default", "test")
  1737. opts.shouldRemoveAuthKey = true
  1738. opts.secretExtraData = map[string][]byte{"device_id": []byte("dkkdi4CNTRL")}
  1739. expectEqual(t, fc, expectedSecret(t, fc, opts))
  1740. }
  1741. func Test_externalNameService(t *testing.T) {
  1742. fc := fake.NewFakeClient()
  1743. ft := &fakeTSClient{}
  1744. zl := zap.Must(zap.NewDevelopment())
  1745. // 1. A External name Service that should be exposed via Tailscale gets
  1746. // created.
  1747. clock := tstest.NewClock(tstest.ClockOpts{})
  1748. sr := &ServiceReconciler{
  1749. Client: fc,
  1750. ssr: &tailscaleSTSReconciler{
  1751. Client: fc,
  1752. tsClient: ft,
  1753. defaultTags: []string{"tag:k8s"},
  1754. operatorNamespace: "operator-ns",
  1755. proxyImage: "tailscale/tailscale",
  1756. },
  1757. logger: zl.Sugar(),
  1758. clock: clock,
  1759. }
  1760. // 1. Create an ExternalName Service that we should manage, and check that the initial round
  1761. // of objects looks right.
  1762. mustCreate(t, fc, &corev1.Service{
  1763. ObjectMeta: metav1.ObjectMeta{
  1764. Name: "test",
  1765. Namespace: "default",
  1766. // The apiserver is supposed to set the UID, but the fake client
  1767. // doesn't. So, set it explicitly because other code later depends
  1768. // on it being set.
  1769. UID: types.UID("1234-UID"),
  1770. Annotations: map[string]string{
  1771. AnnotationExpose: "true",
  1772. },
  1773. },
  1774. Spec: corev1.ServiceSpec{
  1775. Type: corev1.ServiceTypeExternalName,
  1776. ExternalName: "foo.com",
  1777. },
  1778. })
  1779. expectReconciled(t, sr, "default", "test")
  1780. fullName, shortName := findGenName(t, fc, "default", "test", "svc")
  1781. opts := configOpts{
  1782. replicas: ptr.To[int32](1),
  1783. stsName: shortName,
  1784. secretName: fullName,
  1785. namespace: "default",
  1786. parentType: "svc",
  1787. hostname: "default-test",
  1788. clusterTargetDNS: "foo.com",
  1789. app: kubetypes.AppIngressProxy,
  1790. }
  1791. expectEqual(t, fc, expectedSecret(t, fc, opts))
  1792. expectEqual(t, fc, expectedHeadlessService(shortName, "svc"))
  1793. expectEqual(t, fc, expectedSTS(t, fc, opts), removeResourceReqs)
  1794. // 2. Change the ExternalName and verify that changes get propagated.
  1795. mustUpdate(t, sr, "default", "test", func(s *corev1.Service) {
  1796. s.Spec.ExternalName = "bar.com"
  1797. })
  1798. expectReconciled(t, sr, "default", "test")
  1799. opts.clusterTargetDNS = "bar.com"
  1800. expectEqual(t, fc, expectedSTS(t, fc, opts), removeResourceReqs)
  1801. }
  1802. func Test_metricsResourceCreation(t *testing.T) {
  1803. pc := &tsapi.ProxyClass{
  1804. ObjectMeta: metav1.ObjectMeta{Name: "metrics", Generation: 1},
  1805. Spec: tsapi.ProxyClassSpec{},
  1806. Status: tsapi.ProxyClassStatus{
  1807. Conditions: []metav1.Condition{{
  1808. Status: metav1.ConditionTrue,
  1809. Type: string(tsapi.ProxyClassReady),
  1810. ObservedGeneration: 1,
  1811. }},
  1812. },
  1813. }
  1814. svc := &corev1.Service{
  1815. ObjectMeta: metav1.ObjectMeta{
  1816. Name: "test",
  1817. Namespace: "default",
  1818. UID: types.UID("1234-UID"),
  1819. Labels: map[string]string{LabelAnnotationProxyClass: "metrics"},
  1820. },
  1821. Spec: corev1.ServiceSpec{
  1822. ClusterIP: "10.20.30.40",
  1823. Type: corev1.ServiceTypeLoadBalancer,
  1824. LoadBalancerClass: ptr.To("tailscale"),
  1825. },
  1826. }
  1827. crd := &apiextensionsv1.CustomResourceDefinition{ObjectMeta: metav1.ObjectMeta{Name: serviceMonitorCRD}}
  1828. fc := fake.NewClientBuilder().
  1829. WithScheme(tsapi.GlobalScheme).
  1830. WithObjects(pc, svc).
  1831. WithStatusSubresource(pc).
  1832. Build()
  1833. ft := &fakeTSClient{}
  1834. zl := zap.Must(zap.NewDevelopment())
  1835. clock := tstest.NewClock(tstest.ClockOpts{})
  1836. sr := &ServiceReconciler{
  1837. Client: fc,
  1838. ssr: &tailscaleSTSReconciler{
  1839. Client: fc,
  1840. tsClient: ft,
  1841. operatorNamespace: "operator-ns",
  1842. },
  1843. logger: zl.Sugar(),
  1844. clock: clock,
  1845. }
  1846. expectReconciled(t, sr, "default", "test")
  1847. fullName, shortName := findGenName(t, fc, "default", "test", "svc")
  1848. opts := configOpts{
  1849. stsName: shortName,
  1850. secretName: fullName,
  1851. namespace: "default",
  1852. parentType: "svc",
  1853. tailscaleNamespace: "operator-ns",
  1854. hostname: "default-test",
  1855. namespaced: true,
  1856. proxyType: proxyTypeIngressService,
  1857. app: kubetypes.AppIngressProxy,
  1858. resourceVersion: "1",
  1859. }
  1860. // 1. Enable metrics- expect metrics Service to be created
  1861. mustUpdate(t, fc, "", "metrics", func(pc *tsapi.ProxyClass) {
  1862. pc.Spec = tsapi.ProxyClassSpec{Metrics: &tsapi.Metrics{Enable: true}}
  1863. })
  1864. expectReconciled(t, sr, "default", "test")
  1865. opts.enableMetrics = true
  1866. expectEqual(t, fc, expectedMetricsService(opts))
  1867. // 2. Enable ServiceMonitor - should not error when there is no ServiceMonitor CRD in cluster
  1868. mustUpdate(t, fc, "", "metrics", func(pc *tsapi.ProxyClass) {
  1869. pc.Spec.Metrics.ServiceMonitor = &tsapi.ServiceMonitor{Enable: true}
  1870. })
  1871. expectReconciled(t, sr, "default", "test")
  1872. // 3. Create ServiceMonitor CRD and reconcile- ServiceMonitor should get created
  1873. mustCreate(t, fc, crd)
  1874. expectReconciled(t, sr, "default", "test")
  1875. expectEqualUnstructured(t, fc, expectedServiceMonitor(t, opts))
  1876. // 4. A change to ServiceMonitor config gets reflected in the ServiceMonitor resource
  1877. mustUpdate(t, fc, "", "metrics", func(pc *tsapi.ProxyClass) {
  1878. pc.Spec.Metrics.ServiceMonitor.Labels = tsapi.Labels{"foo": "bar"}
  1879. })
  1880. expectReconciled(t, sr, "default", "test")
  1881. opts.serviceMonitorLabels = tsapi.Labels{"foo": "bar"}
  1882. opts.resourceVersion = "2"
  1883. expectEqual(t, fc, expectedMetricsService(opts))
  1884. expectEqualUnstructured(t, fc, expectedServiceMonitor(t, opts))
  1885. // 5. Disable metrics- expect metrics Service to be deleted
  1886. mustUpdate(t, fc, "", "metrics", func(pc *tsapi.ProxyClass) {
  1887. pc.Spec.Metrics = nil
  1888. })
  1889. expectReconciled(t, sr, "default", "test")
  1890. expectMissing[corev1.Service](t, fc, "operator-ns", metricsResourceName(opts.stsName))
  1891. // ServiceMonitor gets garbage collected when Service gets deleted (it has OwnerReference of the Service
  1892. // object). We cannot test this using the fake client.
  1893. }
  1894. func TestIgnorePGService(t *testing.T) {
  1895. // NOTE: creating proxygroup stuff just to be sure that it's all ignored
  1896. _, _, fc, _, _ := setupServiceTest(t)
  1897. ft := &fakeTSClient{}
  1898. zl := zap.Must(zap.NewDevelopment())
  1899. clock := tstest.NewClock(tstest.ClockOpts{})
  1900. sr := &ServiceReconciler{
  1901. Client: fc,
  1902. ssr: &tailscaleSTSReconciler{
  1903. Client: fc,
  1904. tsClient: ft,
  1905. defaultTags: []string{"tag:k8s"},
  1906. operatorNamespace: "operator-ns",
  1907. proxyImage: "tailscale/tailscale",
  1908. },
  1909. logger: zl.Sugar(),
  1910. clock: clock,
  1911. }
  1912. // Create a service that we should manage, and check that the initial round
  1913. // of objects looks right.
  1914. mustCreate(t, fc, &corev1.Service{
  1915. ObjectMeta: metav1.ObjectMeta{
  1916. Name: "test",
  1917. Namespace: "default",
  1918. // The apiserver is supposed to set the UID, but the fake client
  1919. // doesn't. So, set it explicitly because other code later depends
  1920. // on it being set.
  1921. UID: types.UID("1234-UID"),
  1922. Annotations: map[string]string{
  1923. "tailscale.com/proxygroup": "test-pg",
  1924. },
  1925. },
  1926. Spec: corev1.ServiceSpec{
  1927. ClusterIP: "10.20.30.40",
  1928. Type: corev1.ServiceTypeClusterIP,
  1929. },
  1930. })
  1931. expectReconciled(t, sr, "default", "test")
  1932. findNoGenName(t, fc, "default", "test", "svc")
  1933. }
  1934. func toFQDN(t *testing.T, s string) dnsname.FQDN {
  1935. t.Helper()
  1936. fqdn, err := dnsname.ToFQDN(s)
  1937. if err != nil {
  1938. t.Fatalf("error coverting %q to dnsname.FQDN: %v", s, err)
  1939. }
  1940. return fqdn
  1941. }
  1942. func proxyCreatedCondition(clock tstime.Clock) []metav1.Condition {
  1943. return []metav1.Condition{{
  1944. Type: string(tsapi.ProxyReady),
  1945. Status: metav1.ConditionTrue,
  1946. ObservedGeneration: 0,
  1947. LastTransitionTime: conditionTime(clock),
  1948. Reason: reasonProxyCreated,
  1949. Message: reasonProxyCreated,
  1950. }}
  1951. }
  1952. func conditionTime(clock tstime.Clock) metav1.Time {
  1953. return metav1.NewTime(clock.Now().Truncate(time.Second))
  1954. }