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  }