github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/caas/kubernetes/clientconfig/k8s_test.go (about) 1 // Copyright 2017 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package clientconfig_test 5 6 import ( 7 "os" 8 "path/filepath" 9 "strings" 10 "text/template" 11 12 jc "github.com/juju/testing/checkers" 13 gc "gopkg.in/check.v1" 14 "k8s.io/client-go/tools/clientcmd" 15 16 "github.com/juju/juju/caas/kubernetes/clientconfig" 17 "github.com/juju/juju/cloud" 18 coretesting "github.com/juju/juju/testing" 19 ) 20 21 type k8sConfigSuite struct { 22 coretesting.FakeJujuXDGDataHomeSuite 23 dir string 24 } 25 26 var _ = gc.Suite(&k8sConfigSuite{}) 27 28 var ( 29 prefixConfigYAML = ` 30 apiVersion: v1 31 kind: Config 32 clusters: 33 - cluster: 34 server: https://1.1.1.1:8888 35 certificate-authority-data: QQ== 36 name: the-cluster 37 contexts: 38 - context: 39 cluster: the-cluster 40 user: the-user 41 name: the-context 42 current-context: the-context 43 preferences: {} 44 users: 45 ` 46 emptyConfigYAML = ` 47 apiVersion: v1 48 kind: Config 49 clusters: [] 50 contexts: [] 51 current-context: "" 52 preferences: {} 53 users: [] 54 ` 55 56 singleConfigYAML = prefixConfigYAML + ` 57 - name: the-user 58 user: 59 password: thepassword 60 username: theuser 61 ` 62 63 multiConfigYAML = ` 64 apiVersion: v1 65 kind: Config 66 clusters: 67 - cluster: 68 server: https://1.1.1.1:8888 69 certificate-authority-data: QQ== 70 name: the-cluster 71 - cluster: 72 server: https://10.10.10.10:1010 73 name: default-cluster 74 - cluster: 75 server: https://100.100.100.100:1010 76 certificate-authority-data: QQ== 77 name: second-cluster 78 contexts: 79 - context: 80 cluster: the-cluster 81 user: the-user 82 name: the-context 83 - context: 84 cluster: second-cluster 85 user: second-user 86 name: second-context 87 - context: 88 cluster: default-cluster 89 user: default-user 90 name: default-context 91 current-context: default-context 92 preferences: {} 93 users: 94 - name: the-user 95 user: 96 token: tokenwithcerttoken 97 - name: default-user 98 user: 99 password: defaultpassword 100 username: defaultuser 101 - name: second-user 102 user: 103 client-certificate-data: QQ== 104 client-key-data: QQ== 105 - name: third-user 106 user: 107 token: "atoken" 108 - name: fourth-user 109 user: 110 client-certificate-data: QQ== 111 client-key-data: Qg== 112 token: "tokenwithcerttoken" 113 - name: fifth-user 114 user: 115 client-certificate-data: QQ== 116 client-key-data: Qg== 117 username: "fifth-user" 118 password: "userpasscertpass" 119 ` 120 121 externalCAYAMLTemplate = ` 122 apiVersion: v1 123 kind: Config 124 clusters: 125 - cluster: 126 server: https://1.1.1.1:8888 127 certificate-authority: {{ . }} 128 name: the-cluster 129 contexts: 130 - context: 131 cluster: the-cluster 132 user: the-user 133 name: the-context 134 current-context: the-context 135 preferences: {} 136 users: 137 - name: the-user 138 user: 139 password: thepassword 140 username: theuser 141 ` 142 143 insecureTLSYAMLTemplate = ` 144 apiVersion: v1 145 kind: Config 146 clusters: 147 - cluster: 148 server: https://1.1.1.1:8888 149 insecure-skip-tls-verify: true 150 name: the-cluster 151 contexts: 152 - context: 153 cluster: the-cluster 154 user: the-user 155 name: the-context 156 current-context: the-context 157 preferences: {} 158 users: 159 - name: the-user 160 user: 161 password: thepassword 162 username: theuser 163 ` 164 ) 165 166 func (s *k8sConfigSuite) SetUpTest(c *gc.C) { 167 s.FakeJujuXDGDataHomeSuite.SetUpTest(c) 168 os.Unsetenv("HOME") 169 s.dir = c.MkDir() 170 } 171 172 // writeTempKubeConfig writes yaml to a temp file and sets the 173 // KUBECONFIG environment variable so that the clientconfig code reads 174 // it instead of the default. 175 // The caller must close and remove the returned file. 176 func (s *k8sConfigSuite) writeTempKubeConfig(c *gc.C, filename string, data string) (*os.File, error) { 177 fullpath := filepath.Join(s.dir, filename) 178 err := os.MkdirAll(filepath.Dir(fullpath), 0755) 179 if err != nil { 180 c.Fatal(err.Error()) 181 } 182 err = os.WriteFile(fullpath, []byte(data), 0644) 183 if err != nil { 184 c.Fatal(err.Error()) 185 } 186 _ = os.Setenv("KUBECONFIG", fullpath) 187 188 f, err := os.Open(fullpath) 189 return f, err 190 } 191 192 func (s *k8sConfigSuite) TestGetEmptyConfig(c *gc.C) { 193 s.assertNewK8sClientConfig(c, newK8sClientConfigTestCase{ 194 title: "get empty config", 195 configYamlContent: emptyConfigYAML, 196 configYamlFileName: "emptyConfig", 197 errMatch: `no context found for context name: "", cluster name: ""`, 198 }) 199 } 200 201 type newK8sClientConfigTestCase struct { 202 title, contextName, clusterName, configYamlContent, configYamlFileName string 203 expected *clientconfig.ClientConfig 204 errMatch string 205 } 206 207 func (s *k8sConfigSuite) assertNewK8sClientConfig(c *gc.C, testCase newK8sClientConfigTestCase) { 208 f, err := s.writeTempKubeConfig(c, testCase.configYamlFileName, testCase.configYamlContent) 209 defer f.Close() 210 c.Assert(err, jc.ErrorIsNil) 211 212 c.Logf("test: %s", testCase.title) 213 cfg, err := clientconfig.NewK8sClientConfigFromReader("", f, testCase.contextName, testCase.clusterName, nil) 214 if testCase.errMatch != "" { 215 c.Check(err, gc.ErrorMatches, testCase.errMatch) 216 } else { 217 c.Check(err, jc.ErrorIsNil) 218 c.Check(cfg, jc.DeepEquals, testCase.expected) 219 } 220 } 221 222 func (s *k8sConfigSuite) TestGetSingleConfig(c *gc.C) { 223 cred := cloud.NewNamedCredential( 224 "the-user", cloud.UserPassAuthType, 225 map[string]string{"username": "theuser", "password": "thepassword"}, false) 226 s.assertNewK8sClientConfig(c, newK8sClientConfigTestCase{ 227 title: "assert single config", 228 configYamlContent: singleConfigYAML, 229 configYamlFileName: "singleConfig", 230 expected: &clientconfig.ClientConfig{ 231 Type: "kubernetes", 232 Contexts: map[string]clientconfig.Context{ 233 "the-context": { 234 CloudName: "the-cluster", 235 CredentialName: "the-user"}}, 236 CurrentContext: "the-context", 237 Clouds: map[string]clientconfig.CloudConfig{ 238 "the-cluster": { 239 Endpoint: "https://1.1.1.1:8888", 240 Attributes: map[string]interface{}{"CAData": "A"}}}, 241 Credentials: map[string]cloud.Credential{ 242 "the-user": cred, 243 }, 244 }, 245 }) 246 } 247 248 func (s *k8sConfigSuite) TestKubeConfigPathSnapHome(c *gc.C) { 249 f, err := s.writeTempKubeConfig(c, ".kube/config", singleConfigYAML) 250 defer f.Close() 251 c.Assert(err, jc.ErrorIsNil) 252 _ = os.Unsetenv("KUBECONFIG") 253 _ = os.Setenv("SNAP_REAL_HOME", s.dir) 254 255 c.Assert(clientconfig.GetKubeConfigPath(), gc.Equals, f.Name()) 256 } 257 258 func (s *k8sConfigSuite) TestGetSingleConfigSnapHome(c *gc.C) { 259 cred := cloud.NewNamedCredential( 260 "the-user", cloud.UserPassAuthType, 261 map[string]string{"username": "theuser", "password": "thepassword"}, false) 262 f, err := s.writeTempKubeConfig(c, ".kube/config", singleConfigYAML) 263 defer f.Close() 264 c.Assert(err, jc.ErrorIsNil) 265 _ = os.Unsetenv("KUBECONFIG") 266 _ = os.Setenv("SNAP_REAL_HOME", s.dir) 267 268 cfg, err := clientconfig.NewK8sClientConfigFromReader("", nil, "", "", nil) 269 c.Check(err, jc.ErrorIsNil) 270 c.Check(cfg, jc.DeepEquals, &clientconfig.ClientConfig{ 271 Type: "kubernetes", 272 Contexts: map[string]clientconfig.Context{ 273 "the-context": { 274 CloudName: "the-cluster", 275 CredentialName: "the-user"}}, 276 CurrentContext: "the-context", 277 Clouds: map[string]clientconfig.CloudConfig{ 278 "the-cluster": { 279 Endpoint: "https://1.1.1.1:8888", 280 Attributes: map[string]interface{}{"CAData": "A"}}}, 281 Credentials: map[string]cloud.Credential{ 282 "the-user": cred, 283 }, 284 }) 285 } 286 287 func (s *k8sConfigSuite) TestGetMultiConfig(c *gc.C) { 288 firstCred := cloud.NewNamedCredential( 289 "default-user", cloud.UserPassAuthType, 290 map[string]string{"username": "defaultuser", "password": "defaultpassword"}, false) 291 theCred := cloud.NewNamedCredential( 292 "the-user", cloud.OAuth2AuthType, 293 map[string]string{"Token": "tokenwithcerttoken"}, false) 294 secondCred := cloud.NewNamedCredential( 295 "second-user", cloud.ClientCertificateAuthType, 296 map[string]string{"ClientCertificateData": "A", "ClientKeyData": "A"}, false) 297 298 for i, v := range []newK8sClientConfigTestCase{ 299 { 300 title: "no cluster name specified, will select current cluster", 301 clusterName: "", // will use current context. 302 expected: &clientconfig.ClientConfig{ 303 Type: "kubernetes", 304 Contexts: map[string]clientconfig.Context{ 305 "default-context": { 306 CloudName: "default-cluster", 307 CredentialName: "default-user"}, 308 }, 309 CurrentContext: "default-context", 310 Clouds: map[string]clientconfig.CloudConfig{ 311 "default-cluster": { 312 Endpoint: "https://10.10.10.10:1010", 313 Attributes: map[string]interface{}{"CAData": ""}, 314 }, 315 }, 316 Credentials: map[string]cloud.Credential{ 317 "default-user": firstCred, 318 }, 319 }, 320 }, 321 { 322 title: "select the-cluster", 323 clusterName: "the-cluster", 324 expected: &clientconfig.ClientConfig{ 325 Type: "kubernetes", 326 Contexts: map[string]clientconfig.Context{ 327 "the-context": { 328 CloudName: "the-cluster", 329 CredentialName: "the-user"}, 330 }, 331 CurrentContext: "default-context", 332 Clouds: map[string]clientconfig.CloudConfig{ 333 "the-cluster": { 334 Endpoint: "https://1.1.1.1:8888", 335 Attributes: map[string]interface{}{"CAData": "A"}}}, 336 Credentials: map[string]cloud.Credential{ 337 "the-user": theCred, 338 }, 339 }, 340 }, 341 { 342 title: "select second-cluster", 343 clusterName: "second-cluster", 344 expected: &clientconfig.ClientConfig{ 345 Type: "kubernetes", 346 Contexts: map[string]clientconfig.Context{ 347 "second-context": { 348 CloudName: "second-cluster", 349 CredentialName: "second-user"}, 350 }, 351 CurrentContext: "default-context", 352 Clouds: map[string]clientconfig.CloudConfig{ 353 "second-cluster": { 354 Endpoint: "https://100.100.100.100:1010", 355 Attributes: map[string]interface{}{"CAData": "A"}}}, 356 Credentials: map[string]cloud.Credential{ 357 "second-user": secondCred, 358 }, 359 }, 360 }, 361 { 362 title: "select the-context", 363 contextName: "the-context", 364 expected: &clientconfig.ClientConfig{ 365 Type: "kubernetes", 366 Contexts: map[string]clientconfig.Context{ 367 "the-context": { 368 CloudName: "the-cluster", 369 CredentialName: "the-user"}, 370 }, 371 CurrentContext: "default-context", 372 Clouds: map[string]clientconfig.CloudConfig{ 373 "the-cluster": { 374 Endpoint: "https://1.1.1.1:8888", 375 Attributes: map[string]interface{}{"CAData": "A"}}}, 376 Credentials: map[string]cloud.Credential{ 377 "the-user": theCred, 378 }, 379 }, 380 }, 381 { 382 title: "select default-cluster", 383 clusterName: "default-cluster", 384 expected: &clientconfig.ClientConfig{ 385 Type: "kubernetes", 386 Contexts: map[string]clientconfig.Context{ 387 "default-context": { 388 CloudName: "default-cluster", 389 CredentialName: "default-user"}, 390 }, 391 CurrentContext: "default-context", 392 Clouds: map[string]clientconfig.CloudConfig{ 393 "default-cluster": { 394 Endpoint: "https://10.10.10.10:1010", 395 Attributes: map[string]interface{}{"CAData": ""}, 396 }}, 397 Credentials: map[string]cloud.Credential{ 398 "default-user": firstCred, 399 }, 400 }, 401 }, 402 } { 403 c.Logf("testcase %v: %s", i, v.title) 404 v.configYamlFileName = "multiConfig" 405 v.configYamlContent = multiConfigYAML 406 s.assertNewK8sClientConfig(c, v) 407 } 408 } 409 410 func (s *k8sConfigSuite) TestConfigWithExternalCA(c *gc.C) { 411 caFile, err := os.CreateTemp("", "*") 412 c.Assert(err, jc.ErrorIsNil) 413 _, err = caFile.WriteString("QQ==") 414 c.Assert(err, jc.ErrorIsNil) 415 c.Assert(caFile.Close(), jc.ErrorIsNil) 416 417 tpl, err := template.New("").Parse(externalCAYAMLTemplate) 418 c.Assert(err, jc.ErrorIsNil) 419 420 conf := strings.Builder{} 421 c.Assert(tpl.Execute(&conf, caFile.Name()), jc.ErrorIsNil) 422 423 cred := cloud.NewNamedCredential( 424 "the-user", cloud.UserPassAuthType, 425 map[string]string{"username": "theuser", "password": "thepassword"}, false) 426 s.assertNewK8sClientConfig(c, newK8sClientConfigTestCase{ 427 title: "assert config with external ca", 428 clusterName: "the-cluster", 429 configYamlContent: conf.String(), 430 configYamlFileName: "external-ca", 431 expected: &clientconfig.ClientConfig{ 432 Type: "kubernetes", 433 Contexts: map[string]clientconfig.Context{ 434 "the-context": { 435 CloudName: "the-cluster", 436 CredentialName: "the-user"}}, 437 CurrentContext: "the-context", 438 Clouds: map[string]clientconfig.CloudConfig{ 439 "the-cluster": { 440 Endpoint: "https://1.1.1.1:8888", 441 Attributes: map[string]interface{}{"CAData": "QQ=="}}}, 442 Credentials: map[string]cloud.Credential{ 443 "the-user": cred, 444 }, 445 }, 446 }) 447 } 448 449 func (s *k8sConfigSuite) TestConfigWithInsecureSkilTLSVerify(c *gc.C) { 450 cred := cloud.NewNamedCredential( 451 "the-user", cloud.UserPassAuthType, 452 map[string]string{"username": "theuser", "password": "thepassword"}, false) 453 s.assertNewK8sClientConfig(c, newK8sClientConfigTestCase{ 454 title: "assert config with insecure TLS skip verify", 455 clusterName: "the-cluster", 456 configYamlContent: insecureTLSYAMLTemplate, 457 configYamlFileName: "insecure-tls", 458 expected: &clientconfig.ClientConfig{ 459 Type: "kubernetes", 460 Contexts: map[string]clientconfig.Context{ 461 "the-context": { 462 CloudName: "the-cluster", 463 CredentialName: "the-user"}}, 464 CurrentContext: "the-context", 465 Clouds: map[string]clientconfig.CloudConfig{ 466 "the-cluster": { 467 Endpoint: "https://1.1.1.1:8888", 468 SkipTLSVerify: true, 469 Attributes: map[string]interface{}{"CAData": ""}}}, 470 Credentials: map[string]cloud.Credential{ 471 "the-user": cred, 472 }, 473 }, 474 }) 475 } 476 477 // TestGetSingleConfigReadsFilePaths checks that we handle config 478 // with certificate/key file paths the same as we do those with 479 // the data inline. 480 func (s *k8sConfigSuite) TestGetSingleConfigReadsFilePaths(c *gc.C) { 481 482 singleConfig, err := clientcmd.Load([]byte(singleConfigYAML)) 483 c.Assert(err, jc.ErrorIsNil) 484 485 tempdir := c.MkDir() 486 divert := func(name string, data *[]byte, path *string) { 487 *path = filepath.Join(tempdir, name) 488 err := os.WriteFile(*path, *data, 0644) 489 c.Assert(err, jc.ErrorIsNil) 490 *data = nil 491 } 492 493 for name, cluster := range singleConfig.Clusters { 494 divert( 495 "cluster-"+name+".ca", 496 &cluster.CertificateAuthorityData, 497 &cluster.CertificateAuthority, 498 ) 499 } 500 501 for name, authInfo := range singleConfig.AuthInfos { 502 divert( 503 "auth-"+name+".cert", 504 &authInfo.ClientCertificateData, 505 &authInfo.ClientCertificate, 506 ) 507 divert( 508 "auth-"+name+".key", 509 &authInfo.ClientKeyData, 510 &authInfo.ClientKey, 511 ) 512 } 513 514 singleConfigWithPathsYAML, err := clientcmd.Write(*singleConfig) 515 c.Assert(err, jc.ErrorIsNil) 516 517 cred := cloud.NewNamedCredential( 518 "the-user", cloud.UserPassAuthType, 519 map[string]string{"username": "theuser", "password": "thepassword"}, false) 520 s.assertNewK8sClientConfig(c, newK8sClientConfigTestCase{ 521 title: "assert single config", 522 configYamlContent: string(singleConfigWithPathsYAML), 523 configYamlFileName: "singleConfigWithPaths", 524 expected: &clientconfig.ClientConfig{ 525 Type: "kubernetes", 526 Contexts: map[string]clientconfig.Context{ 527 "the-context": { 528 CloudName: "the-cluster", 529 CredentialName: "the-user"}}, 530 CurrentContext: "the-context", 531 Clouds: map[string]clientconfig.CloudConfig{ 532 "the-cluster": { 533 Endpoint: "https://1.1.1.1:8888", 534 Attributes: map[string]interface{}{"CAData": "A"}}}, 535 Credentials: map[string]cloud.Credential{ 536 "the-user": cred, 537 }, 538 }, 539 }) 540 }