keyvault_dual_sdk_test.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538
  1. /*
  2. Copyright © The ESO Authors
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. https://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package keyvault
  14. import (
  15. "context"
  16. "testing"
  17. corev1 "k8s.io/api/core/v1"
  18. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  19. "k8s.io/utils/ptr"
  20. "sigs.k8s.io/controller-runtime/pkg/client/fake"
  21. esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
  22. v1 "github.com/external-secrets/external-secrets/apis/meta/v1"
  23. )
  24. // TestFeatureFlagRouting tests that the UseAzureSDK feature flag correctly routes to the appropriate implementation.
  25. func TestFeatureFlagRouting(t *testing.T) {
  26. testCases := []struct {
  27. name string
  28. useAzureSDK *bool
  29. expectNewSDK bool
  30. description string
  31. }{
  32. {
  33. name: "default_legacy_sdk",
  34. useAzureSDK: nil,
  35. expectNewSDK: false,
  36. description: "When UseAzureSDK is nil (default), should use legacy SDK",
  37. },
  38. {
  39. name: "explicit_legacy_sdk",
  40. useAzureSDK: new(false),
  41. expectNewSDK: false,
  42. description: "When UseAzureSDK is explicitly false, should use legacy SDK",
  43. },
  44. {
  45. name: "explicit_new_sdk",
  46. useAzureSDK: new(true),
  47. expectNewSDK: true,
  48. description: "When UseAzureSDK is true, should use new SDK",
  49. },
  50. }
  51. for _, tc := range testCases {
  52. t.Run(tc.name, func(t *testing.T) {
  53. // Create test provider with the specified feature flag
  54. provider := &esv1.AzureKVProvider{
  55. VaultURL: new("https://test-vault.vault.azure.net/"),
  56. TenantID: new("test-tenant"),
  57. AuthType: ptr.To(esv1.AzureServicePrincipal),
  58. UseAzureSDK: tc.useAzureSDK,
  59. AuthSecretRef: &esv1.AzureKVAuth{
  60. ClientID: &v1.SecretKeySelector{
  61. Name: "test-secret",
  62. Key: "client-id",
  63. },
  64. ClientSecret: &v1.SecretKeySelector{
  65. Name: "test-secret",
  66. Key: "client-secret",
  67. },
  68. },
  69. }
  70. // Create Azure client
  71. azure := &Azure{
  72. provider: provider,
  73. }
  74. // Test the useNewSDK() method
  75. result := azure.useNewSDK()
  76. if result != tc.expectNewSDK {
  77. t.Errorf("Expected useNewSDK() to return %v for %s, got %v", tc.expectNewSDK, tc.description, result)
  78. }
  79. })
  80. }
  81. }
  82. // TestClientInitialization tests that both client initialization paths work correctly.
  83. func TestClientInitialization(t *testing.T) {
  84. // Create test secret with credentials
  85. secret := &corev1.Secret{
  86. ObjectMeta: metav1.ObjectMeta{
  87. Name: "test-secret",
  88. Namespace: "test-namespace",
  89. },
  90. Data: map[string][]byte{
  91. "client-id": []byte("test-client-id"),
  92. "client-secret": []byte("test-client-secret"),
  93. },
  94. }
  95. fakeClient := fake.NewClientBuilder().WithObjects(secret).Build()
  96. testCases := []struct {
  97. name string
  98. useAzureSDK *bool
  99. expectedErrorPrefix string
  100. description string
  101. }{
  102. {
  103. name: "legacy_client_init",
  104. useAzureSDK: new(false),
  105. expectedErrorPrefix: "", // May succeed or fail with auth errors, but should not panic
  106. description: "Legacy client initialization should not panic",
  107. },
  108. {
  109. name: "new_sdk_client_init",
  110. useAzureSDK: new(true),
  111. expectedErrorPrefix: "", // May succeed or fail with auth errors, but should not panic
  112. description: "New SDK client initialization should not panic",
  113. },
  114. }
  115. for _, tc := range testCases {
  116. t.Run(tc.name, func(t *testing.T) {
  117. provider := &esv1.AzureKVProvider{
  118. VaultURL: new("https://test-vault.vault.azure.net/"),
  119. TenantID: new("test-tenant"),
  120. AuthType: ptr.To(esv1.AzureServicePrincipal),
  121. UseAzureSDK: tc.useAzureSDK,
  122. AuthSecretRef: &esv1.AzureKVAuth{
  123. ClientID: &v1.SecretKeySelector{
  124. Name: "test-secret",
  125. Key: "client-id",
  126. },
  127. ClientSecret: &v1.SecretKeySelector{
  128. Name: "test-secret",
  129. Key: "client-secret",
  130. },
  131. },
  132. }
  133. store := &esv1.SecretStore{
  134. ObjectMeta: metav1.ObjectMeta{
  135. Name: "test-store",
  136. Namespace: "test-namespace",
  137. },
  138. Spec: esv1.SecretStoreSpec{
  139. Provider: &esv1.SecretStoreProvider{
  140. AzureKV: provider,
  141. },
  142. },
  143. }
  144. // Test that client initialization doesn't panic
  145. defer func() {
  146. if r := recover(); r != nil {
  147. t.Errorf("Client initialization panicked for %s: %v", tc.description, r)
  148. }
  149. }()
  150. azure := &Azure{}
  151. _, err := azure.NewClient(context.Background(), store, fakeClient, "test-namespace")
  152. // We expect errors due to authentication issues in tests, but no panics
  153. // The important thing is that the code paths are exercised without crashing
  154. if err != nil {
  155. t.Logf("Expected auth error for %s: %v", tc.description, err)
  156. }
  157. })
  158. }
  159. }
  160. // TestConfigurationValidation tests that the feature flag is properly validated and accepted.
  161. func TestConfigurationValidation(t *testing.T) {
  162. testCases := []struct {
  163. name string
  164. useAzureSDK *bool
  165. expectValid bool
  166. description string
  167. }{
  168. {
  169. name: "nil_feature_flag",
  170. useAzureSDK: nil,
  171. expectValid: true,
  172. description: "Nil feature flag should be valid (defaults to legacy)",
  173. },
  174. {
  175. name: "false_feature_flag",
  176. useAzureSDK: new(false),
  177. expectValid: true,
  178. description: "False feature flag should be valid (legacy SDK)",
  179. },
  180. {
  181. name: "true_feature_flag",
  182. useAzureSDK: new(true),
  183. expectValid: true,
  184. description: "True feature flag should be valid (new SDK)",
  185. },
  186. }
  187. for _, tc := range testCases {
  188. t.Run(tc.name, func(t *testing.T) {
  189. provider := &esv1.AzureKVProvider{
  190. VaultURL: new("https://test-vault.vault.azure.net/"),
  191. TenantID: new("test-tenant"),
  192. AuthType: ptr.To(esv1.AzureServicePrincipal),
  193. UseAzureSDK: tc.useAzureSDK,
  194. AuthSecretRef: &esv1.AzureKVAuth{
  195. ClientID: &v1.SecretKeySelector{
  196. Name: "test-secret",
  197. Key: "client-id",
  198. },
  199. ClientSecret: &v1.SecretKeySelector{
  200. Name: "test-secret",
  201. Key: "client-secret",
  202. },
  203. },
  204. }
  205. store := &esv1.SecretStore{
  206. Spec: esv1.SecretStoreSpec{
  207. Provider: &esv1.SecretStoreProvider{
  208. AzureKV: provider,
  209. },
  210. },
  211. }
  212. azure := &Azure{}
  213. warnings, err := azure.ValidateStore(store)
  214. if tc.expectValid {
  215. if err != nil {
  216. t.Errorf("Expected validation to pass for %s, got error: %v", tc.description, err)
  217. }
  218. if len(warnings) > 0 {
  219. t.Logf("Validation warnings for %s: %v", tc.description, warnings)
  220. }
  221. } else if err == nil {
  222. t.Errorf("Expected validation to fail for %s, but it passed", tc.description)
  223. }
  224. })
  225. }
  226. }
  227. // TestBackwardCompatibility ensures that existing configurations continue to work.
  228. func TestBackwardCompatibility(t *testing.T) {
  229. // Test that existing configurations without UseAzureSDK still work
  230. provider := &esv1.AzureKVProvider{
  231. VaultURL: new("https://test-vault.vault.azure.net/"),
  232. TenantID: new("test-tenant"),
  233. AuthType: ptr.To(esv1.AzureServicePrincipal),
  234. // UseAzureSDK intentionally omitted to test backward compatibility
  235. AuthSecretRef: &esv1.AzureKVAuth{
  236. ClientID: &v1.SecretKeySelector{
  237. Name: "test-secret",
  238. Key: "client-id",
  239. },
  240. ClientSecret: &v1.SecretKeySelector{
  241. Name: "test-secret",
  242. Key: "client-secret",
  243. },
  244. },
  245. }
  246. azure := &Azure{
  247. provider: provider,
  248. }
  249. // Should default to legacy SDK (false)
  250. if azure.useNewSDK() {
  251. t.Error("Expected backward compatibility: nil UseAzureSDK should default to legacy SDK (false)")
  252. }
  253. // Validation should still pass
  254. store := &esv1.SecretStore{
  255. Spec: esv1.SecretStoreSpec{
  256. Provider: &esv1.SecretStoreProvider{
  257. AzureKV: provider,
  258. },
  259. },
  260. }
  261. warnings, err := azure.ValidateStore(store)
  262. if err != nil {
  263. t.Errorf("Backward compatibility failed: existing configuration should validate, got error: %v", err)
  264. }
  265. if len(warnings) > 0 {
  266. t.Logf("Backward compatibility warnings: %v", warnings)
  267. }
  268. }
  269. // TestAzureStackCloudConfiguration tests Azure Stack Cloud configuration validation.
  270. func TestAzureStackCloudConfiguration(t *testing.T) {
  271. testCases := []struct {
  272. name string
  273. useAzureSDK *bool
  274. envType esv1.AzureEnvironmentType
  275. customConfig *esv1.AzureCustomCloudConfig
  276. expectError bool
  277. expectedError string
  278. description string
  279. }{
  280. {
  281. name: "azure_stack_with_new_sdk_and_config",
  282. useAzureSDK: new(true),
  283. envType: esv1.AzureEnvironmentAzureStackCloud,
  284. customConfig: &esv1.AzureCustomCloudConfig{
  285. ActiveDirectoryEndpoint: "https://login.microsoftonline.com/",
  286. KeyVaultEndpoint: new("https://vault.local.azurestack.external/"),
  287. KeyVaultDNSSuffix: new(".vault.local.azurestack.external"),
  288. },
  289. expectError: false,
  290. description: "Azure Stack with new SDK and custom config should be valid",
  291. },
  292. {
  293. name: "azure_stack_without_custom_config",
  294. useAzureSDK: new(true),
  295. envType: esv1.AzureEnvironmentAzureStackCloud,
  296. customConfig: nil,
  297. expectError: true,
  298. expectedError: "CustomCloudConfig is required when EnvironmentType is AzureStackCloud",
  299. description: "Azure Stack without custom config should fail",
  300. },
  301. {
  302. name: "azure_stack_with_legacy_sdk",
  303. useAzureSDK: new(false),
  304. envType: esv1.AzureEnvironmentAzureStackCloud,
  305. customConfig: &esv1.AzureCustomCloudConfig{
  306. ActiveDirectoryEndpoint: "https://login.microsoftonline.com/",
  307. },
  308. expectError: true,
  309. expectedError: "CustomCloudConfig requires UseAzureSDK to be set to true - the legacy SDK does not support custom clouds",
  310. description: "Azure Stack with legacy SDK should fail",
  311. },
  312. {
  313. name: "azure_stack_without_new_sdk_flag",
  314. useAzureSDK: nil, // defaults to false
  315. envType: esv1.AzureEnvironmentAzureStackCloud,
  316. customConfig: &esv1.AzureCustomCloudConfig{
  317. ActiveDirectoryEndpoint: "https://login.microsoftonline.com/",
  318. },
  319. expectError: true,
  320. expectedError: "CustomCloudConfig requires UseAzureSDK to be set to true - the legacy SDK does not support custom clouds",
  321. description: "Azure Stack without explicit new SDK flag should fail",
  322. },
  323. {
  324. name: "azure_stack_missing_aad_endpoint",
  325. useAzureSDK: new(true),
  326. envType: esv1.AzureEnvironmentAzureStackCloud,
  327. customConfig: &esv1.AzureCustomCloudConfig{
  328. KeyVaultEndpoint: new("https://vault.custom.cloud/"),
  329. },
  330. expectError: true,
  331. expectedError: "activeDirectoryEndpoint is required in CustomCloudConfig",
  332. description: "Azure Stack without AAD endpoint should fail",
  333. },
  334. {
  335. name: "custom_config_with_china_cloud",
  336. useAzureSDK: new(true),
  337. envType: esv1.AzureEnvironmentChinaCloud,
  338. customConfig: &esv1.AzureCustomCloudConfig{
  339. ActiveDirectoryEndpoint: "https://login.partner.microsoftonline.cn/",
  340. },
  341. expectError: false,
  342. description: "Custom config with ChinaCloud environment should be allowed for AKS Workload Identity",
  343. },
  344. {
  345. name: "public_cloud_without_custom_config",
  346. useAzureSDK: new(true),
  347. envType: esv1.AzureEnvironmentPublicCloud,
  348. customConfig: nil,
  349. expectError: false,
  350. description: "Public cloud without custom config should be valid",
  351. },
  352. }
  353. for _, tc := range testCases {
  354. t.Run(tc.name, func(t *testing.T) {
  355. provider := &esv1.AzureKVProvider{
  356. VaultURL: new("https://test-vault.vault.azure.net/"),
  357. TenantID: new("test-tenant"),
  358. AuthType: ptr.To(esv1.AzureServicePrincipal),
  359. UseAzureSDK: tc.useAzureSDK,
  360. EnvironmentType: tc.envType,
  361. CustomCloudConfig: tc.customConfig,
  362. AuthSecretRef: &esv1.AzureKVAuth{
  363. ClientID: &v1.SecretKeySelector{
  364. Name: "test-secret",
  365. Key: "client-id",
  366. },
  367. ClientSecret: &v1.SecretKeySelector{
  368. Name: "test-secret",
  369. Key: "client-secret",
  370. },
  371. },
  372. }
  373. azure := &Azure{provider: provider}
  374. store := &esv1.SecretStore{
  375. Spec: esv1.SecretStoreSpec{
  376. Provider: &esv1.SecretStoreProvider{
  377. AzureKV: provider,
  378. },
  379. },
  380. }
  381. warnings, err := azure.ValidateStore(store)
  382. if tc.expectError {
  383. if err == nil {
  384. t.Errorf("Expected validation to fail for %s, but it succeeded", tc.description)
  385. } else if tc.expectedError != "" && err.Error() != tc.expectedError {
  386. t.Errorf("Expected error message '%s' for %s, got: %v", tc.expectedError, tc.description, err)
  387. }
  388. } else {
  389. if err != nil {
  390. t.Errorf("Expected validation to succeed for %s, but got error: %v", tc.description, err)
  391. }
  392. }
  393. if len(warnings) > 0 {
  394. t.Logf("Warnings for %s: %v", tc.name, warnings)
  395. }
  396. })
  397. }
  398. }
  399. // TestGetCloudConfiguration tests the cloud configuration resolution.
  400. func TestGetCloudConfiguration(t *testing.T) {
  401. testCases := []struct {
  402. name string
  403. provider *esv1.AzureKVProvider
  404. expectError bool
  405. expectedError string
  406. description string
  407. }{
  408. {
  409. name: "public_cloud",
  410. provider: &esv1.AzureKVProvider{
  411. EnvironmentType: esv1.AzureEnvironmentPublicCloud,
  412. },
  413. expectError: false,
  414. description: "Public cloud should return valid configuration",
  415. },
  416. {
  417. name: "us_government_cloud",
  418. provider: &esv1.AzureKVProvider{
  419. EnvironmentType: esv1.AzureEnvironmentUSGovernmentCloud,
  420. },
  421. expectError: false,
  422. description: "US Government cloud should return valid configuration",
  423. },
  424. {
  425. name: "china_cloud",
  426. provider: &esv1.AzureKVProvider{
  427. EnvironmentType: esv1.AzureEnvironmentChinaCloud,
  428. },
  429. expectError: false,
  430. description: "China cloud should return valid configuration",
  431. },
  432. {
  433. name: "china_cloud_with_custom_endpoint",
  434. provider: &esv1.AzureKVProvider{
  435. EnvironmentType: esv1.AzureEnvironmentChinaCloud,
  436. UseAzureSDK: new(true),
  437. CustomCloudConfig: &esv1.AzureCustomCloudConfig{
  438. ActiveDirectoryEndpoint: "https://login.partner.microsoftonline.cn/",
  439. KeyVaultEndpoint: new("https://vault.azure.cn/"),
  440. ResourceManagerEndpoint: new("https://management.chinacloudapi.cn/"),
  441. },
  442. },
  443. expectError: false,
  444. description: "China cloud with custom AAD endpoint for AKS Workload Identity should work",
  445. },
  446. {
  447. name: "azure_stack_with_config",
  448. provider: &esv1.AzureKVProvider{
  449. EnvironmentType: esv1.AzureEnvironmentAzureStackCloud,
  450. UseAzureSDK: new(true),
  451. CustomCloudConfig: &esv1.AzureCustomCloudConfig{
  452. ActiveDirectoryEndpoint: "https://login.local.azurestack.external/",
  453. KeyVaultEndpoint: new("https://vault.local.azurestack.external/"),
  454. },
  455. },
  456. expectError: false,
  457. description: "Azure Stack with valid config should return custom configuration",
  458. },
  459. {
  460. name: "azure_stack_without_new_sdk",
  461. provider: &esv1.AzureKVProvider{
  462. EnvironmentType: esv1.AzureEnvironmentAzureStackCloud,
  463. UseAzureSDK: new(false),
  464. CustomCloudConfig: &esv1.AzureCustomCloudConfig{
  465. ActiveDirectoryEndpoint: "https://login.local.azurestack.external/",
  466. },
  467. },
  468. expectError: true,
  469. expectedError: "CustomCloudConfig requires UseAzureSDK to be set to true",
  470. description: "Azure Stack without new SDK should fail",
  471. },
  472. {
  473. name: "azure_stack_without_config",
  474. provider: &esv1.AzureKVProvider{
  475. EnvironmentType: esv1.AzureEnvironmentAzureStackCloud,
  476. UseAzureSDK: new(true),
  477. },
  478. expectError: true,
  479. expectedError: "CustomCloudConfig is required when EnvironmentType is AzureStackCloud",
  480. description: "Azure Stack without custom config should fail",
  481. },
  482. }
  483. for _, tc := range testCases {
  484. t.Run(tc.name, func(t *testing.T) {
  485. config, err := getCloudConfiguration(tc.provider)
  486. if tc.expectError {
  487. if err == nil {
  488. t.Errorf("Expected error for %s, but got none", tc.description)
  489. } else if tc.expectedError != "" && err.Error() != tc.expectedError {
  490. t.Errorf("Expected error '%s' for %s, got: %v", tc.expectedError, tc.description, err)
  491. }
  492. } else {
  493. if err != nil {
  494. t.Errorf("Expected no error for %s, but got: %v", tc.description, err)
  495. }
  496. if config.ActiveDirectoryAuthorityHost == "" && tc.provider.EnvironmentType != esv1.AzureEnvironmentAzureStackCloud {
  497. // For predefined clouds, we should have a valid config
  498. t.Errorf("Expected valid cloud configuration for %s", tc.description)
  499. }
  500. }
  501. })
  502. }
  503. }