controller_test.go 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389
  1. package config
  2. import (
  3. "fmt"
  4. "os"
  5. "path/filepath"
  6. "testing"
  7. cloudconfig "github.com/opencost/opencost/pkg/cloud"
  8. "github.com/opencost/opencost/pkg/cloud/aws"
  9. "github.com/opencost/opencost/pkg/cloud/gcp"
  10. )
  11. // Baseline valid config
  12. var validAthenaConf = &aws.AthenaConfiguration{
  13. Bucket: "bucket",
  14. Region: "region",
  15. Database: "database",
  16. Table: "table",
  17. Workgroup: "workgroup",
  18. Account: "account",
  19. Authorizer: &aws.ServiceAccount{},
  20. }
  21. // Config with the same key as the baseline but is not equal to it because of the change in the non-keyed property Workgroup
  22. var validAthenaConfModifiedProperty = &aws.AthenaConfiguration{
  23. Bucket: "bucket",
  24. Region: "region",
  25. Database: "database",
  26. Table: "table",
  27. Workgroup: "workgroup1",
  28. Account: "account",
  29. Authorizer: &aws.ServiceAccount{},
  30. }
  31. // Config with the same key as baseline but is invalid due to missing Authorizer
  32. var invalidAthenaConf = &aws.AthenaConfiguration{
  33. Bucket: "bucket",
  34. Region: "region",
  35. Database: "database",
  36. Table: "table",
  37. Workgroup: "workgroup",
  38. Account: "account",
  39. Authorizer: nil,
  40. }
  41. // A valid config with a different key from the baseline
  42. var validBigQueryConf = &gcp.BigQueryConfiguration{
  43. ProjectID: "projectID",
  44. Dataset: "dataset",
  45. Table: "table",
  46. Authorizer: &gcp.WorkloadIdentity{},
  47. }
  48. func TestIntegrationController_pullWatchers(t *testing.T) {
  49. testCases := map[string]struct {
  50. initialStatuses []*Status
  51. configWatchers map[ConfigSource]cloudconfig.KeyedConfigWatcher
  52. expectedStatuses []*Status
  53. }{
  54. // Helm Source
  55. "Helm Source init": {
  56. initialStatuses: []*Status{},
  57. configWatchers: map[ConfigSource]cloudconfig.KeyedConfigWatcher{
  58. HelmSource: &MockKeyedConfigWatcher{
  59. Integrations: []cloudconfig.KeyedConfig{
  60. validAthenaConf,
  61. },
  62. },
  63. },
  64. expectedStatuses: []*Status{
  65. {
  66. Source: HelmSource,
  67. Key: validAthenaConf.Key(),
  68. Active: true,
  69. Valid: true,
  70. ConfigType: AthenaConfigType,
  71. Config: validAthenaConf,
  72. },
  73. },
  74. },
  75. "Helm Source No Change": {
  76. initialStatuses: []*Status{
  77. {
  78. Source: HelmSource,
  79. Key: validAthenaConf.Key(),
  80. Active: true,
  81. Valid: true,
  82. ConfigType: AthenaConfigType,
  83. Config: validAthenaConf,
  84. },
  85. },
  86. configWatchers: map[ConfigSource]cloudconfig.KeyedConfigWatcher{
  87. HelmSource: &MockKeyedConfigWatcher{
  88. Integrations: []cloudconfig.KeyedConfig{
  89. validAthenaConf,
  90. },
  91. },
  92. },
  93. expectedStatuses: []*Status{
  94. {
  95. Source: HelmSource,
  96. Key: validAthenaConf.Key(),
  97. Active: true,
  98. Valid: true,
  99. ConfigType: AthenaConfigType,
  100. Config: validAthenaConf,
  101. },
  102. },
  103. },
  104. "Helm Source Update Config": {
  105. initialStatuses: []*Status{
  106. {
  107. Source: HelmSource,
  108. Key: validAthenaConfModifiedProperty.Key(),
  109. Active: true,
  110. Valid: true,
  111. ConfigType: AthenaConfigType,
  112. Config: validAthenaConfModifiedProperty,
  113. },
  114. },
  115. configWatchers: map[ConfigSource]cloudconfig.KeyedConfigWatcher{
  116. HelmSource: &MockKeyedConfigWatcher{
  117. Integrations: []cloudconfig.KeyedConfig{
  118. validAthenaConf,
  119. },
  120. },
  121. },
  122. expectedStatuses: []*Status{
  123. {
  124. Source: HelmSource,
  125. Key: validAthenaConf.Key(),
  126. Active: true,
  127. Valid: true,
  128. ConfigType: AthenaConfigType,
  129. Config: validAthenaConf,
  130. },
  131. },
  132. },
  133. "Helm Source Update Config Invalid": {
  134. initialStatuses: []*Status{
  135. {
  136. Source: HelmSource,
  137. Key: validAthenaConf.Key(),
  138. Active: true,
  139. Valid: true,
  140. ConfigType: AthenaConfigType,
  141. Config: validAthenaConf,
  142. },
  143. },
  144. configWatchers: map[ConfigSource]cloudconfig.KeyedConfigWatcher{
  145. HelmSource: &MockKeyedConfigWatcher{
  146. Integrations: []cloudconfig.KeyedConfig{
  147. invalidAthenaConf,
  148. },
  149. },
  150. },
  151. expectedStatuses: []*Status{
  152. {
  153. Source: HelmSource,
  154. Key: invalidAthenaConf.Key(),
  155. Active: false,
  156. Valid: false,
  157. ConfigType: AthenaConfigType,
  158. Config: invalidAthenaConf,
  159. },
  160. },
  161. },
  162. "Helm Source New Config": {
  163. initialStatuses: []*Status{
  164. {
  165. Source: HelmSource,
  166. Key: validAthenaConf.Key(),
  167. Active: true,
  168. Valid: true,
  169. ConfigType: AthenaConfigType,
  170. Config: validAthenaConf,
  171. },
  172. },
  173. configWatchers: map[ConfigSource]cloudconfig.KeyedConfigWatcher{
  174. HelmSource: &MockKeyedConfigWatcher{
  175. Integrations: []cloudconfig.KeyedConfig{
  176. validBigQueryConf,
  177. },
  178. },
  179. },
  180. expectedStatuses: []*Status{
  181. {
  182. Source: HelmSource,
  183. Key: validBigQueryConf.Key(),
  184. Active: true,
  185. Valid: true,
  186. ConfigType: AthenaConfigType,
  187. Config: validBigQueryConf,
  188. },
  189. },
  190. },
  191. // Config File
  192. "Config File Source init": {
  193. initialStatuses: []*Status{},
  194. configWatchers: map[ConfigSource]cloudconfig.KeyedConfigWatcher{
  195. ConfigFileSource: &MockKeyedConfigWatcher{
  196. Integrations: []cloudconfig.KeyedConfig{
  197. validAthenaConf,
  198. },
  199. },
  200. },
  201. expectedStatuses: []*Status{
  202. {
  203. Source: ConfigFileSource,
  204. Key: validAthenaConf.Key(),
  205. Active: true,
  206. Valid: true,
  207. ConfigType: AthenaConfigType,
  208. Config: validAthenaConf,
  209. },
  210. },
  211. },
  212. "Config File No Change": {
  213. initialStatuses: []*Status{
  214. {
  215. Source: ConfigFileSource,
  216. Key: validAthenaConf.Key(),
  217. Active: true,
  218. Valid: true,
  219. ConfigType: AthenaConfigType,
  220. Config: validAthenaConf,
  221. },
  222. },
  223. configWatchers: map[ConfigSource]cloudconfig.KeyedConfigWatcher{
  224. ConfigFileSource: &MockKeyedConfigWatcher{
  225. Integrations: []cloudconfig.KeyedConfig{
  226. validAthenaConf,
  227. },
  228. },
  229. },
  230. expectedStatuses: []*Status{
  231. {
  232. Source: ConfigFileSource,
  233. Key: validAthenaConf.Key(),
  234. Active: true,
  235. Valid: true,
  236. ConfigType: AthenaConfigType,
  237. Config: validAthenaConf,
  238. },
  239. },
  240. },
  241. "Config File Update Config": {
  242. initialStatuses: []*Status{
  243. {
  244. Source: ConfigFileSource,
  245. Key: validAthenaConf.Key(),
  246. Active: true,
  247. Valid: true,
  248. ConfigType: AthenaConfigType,
  249. Config: validAthenaConf,
  250. },
  251. },
  252. configWatchers: map[ConfigSource]cloudconfig.KeyedConfigWatcher{
  253. ConfigFileSource: &MockKeyedConfigWatcher{
  254. Integrations: []cloudconfig.KeyedConfig{
  255. validAthenaConf,
  256. },
  257. },
  258. },
  259. expectedStatuses: []*Status{
  260. {
  261. Source: ConfigFileSource,
  262. Key: validAthenaConf.Key(),
  263. Active: true,
  264. Valid: true,
  265. ConfigType: AthenaConfigType,
  266. Config: validAthenaConf,
  267. },
  268. },
  269. },
  270. "Config File Update Config Invalid": {
  271. initialStatuses: []*Status{
  272. {
  273. Source: ConfigFileSource,
  274. Key: validAthenaConf.Key(),
  275. Active: true,
  276. Valid: true,
  277. ConfigType: AthenaConfigType,
  278. Config: validAthenaConf,
  279. },
  280. },
  281. configWatchers: map[ConfigSource]cloudconfig.KeyedConfigWatcher{
  282. ConfigFileSource: &MockKeyedConfigWatcher{
  283. Integrations: []cloudconfig.KeyedConfig{
  284. invalidAthenaConf,
  285. },
  286. },
  287. },
  288. expectedStatuses: []*Status{
  289. {
  290. Source: ConfigFileSource,
  291. Key: invalidAthenaConf.Key(),
  292. Active: false,
  293. Valid: false,
  294. ConfigType: AthenaConfigType,
  295. Config: invalidAthenaConf,
  296. },
  297. },
  298. },
  299. "Config File New Config": {
  300. initialStatuses: []*Status{
  301. {
  302. Source: ConfigFileSource,
  303. Key: validAthenaConf.Key(),
  304. Active: true,
  305. Valid: true,
  306. ConfigType: AthenaConfigType,
  307. Config: validAthenaConf,
  308. },
  309. },
  310. configWatchers: map[ConfigSource]cloudconfig.KeyedConfigWatcher{
  311. ConfigFileSource: &MockKeyedConfigWatcher{
  312. Integrations: []cloudconfig.KeyedConfig{
  313. validBigQueryConf,
  314. },
  315. },
  316. },
  317. expectedStatuses: []*Status{
  318. {
  319. Source: ConfigFileSource,
  320. Key: validBigQueryConf.Key(),
  321. Active: true,
  322. Valid: true,
  323. ConfigType: BigQueryConfigType,
  324. Config: validBigQueryConf,
  325. },
  326. },
  327. },
  328. // Multi Cloud
  329. "Multi Cloud Source init": {
  330. initialStatuses: []*Status{},
  331. configWatchers: map[ConfigSource]cloudconfig.KeyedConfigWatcher{
  332. MultiCloudSource: &MockKeyedConfigWatcher{
  333. Integrations: []cloudconfig.KeyedConfig{
  334. validAthenaConf,
  335. },
  336. },
  337. },
  338. expectedStatuses: []*Status{
  339. {
  340. Source: MultiCloudSource,
  341. Key: validAthenaConf.Key(),
  342. Active: true,
  343. Valid: true,
  344. ConfigType: AthenaConfigType,
  345. Config: validAthenaConf,
  346. },
  347. },
  348. },
  349. "Multi Cloud No Change": {
  350. initialStatuses: []*Status{
  351. {
  352. Source: MultiCloudSource,
  353. Key: validAthenaConf.Key(),
  354. Active: true,
  355. Valid: true,
  356. ConfigType: AthenaConfigType,
  357. Config: validAthenaConf,
  358. },
  359. },
  360. configWatchers: map[ConfigSource]cloudconfig.KeyedConfigWatcher{
  361. MultiCloudSource: &MockKeyedConfigWatcher{
  362. Integrations: []cloudconfig.KeyedConfig{
  363. validAthenaConf,
  364. },
  365. },
  366. },
  367. expectedStatuses: []*Status{
  368. {
  369. Source: MultiCloudSource,
  370. Key: validAthenaConf.Key(),
  371. Active: true,
  372. Valid: true,
  373. ConfigType: AthenaConfigType,
  374. Config: validAthenaConf,
  375. },
  376. },
  377. },
  378. "Multi Cloud Update Config": {
  379. initialStatuses: []*Status{
  380. {
  381. Source: MultiCloudSource,
  382. Key: validAthenaConf.Key(),
  383. Active: true,
  384. Valid: true,
  385. ConfigType: AthenaConfigType,
  386. Config: validAthenaConf,
  387. },
  388. },
  389. configWatchers: map[ConfigSource]cloudconfig.KeyedConfigWatcher{
  390. MultiCloudSource: &MockKeyedConfigWatcher{
  391. Integrations: []cloudconfig.KeyedConfig{
  392. validAthenaConfModifiedProperty,
  393. },
  394. },
  395. },
  396. expectedStatuses: []*Status{
  397. {
  398. Source: MultiCloudSource,
  399. Key: validAthenaConfModifiedProperty.Key(),
  400. Active: true,
  401. Valid: true,
  402. ConfigType: AthenaConfigType,
  403. Config: validAthenaConfModifiedProperty,
  404. },
  405. },
  406. },
  407. "Multi Cloud Update Config Invalid": {
  408. initialStatuses: []*Status{
  409. {
  410. Source: MultiCloudSource,
  411. Key: validAthenaConf.Key(),
  412. Active: true,
  413. Valid: true,
  414. ConfigType: AthenaConfigType,
  415. Config: validAthenaConf,
  416. },
  417. },
  418. configWatchers: map[ConfigSource]cloudconfig.KeyedConfigWatcher{
  419. MultiCloudSource: &MockKeyedConfigWatcher{
  420. Integrations: []cloudconfig.KeyedConfig{
  421. invalidAthenaConf,
  422. },
  423. },
  424. },
  425. expectedStatuses: []*Status{
  426. {
  427. Source: MultiCloudSource,
  428. Key: invalidAthenaConf.Key(),
  429. Active: false,
  430. Valid: false,
  431. ConfigType: AthenaConfigType,
  432. Config: invalidAthenaConf,
  433. },
  434. },
  435. },
  436. "Multi Cloud New Config": {
  437. initialStatuses: []*Status{
  438. {
  439. Source: MultiCloudSource,
  440. Key: validAthenaConf.Key(),
  441. Active: true,
  442. Valid: true,
  443. ConfigType: AthenaConfigType,
  444. Config: validAthenaConf,
  445. },
  446. },
  447. configWatchers: map[ConfigSource]cloudconfig.KeyedConfigWatcher{
  448. MultiCloudSource: &MockKeyedConfigWatcher{
  449. Integrations: []cloudconfig.KeyedConfig{
  450. validBigQueryConf,
  451. },
  452. },
  453. },
  454. expectedStatuses: []*Status{
  455. {
  456. Source: MultiCloudSource,
  457. Key: validBigQueryConf.Key(),
  458. Active: true,
  459. Valid: true,
  460. ConfigType: BigQueryConfigType,
  461. Config: validBigQueryConf,
  462. },
  463. },
  464. },
  465. "Multi Cloud Delete All": {
  466. initialStatuses: []*Status{
  467. {
  468. Source: MultiCloudSource,
  469. Key: validAthenaConf.Key(),
  470. Active: true,
  471. Valid: true,
  472. ConfigType: AthenaConfigType,
  473. Config: validAthenaConf,
  474. },
  475. },
  476. configWatchers: map[ConfigSource]cloudconfig.KeyedConfigWatcher{
  477. MultiCloudSource: &MockKeyedConfigWatcher{},
  478. },
  479. expectedStatuses: []*Status{},
  480. },
  481. // Watch Interaction
  482. "New Helm, Existing Config File": {
  483. initialStatuses: []*Status{
  484. {
  485. Source: ConfigFileSource,
  486. Key: validAthenaConf.Key(),
  487. Active: true,
  488. Valid: true,
  489. ConfigType: AthenaConfigType,
  490. Config: validAthenaConf,
  491. },
  492. },
  493. configWatchers: map[ConfigSource]cloudconfig.KeyedConfigWatcher{
  494. ConfigFileSource: &MockKeyedConfigWatcher{
  495. Integrations: []cloudconfig.KeyedConfig{
  496. validAthenaConf,
  497. },
  498. },
  499. HelmSource: &MockKeyedConfigWatcher{
  500. Integrations: []cloudconfig.KeyedConfig{
  501. validBigQueryConf,
  502. },
  503. },
  504. },
  505. expectedStatuses: []*Status{
  506. {
  507. Source: ConfigFileSource,
  508. Key: validAthenaConf.Key(),
  509. Active: false,
  510. Valid: true,
  511. ConfigType: AthenaConfigType,
  512. Config: validAthenaConf,
  513. },
  514. {
  515. Source: HelmSource,
  516. Key: validBigQueryConf.Key(),
  517. Active: true,
  518. Valid: true,
  519. ConfigType: BigQueryConfigType,
  520. Config: validBigQueryConf,
  521. },
  522. },
  523. },
  524. "Update Helm, Existing Config File": {
  525. initialStatuses: []*Status{
  526. {
  527. Source: HelmSource,
  528. Key: validAthenaConf.Key(),
  529. Active: false,
  530. Valid: true,
  531. ConfigType: AthenaConfigType,
  532. Config: validAthenaConf,
  533. },
  534. {
  535. Source: ConfigFileSource,
  536. Key: validBigQueryConf.Key(),
  537. Active: true,
  538. Valid: true,
  539. ConfigType: BigQueryConfigType,
  540. Config: validBigQueryConf,
  541. },
  542. },
  543. configWatchers: map[ConfigSource]cloudconfig.KeyedConfigWatcher{
  544. ConfigFileSource: &MockKeyedConfigWatcher{
  545. Integrations: []cloudconfig.KeyedConfig{
  546. validBigQueryConf,
  547. },
  548. },
  549. HelmSource: &MockKeyedConfigWatcher{
  550. Integrations: []cloudconfig.KeyedConfig{
  551. validAthenaConfModifiedProperty,
  552. },
  553. },
  554. },
  555. expectedStatuses: []*Status{
  556. {
  557. Source: HelmSource,
  558. Key: validAthenaConfModifiedProperty.Key(),
  559. Active: true,
  560. Valid: true,
  561. ConfigType: AthenaConfigType,
  562. Config: validAthenaConfModifiedProperty,
  563. },
  564. {
  565. Source: ConfigFileSource,
  566. Key: validBigQueryConf.Key(),
  567. Active: false,
  568. Valid: true,
  569. ConfigType: BigQueryConfigType,
  570. Config: validBigQueryConf,
  571. },
  572. },
  573. },
  574. "New Helm Invalid, Existing Config File": {
  575. initialStatuses: []*Status{
  576. {
  577. Source: ConfigFileSource,
  578. Key: validBigQueryConf.Key(),
  579. Active: true,
  580. Valid: true,
  581. ConfigType: BigQueryConfigType,
  582. Config: validBigQueryConf,
  583. },
  584. },
  585. configWatchers: map[ConfigSource]cloudconfig.KeyedConfigWatcher{
  586. ConfigFileSource: &MockKeyedConfigWatcher{
  587. Integrations: []cloudconfig.KeyedConfig{
  588. validBigQueryConf,
  589. },
  590. },
  591. HelmSource: &MockKeyedConfigWatcher{
  592. Integrations: []cloudconfig.KeyedConfig{
  593. invalidAthenaConf,
  594. },
  595. },
  596. },
  597. expectedStatuses: []*Status{
  598. {
  599. Source: ConfigFileSource,
  600. Key: validBigQueryConf.Key(),
  601. Active: true,
  602. Valid: true,
  603. ConfigType: BigQueryConfigType,
  604. Config: validBigQueryConf,
  605. },
  606. {
  607. Source: HelmSource,
  608. Key: invalidAthenaConf.Key(),
  609. Active: false,
  610. Valid: false,
  611. ConfigType: AthenaConfigType,
  612. Config: invalidAthenaConf,
  613. },
  614. },
  615. },
  616. "Update Helm Invalid, Existing Config File": {
  617. initialStatuses: []*Status{
  618. {
  619. Source: ConfigFileSource,
  620. Key: validBigQueryConf.Key(),
  621. Active: true,
  622. Valid: true,
  623. ConfigType: BigQueryConfigType,
  624. Config: validBigQueryConf,
  625. },
  626. {
  627. Source: HelmSource,
  628. Key: validAthenaConf.Key(),
  629. Active: false,
  630. Valid: true,
  631. ConfigType: AthenaConfigType,
  632. Config: validAthenaConf,
  633. },
  634. },
  635. configWatchers: map[ConfigSource]cloudconfig.KeyedConfigWatcher{
  636. ConfigFileSource: &MockKeyedConfigWatcher{
  637. Integrations: []cloudconfig.KeyedConfig{
  638. validBigQueryConf,
  639. },
  640. },
  641. HelmSource: &MockKeyedConfigWatcher{
  642. Integrations: []cloudconfig.KeyedConfig{
  643. invalidAthenaConf,
  644. },
  645. },
  646. },
  647. expectedStatuses: []*Status{
  648. {
  649. Source: ConfigFileSource,
  650. Key: validBigQueryConf.Key(),
  651. Active: true,
  652. Valid: true,
  653. ConfigType: BigQueryConfigType,
  654. Config: validBigQueryConf,
  655. },
  656. {
  657. Source: HelmSource,
  658. Key: invalidAthenaConf.Key(),
  659. Active: false,
  660. Valid: false,
  661. ConfigType: AthenaConfigType,
  662. Config: invalidAthenaConf,
  663. },
  664. },
  665. },
  666. "New Config File, Existing Helm": {
  667. initialStatuses: []*Status{
  668. {
  669. Source: HelmSource,
  670. Key: validAthenaConf.Key(),
  671. Active: true,
  672. Valid: true,
  673. ConfigType: AthenaConfigType,
  674. Config: validAthenaConf,
  675. },
  676. },
  677. configWatchers: map[ConfigSource]cloudconfig.KeyedConfigWatcher{
  678. HelmSource: &MockKeyedConfigWatcher{
  679. Integrations: []cloudconfig.KeyedConfig{
  680. validAthenaConf,
  681. },
  682. },
  683. ConfigFileSource: &MockKeyedConfigWatcher{
  684. Integrations: []cloudconfig.KeyedConfig{
  685. validBigQueryConf,
  686. },
  687. },
  688. },
  689. expectedStatuses: []*Status{
  690. {
  691. Source: HelmSource,
  692. Key: validAthenaConf.Key(),
  693. Active: false,
  694. Valid: true,
  695. ConfigType: AthenaConfigType,
  696. Config: validAthenaConf,
  697. },
  698. {
  699. Source: ConfigFileSource,
  700. Key: validBigQueryConf.Key(),
  701. Active: true,
  702. Valid: true,
  703. ConfigType: BigQueryConfigType,
  704. Config: validBigQueryConf,
  705. },
  706. },
  707. },
  708. "Update Config File, Existing Helm": {
  709. initialStatuses: []*Status{
  710. {
  711. Source: HelmSource,
  712. Key: validBigQueryConf.Key(),
  713. Active: true,
  714. Valid: true,
  715. ConfigType: BigQueryConfigType,
  716. Config: validBigQueryConf,
  717. },
  718. },
  719. configWatchers: map[ConfigSource]cloudconfig.KeyedConfigWatcher{
  720. HelmSource: &MockKeyedConfigWatcher{
  721. Integrations: []cloudconfig.KeyedConfig{},
  722. },
  723. ConfigFileSource: &MockKeyedConfigWatcher{
  724. Integrations: []cloudconfig.KeyedConfig{
  725. validAthenaConfModifiedProperty,
  726. },
  727. },
  728. },
  729. expectedStatuses: []*Status{
  730. {
  731. Source: ConfigFileSource,
  732. Key: validAthenaConfModifiedProperty.Key(),
  733. Active: true,
  734. Valid: true,
  735. ConfigType: AthenaConfigType,
  736. Config: validAthenaConfModifiedProperty,
  737. },
  738. },
  739. },
  740. "New Config File Invalid, Existing Helm": {
  741. initialStatuses: []*Status{
  742. {
  743. Source: HelmSource,
  744. Key: validBigQueryConf.Key(),
  745. Active: true,
  746. Valid: true,
  747. ConfigType: BigQueryConfigType,
  748. Config: validBigQueryConf,
  749. },
  750. },
  751. configWatchers: map[ConfigSource]cloudconfig.KeyedConfigWatcher{
  752. HelmSource: &MockKeyedConfigWatcher{
  753. Integrations: []cloudconfig.KeyedConfig{
  754. validBigQueryConf,
  755. },
  756. },
  757. ConfigFileSource: &MockKeyedConfigWatcher{
  758. Integrations: []cloudconfig.KeyedConfig{
  759. invalidAthenaConf,
  760. },
  761. },
  762. },
  763. expectedStatuses: []*Status{
  764. {
  765. Source: HelmSource,
  766. Key: validBigQueryConf.Key(),
  767. Active: true,
  768. Valid: true,
  769. ConfigType: BigQueryConfigType,
  770. Config: validBigQueryConf,
  771. },
  772. {
  773. Source: ConfigFileSource,
  774. Key: invalidAthenaConf.Key(),
  775. Active: false,
  776. Valid: false,
  777. ConfigType: AthenaConfigType,
  778. Config: invalidAthenaConf,
  779. },
  780. },
  781. },
  782. "Update Config File Invalid, Existing Helm": {
  783. initialStatuses: []*Status{
  784. {
  785. Source: HelmSource,
  786. Key: validBigQueryConf.Key(),
  787. Active: true,
  788. Valid: true,
  789. ConfigType: BigQueryConfigType,
  790. Config: validBigQueryConf,
  791. },
  792. {
  793. Source: ConfigFileSource,
  794. Key: validAthenaConf.Key(),
  795. Active: false,
  796. Valid: true,
  797. ConfigType: AthenaConfigType,
  798. Config: validAthenaConf,
  799. },
  800. },
  801. configWatchers: map[ConfigSource]cloudconfig.KeyedConfigWatcher{
  802. HelmSource: &MockKeyedConfigWatcher{
  803. Integrations: []cloudconfig.KeyedConfig{
  804. validBigQueryConf,
  805. },
  806. },
  807. ConfigFileSource: &MockKeyedConfigWatcher{
  808. Integrations: []cloudconfig.KeyedConfig{
  809. invalidAthenaConf,
  810. },
  811. },
  812. },
  813. expectedStatuses: []*Status{
  814. {
  815. Source: HelmSource,
  816. Key: validBigQueryConf.Key(),
  817. Active: true,
  818. Valid: true,
  819. ConfigType: BigQueryConfigType,
  820. Config: validBigQueryConf,
  821. },
  822. {
  823. Source: ConfigFileSource,
  824. Key: invalidAthenaConf.Key(),
  825. Active: false,
  826. Valid: false,
  827. ConfigType: AthenaConfigType,
  828. Config: invalidAthenaConf,
  829. },
  830. },
  831. },
  832. }
  833. for name, tc := range testCases {
  834. t.Run(name, func(t *testing.T) {
  835. // Test set up and validation
  836. initialStatuses, err := buildStatuses(tc.initialStatuses)
  837. if err != nil {
  838. t.Errorf("initial statuses: %s", err.Error())
  839. }
  840. expectedStatuses, err := buildStatuses(tc.expectedStatuses)
  841. if err != nil {
  842. t.Errorf("initial statuses: %s", err.Error())
  843. }
  844. tempDir := os.TempDir()
  845. path := filepath.Join(tempDir, configFile)
  846. defer os.Remove(path)
  847. // Initialize controller
  848. icd := &Controller{
  849. path: path,
  850. watchers: tc.configWatchers,
  851. }
  852. err = icd.save(initialStatuses)
  853. if err != nil {
  854. t.Errorf("failed to save initial statuses: %s", err.Error())
  855. }
  856. // Functionality being tested
  857. icd.pullWatchers()
  858. // Test Result
  859. status, err := icd.load()
  860. if err != nil {
  861. t.Errorf("failed to load status file: %s", err.Error())
  862. }
  863. err = checkStatuses(status, expectedStatuses)
  864. if err != nil {
  865. t.Errorf("statuses equality check failed: %s", err.Error())
  866. }
  867. })
  868. }
  869. }
  870. func TestIntegrationController_CreateConfig(t *testing.T) {
  871. testCases := map[string]struct {
  872. initial []*Status
  873. expected []*Status
  874. input cloudconfig.KeyedConfig
  875. expectErr bool
  876. }{
  877. "Invalid Config": {
  878. initial: nil,
  879. expected: nil,
  880. input: invalidAthenaConf,
  881. expectErr: true,
  882. },
  883. "config exists from this source": {
  884. initial: []*Status{
  885. makeStatus(validAthenaConf, true, ConfigControllerSource),
  886. },
  887. expected: []*Status{
  888. makeStatus(validAthenaConf, true, ConfigControllerSource),
  889. },
  890. input: validAthenaConf,
  891. expectErr: true,
  892. },
  893. "config exists from this source altered": {
  894. initial: []*Status{
  895. makeStatus(validAthenaConf, true, ConfigControllerSource),
  896. },
  897. expected: []*Status{
  898. makeStatus(validAthenaConf, true, ConfigControllerSource),
  899. },
  900. input: validAthenaConfModifiedProperty,
  901. expectErr: true,
  902. },
  903. "config exists from other source enabled": {
  904. initial: []*Status{
  905. makeStatus(validAthenaConf, true, MultiCloudSource),
  906. },
  907. expected: []*Status{
  908. makeStatus(validAthenaConf, false, MultiCloudSource),
  909. makeStatus(validAthenaConf, true, ConfigControllerSource),
  910. },
  911. input: validAthenaConf,
  912. expectErr: false,
  913. },
  914. "config exists from other source disabled": {
  915. initial: []*Status{
  916. makeStatus(validAthenaConf, false, MultiCloudSource),
  917. },
  918. expected: []*Status{
  919. makeStatus(validAthenaConf, false, MultiCloudSource),
  920. makeStatus(validAthenaConf, true, ConfigControllerSource),
  921. },
  922. input: validAthenaConf,
  923. expectErr: false,
  924. },
  925. "config into empty": {
  926. initial: []*Status{},
  927. expected: []*Status{
  928. makeStatus(validAthenaConf, true, ConfigControllerSource),
  929. },
  930. input: validAthenaConf,
  931. expectErr: false,
  932. },
  933. }
  934. for name, tc := range testCases {
  935. t.Run(name, func(t *testing.T) {
  936. // Test set up and validation
  937. initialStatuses, err := buildStatuses(tc.initial)
  938. if err != nil {
  939. t.Errorf("initial statuses: %s", err.Error())
  940. }
  941. expectedStatuses, err := buildStatuses(tc.expected)
  942. if err != nil {
  943. t.Errorf("initial statuses: %s", err.Error())
  944. }
  945. tempDir := os.TempDir()
  946. path := filepath.Join(tempDir, configFile)
  947. defer os.Remove(path)
  948. // Initialize controller
  949. icd := &Controller{
  950. path: path,
  951. }
  952. err = icd.save(initialStatuses)
  953. if err != nil {
  954. t.Errorf("failed to save initial statuses: %s", err.Error())
  955. }
  956. // Functionality being tested
  957. err = icd.CreateConfig(tc.input)
  958. // Test Result
  959. if err != nil && !tc.expectErr {
  960. t.Errorf("unexpected error when creating config: %s", err.Error())
  961. }
  962. if err == nil && tc.expectErr {
  963. t.Errorf("no error where expect")
  964. }
  965. status, err := icd.load()
  966. if err != nil {
  967. t.Errorf("failed to load status file: %s", err.Error())
  968. }
  969. err = checkStatuses(status, expectedStatuses)
  970. if err != nil {
  971. t.Errorf("statuses equality check failed: %s", err.Error())
  972. }
  973. })
  974. }
  975. }
  976. func TestIntegrationController_EnableConfig(t *testing.T) {
  977. testCases := map[string]struct {
  978. initial []*Status
  979. expected []*Status
  980. inputKey string
  981. inputSource string
  982. expectErr bool
  983. }{
  984. "config doesn't exist": {
  985. initial: []*Status{},
  986. expected: []*Status{},
  987. inputKey: validAthenaConf.Key(),
  988. inputSource: ConfigControllerSource.String(),
  989. expectErr: true,
  990. },
  991. "config is already enabled": {
  992. initial: []*Status{
  993. makeStatus(validAthenaConf, true, ConfigControllerSource),
  994. },
  995. expected: []*Status{
  996. makeStatus(validAthenaConf, true, ConfigControllerSource),
  997. },
  998. inputKey: validAthenaConf.Key(),
  999. inputSource: ConfigControllerSource.String(),
  1000. expectErr: true,
  1001. },
  1002. "alternate source": {
  1003. initial: []*Status{
  1004. makeStatus(validAthenaConf, false, MultiCloudSource),
  1005. },
  1006. expected: []*Status{
  1007. makeStatus(validAthenaConf, true, MultiCloudSource),
  1008. },
  1009. inputKey: validAthenaConf.Key(),
  1010. inputSource: MultiCloudSource.String(),
  1011. expectErr: false,
  1012. },
  1013. "enabled disabled single config": {
  1014. initial: []*Status{
  1015. makeStatus(validAthenaConf, false, ConfigControllerSource),
  1016. },
  1017. expected: []*Status{
  1018. makeStatus(validAthenaConf, true, ConfigControllerSource),
  1019. },
  1020. inputKey: validAthenaConf.Key(),
  1021. inputSource: ConfigControllerSource.String(),
  1022. expectErr: false,
  1023. },
  1024. "enable config which is enabled by another source": {
  1025. initial: []*Status{
  1026. makeStatus(validAthenaConf, false, ConfigControllerSource),
  1027. makeStatus(validAthenaConf, true, MultiCloudSource),
  1028. },
  1029. expected: []*Status{
  1030. makeStatus(validAthenaConf, true, ConfigControllerSource),
  1031. makeStatus(validAthenaConf, false, MultiCloudSource),
  1032. },
  1033. inputKey: validAthenaConf.Key(),
  1034. inputSource: ConfigControllerSource.String(),
  1035. expectErr: false,
  1036. },
  1037. }
  1038. for name, tc := range testCases {
  1039. t.Run(name, func(t *testing.T) {
  1040. // Test set up and validation
  1041. initialStatuses, err := buildStatuses(tc.initial)
  1042. if err != nil {
  1043. t.Errorf("initial statuses: %s", err.Error())
  1044. }
  1045. expectedStatuses, err := buildStatuses(tc.expected)
  1046. if err != nil {
  1047. t.Errorf("initial statuses: %s", err.Error())
  1048. }
  1049. tempDir := os.TempDir()
  1050. path := filepath.Join(tempDir, configFile)
  1051. defer os.Remove(path)
  1052. // Initialize controller
  1053. icd := &Controller{
  1054. path: path,
  1055. }
  1056. err = icd.save(initialStatuses)
  1057. if err != nil {
  1058. t.Errorf("failed to save initial statuses: %s", err.Error())
  1059. }
  1060. // Functionality being tested
  1061. err = icd.EnableConfig(tc.inputKey, tc.inputSource)
  1062. // Test Result
  1063. if err != nil && !tc.expectErr {
  1064. t.Errorf("unexpected error when enabling config: %s", err.Error())
  1065. }
  1066. if err == nil && tc.expectErr {
  1067. t.Errorf("no error where expect")
  1068. }
  1069. status, err := icd.load()
  1070. if err != nil {
  1071. t.Errorf("failed to load status file: %s", err.Error())
  1072. }
  1073. err = checkStatuses(status, expectedStatuses)
  1074. if err != nil {
  1075. t.Errorf("statuses equality check failed: %s", err.Error())
  1076. }
  1077. })
  1078. }
  1079. }
  1080. func TestIntegrationController_DisableConfig(t *testing.T) {
  1081. testCases := map[string]struct {
  1082. initial []*Status
  1083. expected []*Status
  1084. inputKey string
  1085. inputSource string
  1086. expectErr bool
  1087. }{
  1088. "config doesn't exist": {
  1089. initial: []*Status{},
  1090. expected: []*Status{},
  1091. inputKey: validAthenaConf.Key(),
  1092. inputSource: ConfigControllerSource.String(),
  1093. expectErr: true,
  1094. },
  1095. "config is already disabled": {
  1096. initial: []*Status{
  1097. makeStatus(validAthenaConf, false, ConfigControllerSource),
  1098. makeStatus(validAthenaConf, true, MultiCloudSource),
  1099. },
  1100. expected: []*Status{
  1101. makeStatus(validAthenaConf, false, ConfigControllerSource),
  1102. makeStatus(validAthenaConf, true, MultiCloudSource),
  1103. },
  1104. inputKey: validAthenaConf.Key(),
  1105. inputSource: ConfigControllerSource.String(),
  1106. expectErr: true,
  1107. },
  1108. "disable single config": {
  1109. initial: []*Status{
  1110. makeStatus(validAthenaConf, true, ConfigControllerSource),
  1111. },
  1112. expected: []*Status{
  1113. makeStatus(validAthenaConf, false, ConfigControllerSource),
  1114. },
  1115. inputKey: validAthenaConf.Key(),
  1116. inputSource: ConfigControllerSource.String(),
  1117. expectErr: false,
  1118. },
  1119. "alternate source": {
  1120. initial: []*Status{
  1121. makeStatus(validAthenaConf, true, MultiCloudSource),
  1122. },
  1123. expected: []*Status{
  1124. makeStatus(validAthenaConf, false, MultiCloudSource),
  1125. },
  1126. inputKey: validAthenaConf.Key(),
  1127. inputSource: MultiCloudSource.String(),
  1128. expectErr: false,
  1129. },
  1130. "disable config, matching config from separate source": {
  1131. initial: []*Status{
  1132. makeStatus(validAthenaConf, true, ConfigControllerSource),
  1133. makeStatus(validAthenaConf, false, MultiCloudSource),
  1134. },
  1135. expected: []*Status{
  1136. makeStatus(validAthenaConf, false, ConfigControllerSource),
  1137. makeStatus(validAthenaConf, false, MultiCloudSource),
  1138. },
  1139. inputKey: validAthenaConf.Key(),
  1140. inputSource: ConfigControllerSource.String(),
  1141. expectErr: false,
  1142. },
  1143. }
  1144. for name, tc := range testCases {
  1145. t.Run(name, func(t *testing.T) {
  1146. // Test set up and validation
  1147. initialStatuses, err := buildStatuses(tc.initial)
  1148. if err != nil {
  1149. t.Errorf("initial statuses: %s", err.Error())
  1150. }
  1151. expectedStatuses, err := buildStatuses(tc.expected)
  1152. if err != nil {
  1153. t.Errorf("initial statuses: %s", err.Error())
  1154. }
  1155. tempDir := os.TempDir()
  1156. path := filepath.Join(tempDir, configFile)
  1157. defer os.Remove(path)
  1158. // Initialize controller
  1159. icd := &Controller{
  1160. path: path,
  1161. }
  1162. err = icd.save(initialStatuses)
  1163. if err != nil {
  1164. t.Errorf("failed to save initial statuses: %s", err.Error())
  1165. }
  1166. // Functionality being tested
  1167. err = icd.DisableConfig(tc.inputKey, tc.inputSource)
  1168. // Test Result
  1169. if err != nil && !tc.expectErr {
  1170. t.Errorf("unexpected error when disabling config: %s", err.Error())
  1171. }
  1172. if err == nil && tc.expectErr {
  1173. t.Errorf("no error where expect")
  1174. }
  1175. status, err := icd.load()
  1176. if err != nil {
  1177. t.Errorf("failed to load status file: %s", err.Error())
  1178. }
  1179. err = checkStatuses(status, expectedStatuses)
  1180. if err != nil {
  1181. t.Errorf("statuses equality check failed: %s", err.Error())
  1182. }
  1183. })
  1184. }
  1185. }
  1186. func TestIntegrationController_DeleteConfig(t *testing.T) {
  1187. testCases := map[string]struct {
  1188. initial []*Status
  1189. expected []*Status
  1190. inputKey string
  1191. inputSource string
  1192. expectErr bool
  1193. }{
  1194. "config doesn't exist": {
  1195. initial: []*Status{},
  1196. expected: []*Status{},
  1197. inputKey: validAthenaConf.Key(),
  1198. inputSource: ConfigControllerSource.String(),
  1199. expectErr: true,
  1200. },
  1201. "invalid source": {
  1202. initial: []*Status{},
  1203. expected: []*Status{},
  1204. inputKey: validAthenaConf.Key(),
  1205. inputSource: MultiCloudSource.String(),
  1206. expectErr: true,
  1207. },
  1208. "delete single config": {
  1209. initial: []*Status{
  1210. makeStatus(validAthenaConf, true, ConfigControllerSource),
  1211. },
  1212. expected: []*Status{},
  1213. inputKey: validAthenaConf.Key(),
  1214. inputSource: ConfigControllerSource.String(),
  1215. expectErr: false,
  1216. },
  1217. "disable config, matching config from separate source": {
  1218. initial: []*Status{
  1219. makeStatus(validAthenaConf, true, ConfigControllerSource),
  1220. makeStatus(validAthenaConf, false, MultiCloudSource),
  1221. },
  1222. expected: []*Status{
  1223. makeStatus(validAthenaConf, false, MultiCloudSource),
  1224. },
  1225. inputKey: validAthenaConf.Key(),
  1226. inputSource: ConfigControllerSource.String(),
  1227. expectErr: false,
  1228. },
  1229. }
  1230. for name, tc := range testCases {
  1231. t.Run(name, func(t *testing.T) {
  1232. // Test set up and validation
  1233. initialStatuses, err := buildStatuses(tc.initial)
  1234. if err != nil {
  1235. t.Errorf("initial statuses: %s", err.Error())
  1236. }
  1237. expectedStatuses, err := buildStatuses(tc.expected)
  1238. if err != nil {
  1239. t.Errorf("initial statuses: %s", err.Error())
  1240. }
  1241. tempDir := os.TempDir()
  1242. path := filepath.Join(tempDir, configFile)
  1243. defer os.Remove(path)
  1244. // Initialize controller
  1245. icd := &Controller{
  1246. path: path,
  1247. }
  1248. err = icd.save(initialStatuses)
  1249. if err != nil {
  1250. t.Errorf("failed to save initial statuses: %s", err.Error())
  1251. }
  1252. // Functionality being tested
  1253. err = icd.DeleteConfig(tc.inputKey, tc.inputSource)
  1254. // Test Result
  1255. if err != nil && !tc.expectErr {
  1256. t.Errorf("unexpected error when deleting config: %s", err.Error())
  1257. }
  1258. if err == nil && tc.expectErr {
  1259. t.Errorf("no error where expect")
  1260. }
  1261. status, err := icd.load()
  1262. if err != nil {
  1263. t.Errorf("failed to load status file: %s", err.Error())
  1264. }
  1265. err = checkStatuses(status, expectedStatuses)
  1266. if err != nil {
  1267. t.Errorf("statuses equality check failed: %s", err.Error())
  1268. }
  1269. })
  1270. }
  1271. }
  1272. func makeStatus(config cloudconfig.KeyedConfig, active bool, source ConfigSource) *Status {
  1273. err := config.Validate()
  1274. valid := err == nil
  1275. configType, err := ConfigTypeFromConfig(config)
  1276. if err != nil {
  1277. panic(fmt.Errorf("config type not recognised: %w", err))
  1278. }
  1279. return &Status{
  1280. Source: source,
  1281. Key: config.Key(),
  1282. Active: active,
  1283. Valid: valid,
  1284. ConfigType: configType,
  1285. Config: config,
  1286. }
  1287. }
  1288. func buildStatuses(statusList []*Status) (Statuses, error) {
  1289. statuses := Statuses{}
  1290. for _, status := range statusList {
  1291. if _, ok := statuses.Get(status.Key, status.Source); ok {
  1292. return nil, fmt.Errorf("invalid test, duplicate status with key: %s source: %s", status.Key, status.Source.String())
  1293. }
  1294. statuses.Insert(status)
  1295. }
  1296. return statuses, nil
  1297. }
  1298. func checkStatuses(actual, expected Statuses) error {
  1299. if len(actual.List()) != len(expected.List()) {
  1300. return fmt.Errorf("integration statueses did not have the correct length actaul: %d, expected: %d", len(actual.List()), len(expected.List()))
  1301. }
  1302. for _, actualStatus := range actual.List() {
  1303. expectedStatus, ok := expected.Get(actualStatus.Key, actualStatus.Source)
  1304. if !ok {
  1305. return fmt.Errorf("expected integration statuses is missing with integration key: %s, source: %s", actualStatus.Key, actualStatus.Source.String())
  1306. }
  1307. // failure here indicates an issue with the configID
  1308. if actualStatus.Key != expectedStatus.Key {
  1309. return fmt.Errorf("integration status does not have the correct Key values actual: %s, expected: %s", actualStatus.Key, expectedStatus.Key)
  1310. }
  1311. // failure here indicates an issue with the configID
  1312. if actualStatus.Key != expectedStatus.Key {
  1313. return fmt.Errorf("integration status does not have the correct Source values actual: %s, expected: %s", actualStatus.Source, expectedStatus.Source)
  1314. }
  1315. if actualStatus.Active != expectedStatus.Active {
  1316. return fmt.Errorf("integration status does not have the correct Active values actual: %v, expected: %v", actualStatus.Active, expectedStatus.Active)
  1317. }
  1318. if actualStatus.Valid != expectedStatus.Valid {
  1319. return fmt.Errorf("integration status does not have the correct Valid values actual: %v, expected: %v", actualStatus.Valid, expectedStatus.Valid)
  1320. }
  1321. if !actualStatus.Config.Equals(expectedStatus.Config) {
  1322. return fmt.Errorf("integration status does not have the correct config values actual: %v, expected: %v", actualStatus.Config, expectedStatus.Config)
  1323. }
  1324. }
  1325. return nil
  1326. }