k8s.io/client-go@v0.31.1/tools/clientcmd/loader_test.go (about)

     1  /*
     2  Copyright 2014 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package clientcmd
    18  
    19  import (
    20  	"bytes"
    21  	"flag"
    22  	"fmt"
    23  	"os"
    24  	"path/filepath"
    25  	"reflect"
    26  	"strings"
    27  	"testing"
    28  
    29  	utiltesting "k8s.io/client-go/util/testing"
    30  
    31  	"github.com/google/go-cmp/cmp"
    32  	"sigs.k8s.io/yaml"
    33  
    34  	"k8s.io/apimachinery/pkg/runtime"
    35  	clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
    36  	clientcmdlatest "k8s.io/client-go/tools/clientcmd/api/latest"
    37  	"k8s.io/klog/v2"
    38  )
    39  
    40  var (
    41  	testConfigAlfa = clientcmdapi.Config{
    42  		AuthInfos: map[string]*clientcmdapi.AuthInfo{
    43  			"red-user": {Token: "red-token"}},
    44  		Clusters: map[string]*clientcmdapi.Cluster{
    45  			"cow-cluster": {Server: "http://cow.org:8080"}},
    46  		Contexts: map[string]*clientcmdapi.Context{
    47  			"federal-context": {AuthInfo: "red-user", Cluster: "cow-cluster", Namespace: "hammer-ns"}},
    48  	}
    49  	testConfigBravo = clientcmdapi.Config{
    50  		AuthInfos: map[string]*clientcmdapi.AuthInfo{
    51  			"black-user": {Token: "black-token"}},
    52  		Clusters: map[string]*clientcmdapi.Cluster{
    53  			"pig-cluster": {Server: "http://pig.org:8080"}},
    54  		Contexts: map[string]*clientcmdapi.Context{
    55  			"queen-anne-context": {AuthInfo: "black-user", Cluster: "pig-cluster", Namespace: "saw-ns"}},
    56  	}
    57  	testConfigCharlie = clientcmdapi.Config{
    58  		AuthInfos: map[string]*clientcmdapi.AuthInfo{
    59  			"green-user": {Token: "green-token"}},
    60  		Clusters: map[string]*clientcmdapi.Cluster{
    61  			"horse-cluster": {Server: "http://horse.org:8080"}},
    62  		Contexts: map[string]*clientcmdapi.Context{
    63  			"shaker-context": {AuthInfo: "green-user", Cluster: "horse-cluster", Namespace: "chisel-ns"}},
    64  	}
    65  	testConfigDelta = clientcmdapi.Config{
    66  		AuthInfos: map[string]*clientcmdapi.AuthInfo{
    67  			"blue-user": {Token: "blue-token"}},
    68  		Clusters: map[string]*clientcmdapi.Cluster{
    69  			"chicken-cluster": {Server: "http://chicken.org:8080"}},
    70  		Contexts: map[string]*clientcmdapi.Context{
    71  			"gothic-context": {AuthInfo: "blue-user", Cluster: "chicken-cluster", Namespace: "plane-ns"}},
    72  	}
    73  
    74  	testConfigConflictAlfa = clientcmdapi.Config{
    75  		AuthInfos: map[string]*clientcmdapi.AuthInfo{
    76  			"red-user":    {Token: "a-different-red-token"},
    77  			"yellow-user": {Token: "yellow-token"}},
    78  		Clusters: map[string]*clientcmdapi.Cluster{
    79  			"cow-cluster":    {Server: "http://a-different-cow.org:8080", InsecureSkipTLSVerify: true, DisableCompression: true},
    80  			"donkey-cluster": {Server: "http://donkey.org:8080", InsecureSkipTLSVerify: true, DisableCompression: true}},
    81  		CurrentContext: "federal-context",
    82  	}
    83  )
    84  
    85  func TestNilOutMap(t *testing.T) {
    86  	var fakeKubeconfigData = `apiVersion: v1
    87  kind: Config
    88  clusters:
    89  - cluster:
    90      certificate-authority-data: UEhPTlkK
    91      server: https://1.1.1.1
    92    name: production
    93  contexts:
    94  - context:
    95      cluster: production
    96      user: production
    97    name: production
    98  current-context: production
    99  users:
   100  - name: production
   101    user:
   102      auth-provider:
   103        name: gcp`
   104  
   105  	_, _, err := clientcmdlatest.Codec.Decode([]byte(fakeKubeconfigData), nil, nil)
   106  	if err != nil {
   107  		t.Fatalf("unexpected error: %v", err)
   108  	}
   109  }
   110  
   111  func TestNonExistentCommandLineFile(t *testing.T) {
   112  	loadingRules := ClientConfigLoadingRules{
   113  		ExplicitPath: "bogus_file",
   114  	}
   115  
   116  	_, err := loadingRules.Load()
   117  	if err == nil {
   118  		t.Fatalf("Expected error for missing command-line file, got none")
   119  	}
   120  	if !strings.Contains(err.Error(), "bogus_file") {
   121  		t.Fatalf("Expected error about 'bogus_file', got %s", err.Error())
   122  	}
   123  }
   124  
   125  func TestToleratingMissingFiles(t *testing.T) {
   126  	envVarValue := "bogus"
   127  	loadingRules := ClientConfigLoadingRules{
   128  		Precedence:       []string{"bogus1", "bogus2", "bogus3"},
   129  		WarnIfAllMissing: true,
   130  		Warner:           func(err error) { klog.Warning(err) },
   131  	}
   132  
   133  	buffer := &bytes.Buffer{}
   134  
   135  	klog.LogToStderr(false)
   136  	klog.SetOutput(buffer)
   137  
   138  	_, err := loadingRules.Load()
   139  	if err != nil {
   140  		t.Fatalf("Unexpected error: %v", err)
   141  	}
   142  	klog.Flush()
   143  	expectedLog := fmt.Sprintf("Config not found: %s", envVarValue)
   144  	if !strings.Contains(buffer.String(), expectedLog) {
   145  		t.Fatalf("expected log: \"%s\"", expectedLog)
   146  	}
   147  }
   148  
   149  func TestWarningMissingFiles(t *testing.T) {
   150  	envVarValue := "bogus"
   151  	t.Setenv(RecommendedConfigPathEnvVar, envVarValue)
   152  	loadingRules := NewDefaultClientConfigLoadingRules()
   153  
   154  	buffer := &bytes.Buffer{}
   155  
   156  	flags := &flag.FlagSet{}
   157  	klog.InitFlags(flags)
   158  	flags.Set("v", "1")
   159  	klog.LogToStderr(false)
   160  	klog.SetOutput(buffer)
   161  
   162  	_, err := loadingRules.Load()
   163  	if err != nil {
   164  		t.Fatalf("Unexpected error: %v", err)
   165  	}
   166  	klog.Flush()
   167  
   168  	expectedLog := fmt.Sprintf("Config not found: %s", envVarValue)
   169  	if !strings.Contains(buffer.String(), expectedLog) {
   170  		t.Fatalf("expected log: \"%s\"", expectedLog)
   171  	}
   172  }
   173  
   174  func TestNoWarningMissingFiles(t *testing.T) {
   175  	envVarValue := "bogus"
   176  	t.Setenv(RecommendedConfigPathEnvVar, envVarValue)
   177  	loadingRules := NewDefaultClientConfigLoadingRules()
   178  
   179  	buffer := &bytes.Buffer{}
   180  
   181  	flags := &flag.FlagSet{}
   182  	klog.InitFlags(flags)
   183  	flags.Set("v", "0")
   184  	klog.LogToStderr(false)
   185  	klog.SetOutput(buffer)
   186  
   187  	_, err := loadingRules.Load()
   188  	if err != nil {
   189  		t.Fatalf("Unexpected error: %v", err)
   190  	}
   191  	klog.Flush()
   192  
   193  	logNotExpected := fmt.Sprintf("Config not found: %s", envVarValue)
   194  	if strings.Contains(buffer.String(), logNotExpected) {
   195  		t.Fatalf("log not expected: \"%s\"", logNotExpected)
   196  	}
   197  }
   198  
   199  func TestErrorReadingFile(t *testing.T) {
   200  	commandLineFile, _ := os.CreateTemp("", "")
   201  	defer utiltesting.CloseAndRemove(t, commandLineFile)
   202  
   203  	if err := os.WriteFile(commandLineFile.Name(), []byte("bogus value"), 0644); err != nil {
   204  		t.Fatalf("Error creating tempfile: %v", err)
   205  	}
   206  
   207  	loadingRules := ClientConfigLoadingRules{
   208  		ExplicitPath: commandLineFile.Name(),
   209  	}
   210  
   211  	_, err := loadingRules.Load()
   212  	if err == nil {
   213  		t.Fatalf("Expected error for unloadable file, got none")
   214  	}
   215  	if !strings.Contains(err.Error(), commandLineFile.Name()) {
   216  		t.Fatalf("Expected error about '%s', got %s", commandLineFile.Name(), err.Error())
   217  	}
   218  }
   219  
   220  func TestErrorReadingNonFile(t *testing.T) {
   221  	tmpdir, err := os.MkdirTemp("", "")
   222  	if err != nil {
   223  		t.Fatalf("Couldn't create tmpdir")
   224  	}
   225  	defer os.RemoveAll(tmpdir)
   226  
   227  	loadingRules := ClientConfigLoadingRules{
   228  		ExplicitPath: tmpdir,
   229  	}
   230  
   231  	_, err = loadingRules.Load()
   232  	if err == nil {
   233  		t.Fatalf("Expected error for non-file, got none")
   234  	}
   235  	if !strings.Contains(err.Error(), tmpdir) {
   236  		t.Fatalf("Expected error about '%s', got %s", tmpdir, err.Error())
   237  	}
   238  }
   239  
   240  func TestConflictingCurrentContext(t *testing.T) {
   241  	commandLineFile, _ := os.CreateTemp("", "")
   242  	envVarFile, _ := os.CreateTemp("", "")
   243  	defer utiltesting.CloseAndRemove(t, commandLineFile, envVarFile)
   244  
   245  	mockCommandLineConfig := clientcmdapi.Config{
   246  		CurrentContext: "any-context-value",
   247  	}
   248  	mockEnvVarConfig := clientcmdapi.Config{
   249  		CurrentContext: "a-different-context",
   250  	}
   251  
   252  	WriteToFile(mockCommandLineConfig, commandLineFile.Name())
   253  	WriteToFile(mockEnvVarConfig, envVarFile.Name())
   254  
   255  	loadingRules := ClientConfigLoadingRules{
   256  		ExplicitPath: commandLineFile.Name(),
   257  		Precedence:   []string{envVarFile.Name()},
   258  	}
   259  
   260  	mergedConfig, err := loadingRules.Load()
   261  	if err != nil {
   262  		t.Errorf("Unexpected error: %v", err)
   263  	}
   264  
   265  	if mergedConfig.CurrentContext != mockCommandLineConfig.CurrentContext {
   266  		t.Errorf("expected %v, got %v", mockCommandLineConfig.CurrentContext, mergedConfig.CurrentContext)
   267  	}
   268  }
   269  
   270  func TestEncodeYAML(t *testing.T) {
   271  	config := clientcmdapi.Config{
   272  		CurrentContext: "any-context-value",
   273  		Contexts: map[string]*clientcmdapi.Context{
   274  			"433e40": {
   275  				Cluster: "433e40",
   276  			},
   277  		},
   278  		Clusters: map[string]*clientcmdapi.Cluster{
   279  			"0": {
   280  				Server: "https://localhost:1234",
   281  			},
   282  			"1": {
   283  				Server: "https://localhost:1234",
   284  			},
   285  			"433e40": {
   286  				Server: "https://localhost:1234",
   287  			},
   288  		},
   289  	}
   290  	data, err := Write(config)
   291  	if err != nil {
   292  		t.Fatal(err)
   293  	}
   294  	expected := []byte(`apiVersion: v1
   295  clusters:
   296  - cluster:
   297      server: https://localhost:1234
   298    name: "0"
   299  - cluster:
   300      server: https://localhost:1234
   301    name: "1"
   302  - cluster:
   303      server: https://localhost:1234
   304    name: "433e40"
   305  contexts:
   306  - context:
   307      cluster: "433e40"
   308      user: ""
   309    name: "433e40"
   310  current-context: any-context-value
   311  kind: Config
   312  preferences: {}
   313  users: null
   314  `)
   315  	if !bytes.Equal(expected, data) {
   316  		t.Error(cmp.Diff(string(expected), string(data)))
   317  	}
   318  }
   319  
   320  func TestLoadingEmptyMaps(t *testing.T) {
   321  	configFile, _ := os.CreateTemp("", "")
   322  	defer utiltesting.CloseAndRemove(t, configFile)
   323  
   324  	mockConfig := clientcmdapi.Config{
   325  		CurrentContext: "any-context-value",
   326  	}
   327  
   328  	WriteToFile(mockConfig, configFile.Name())
   329  
   330  	config, err := LoadFromFile(configFile.Name())
   331  	if err != nil {
   332  		t.Errorf("Unexpected error: %v", err)
   333  	}
   334  
   335  	if config.Clusters == nil {
   336  		t.Error("expected config.Clusters to be non-nil")
   337  	}
   338  	if config.AuthInfos == nil {
   339  		t.Error("expected config.AuthInfos to be non-nil")
   340  	}
   341  	if config.Contexts == nil {
   342  		t.Error("expected config.Contexts to be non-nil")
   343  	}
   344  }
   345  
   346  func TestDuplicateClusterName(t *testing.T) {
   347  	configFile, _ := os.CreateTemp("", "")
   348  	defer utiltesting.CloseAndRemove(t, configFile)
   349  
   350  	err := os.WriteFile(configFile.Name(), []byte(`
   351  kind: Config
   352  apiVersion: v1
   353  clusters:
   354  - cluster:
   355      api-version: v1
   356      server: https://kubernetes.default.svc:443
   357      certificate-authority: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
   358    name: kubeconfig-cluster
   359  - cluster:
   360      api-version: v2
   361      server: https://test.example.server:443
   362      certificate-authority: /var/run/secrets/test.example.io/serviceaccount/ca.crt
   363    name: kubeconfig-cluster
   364  contexts:
   365  - context:
   366      cluster: kubeconfig-cluster
   367      namespace: default
   368      user: kubeconfig-user
   369    name: kubeconfig-context
   370  current-context: kubeconfig-context
   371  users:
   372  - name: kubeconfig-user
   373    user:
   374      tokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token
   375  `), os.FileMode(0755))
   376  
   377  	if err != nil {
   378  		t.Errorf("Unexpected error: %v", err)
   379  	}
   380  
   381  	_, err = LoadFromFile(configFile.Name())
   382  	if err == nil || !strings.Contains(err.Error(),
   383  		"error converting *[]NamedCluster into *map[string]*api.Cluster: duplicate name \"kubeconfig-cluster\" in list") {
   384  		t.Error("Expected error in loading duplicate cluster name, got none")
   385  	}
   386  }
   387  
   388  func TestDuplicateContextName(t *testing.T) {
   389  	configFile, _ := os.CreateTemp("", "")
   390  	defer utiltesting.CloseAndRemove(t, configFile)
   391  
   392  	err := os.WriteFile(configFile.Name(), []byte(`
   393  kind: Config
   394  apiVersion: v1
   395  clusters:
   396  - cluster:
   397      api-version: v1
   398      server: https://kubernetes.default.svc:443
   399      certificate-authority: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
   400    name: kubeconfig-cluster
   401  contexts:
   402  - context:
   403      cluster: kubeconfig-cluster
   404      namespace: default
   405      user: kubeconfig-user
   406    name: kubeconfig-context
   407  - context:
   408      cluster: test-example-cluster
   409      namespace: test-example
   410      user: test-example-user
   411    name: kubeconfig-context
   412  current-context: kubeconfig-context
   413  users:
   414  - name: kubeconfig-user
   415    user:
   416      tokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token
   417  `), os.FileMode(0755))
   418  
   419  	if err != nil {
   420  		t.Errorf("Unexpected error: %v", err)
   421  	}
   422  
   423  	_, err = LoadFromFile(configFile.Name())
   424  	if err == nil || !strings.Contains(err.Error(),
   425  		"error converting *[]NamedContext into *map[string]*api.Context: duplicate name \"kubeconfig-context\" in list") {
   426  		t.Error("Expected error in loading duplicate context name, got none")
   427  	}
   428  }
   429  
   430  func TestDuplicateUserName(t *testing.T) {
   431  	configFile, _ := os.CreateTemp("", "")
   432  	defer utiltesting.CloseAndRemove(t, configFile)
   433  
   434  	err := os.WriteFile(configFile.Name(), []byte(`
   435  kind: Config
   436  apiVersion: v1
   437  clusters:
   438  - cluster:
   439      api-version: v1
   440      server: https://kubernetes.default.svc:443
   441      certificate-authority: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
   442    name: kubeconfig-cluster
   443  contexts:
   444  - context:
   445      cluster: kubeconfig-cluster
   446      namespace: default
   447      user: kubeconfig-user
   448    name: kubeconfig-context
   449  current-context: kubeconfig-context
   450  users:
   451  - name: kubeconfig-user
   452    user:
   453      tokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token
   454  - name: kubeconfig-user
   455    user:
   456      tokenFile: /var/run/secrets/test.example.com/serviceaccount/token
   457  `), os.FileMode(0755))
   458  
   459  	if err != nil {
   460  		t.Errorf("Unexpected error: %v", err)
   461  	}
   462  
   463  	_, err = LoadFromFile(configFile.Name())
   464  	if err == nil || !strings.Contains(err.Error(),
   465  		"error converting *[]NamedAuthInfo into *map[string]*api.AuthInfo: duplicate name \"kubeconfig-user\" in list") {
   466  		t.Error("Expected error in loading duplicate user name, got none")
   467  	}
   468  }
   469  
   470  func TestDuplicateExtensionName(t *testing.T) {
   471  	configFile, _ := os.CreateTemp("", "")
   472  	defer utiltesting.CloseAndRemove(t, configFile)
   473  
   474  	err := os.WriteFile(configFile.Name(), []byte(`
   475  kind: Config
   476  apiVersion: v1
   477  clusters:
   478  - cluster:
   479      api-version: v1
   480      server: https://kubernetes.default.svc:443
   481      certificate-authority: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
   482    name: kubeconfig-cluster
   483  contexts:
   484  - context:
   485      cluster: kubeconfig-cluster
   486      namespace: default
   487      user: kubeconfig-user
   488    name: kubeconfig-context
   489  current-context: kubeconfig-context
   490  users:
   491  - name: kubeconfig-user
   492    user:
   493      tokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token
   494  extensions:
   495  - extension:
   496      bytes: test
   497    name: test-extension
   498  - extension:
   499      bytes: some-example
   500    name: test-extension
   501  `), os.FileMode(0755))
   502  
   503  	if err != nil {
   504  		t.Errorf("Unexpected error: %v", err)
   505  	}
   506  
   507  	_, err = LoadFromFile(configFile.Name())
   508  	if err == nil || !strings.Contains(err.Error(),
   509  		"error converting *[]NamedExtension into *map[string]runtime.Object: duplicate name \"test-extension\" in list") {
   510  		t.Error("Expected error in loading duplicate extension name, got none")
   511  	}
   512  }
   513  
   514  func TestResolveRelativePaths(t *testing.T) {
   515  	pathResolutionConfig1 := clientcmdapi.Config{
   516  		AuthInfos: map[string]*clientcmdapi.AuthInfo{
   517  			"relative-user-1": {ClientCertificate: "relative/client/cert", ClientKey: "../relative/client/key"},
   518  			"absolute-user-1": {ClientCertificate: "/absolute/client/cert", ClientKey: "/absolute/client/key"},
   519  			"relative-cmd-1":  {Exec: &clientcmdapi.ExecConfig{Command: "../relative/client/cmd"}},
   520  			"absolute-cmd-1":  {Exec: &clientcmdapi.ExecConfig{Command: "/absolute/client/cmd"}},
   521  			"PATH-cmd-1":      {Exec: &clientcmdapi.ExecConfig{Command: "cmd"}},
   522  		},
   523  		Clusters: map[string]*clientcmdapi.Cluster{
   524  			"relative-server-1": {CertificateAuthority: "../relative/ca"},
   525  			"absolute-server-1": {CertificateAuthority: "/absolute/ca"},
   526  		},
   527  	}
   528  	pathResolutionConfig2 := clientcmdapi.Config{
   529  		AuthInfos: map[string]*clientcmdapi.AuthInfo{
   530  			"relative-user-2": {ClientCertificate: "relative/client/cert2", ClientKey: "../relative/client/key2"},
   531  			"absolute-user-2": {ClientCertificate: "/absolute/client/cert2", ClientKey: "/absolute/client/key2"},
   532  		},
   533  		Clusters: map[string]*clientcmdapi.Cluster{
   534  			"relative-server-2": {CertificateAuthority: "../relative/ca2"},
   535  			"absolute-server-2": {CertificateAuthority: "/absolute/ca2"},
   536  		},
   537  	}
   538  
   539  	configDir1, _ := os.MkdirTemp("", "")
   540  	defer os.RemoveAll(configDir1)
   541  	configFile1 := filepath.Join(configDir1, ".kubeconfig")
   542  	configDir1, _ = filepath.Abs(configDir1)
   543  
   544  	configDir2, _ := os.MkdirTemp("", "")
   545  	defer os.RemoveAll(configDir2)
   546  	configDir2, _ = os.MkdirTemp(configDir2, "")
   547  	configFile2 := filepath.Join(configDir2, ".kubeconfig")
   548  	configDir2, _ = filepath.Abs(configDir2)
   549  
   550  	WriteToFile(pathResolutionConfig1, configFile1)
   551  	WriteToFile(pathResolutionConfig2, configFile2)
   552  
   553  	loadingRules := ClientConfigLoadingRules{
   554  		Precedence: []string{configFile1, configFile2},
   555  	}
   556  
   557  	mergedConfig, err := loadingRules.Load()
   558  	if err != nil {
   559  		t.Errorf("Unexpected error: %v", err)
   560  	}
   561  
   562  	foundClusterCount := 0
   563  	for key, cluster := range mergedConfig.Clusters {
   564  		if key == "relative-server-1" {
   565  			foundClusterCount++
   566  			matchStringArg(filepath.Join(configDir1, pathResolutionConfig1.Clusters["relative-server-1"].CertificateAuthority), cluster.CertificateAuthority, t)
   567  		}
   568  		if key == "relative-server-2" {
   569  			foundClusterCount++
   570  			matchStringArg(filepath.Join(configDir2, pathResolutionConfig2.Clusters["relative-server-2"].CertificateAuthority), cluster.CertificateAuthority, t)
   571  		}
   572  		if key == "absolute-server-1" {
   573  			foundClusterCount++
   574  			matchStringArg(pathResolutionConfig1.Clusters["absolute-server-1"].CertificateAuthority, cluster.CertificateAuthority, t)
   575  		}
   576  		if key == "absolute-server-2" {
   577  			foundClusterCount++
   578  			matchStringArg(pathResolutionConfig2.Clusters["absolute-server-2"].CertificateAuthority, cluster.CertificateAuthority, t)
   579  		}
   580  	}
   581  	if foundClusterCount != 4 {
   582  		t.Errorf("Expected 4 clusters, found %v: %v", foundClusterCount, mergedConfig.Clusters)
   583  	}
   584  
   585  	foundAuthInfoCount := 0
   586  	for key, authInfo := range mergedConfig.AuthInfos {
   587  		if key == "relative-user-1" {
   588  			foundAuthInfoCount++
   589  			matchStringArg(filepath.Join(configDir1, pathResolutionConfig1.AuthInfos["relative-user-1"].ClientCertificate), authInfo.ClientCertificate, t)
   590  			matchStringArg(filepath.Join(configDir1, pathResolutionConfig1.AuthInfos["relative-user-1"].ClientKey), authInfo.ClientKey, t)
   591  		}
   592  		if key == "relative-user-2" {
   593  			foundAuthInfoCount++
   594  			matchStringArg(filepath.Join(configDir2, pathResolutionConfig2.AuthInfos["relative-user-2"].ClientCertificate), authInfo.ClientCertificate, t)
   595  			matchStringArg(filepath.Join(configDir2, pathResolutionConfig2.AuthInfos["relative-user-2"].ClientKey), authInfo.ClientKey, t)
   596  		}
   597  		if key == "absolute-user-1" {
   598  			foundAuthInfoCount++
   599  			matchStringArg(pathResolutionConfig1.AuthInfos["absolute-user-1"].ClientCertificate, authInfo.ClientCertificate, t)
   600  			matchStringArg(pathResolutionConfig1.AuthInfos["absolute-user-1"].ClientKey, authInfo.ClientKey, t)
   601  		}
   602  		if key == "absolute-user-2" {
   603  			foundAuthInfoCount++
   604  			matchStringArg(pathResolutionConfig2.AuthInfos["absolute-user-2"].ClientCertificate, authInfo.ClientCertificate, t)
   605  			matchStringArg(pathResolutionConfig2.AuthInfos["absolute-user-2"].ClientKey, authInfo.ClientKey, t)
   606  		}
   607  		if key == "relative-cmd-1" {
   608  			foundAuthInfoCount++
   609  			matchStringArg(filepath.Join(configDir1, pathResolutionConfig1.AuthInfos[key].Exec.Command), authInfo.Exec.Command, t)
   610  		}
   611  		if key == "absolute-cmd-1" {
   612  			foundAuthInfoCount++
   613  			matchStringArg(pathResolutionConfig1.AuthInfos[key].Exec.Command, authInfo.Exec.Command, t)
   614  		}
   615  		if key == "PATH-cmd-1" {
   616  			foundAuthInfoCount++
   617  			matchStringArg(pathResolutionConfig1.AuthInfos[key].Exec.Command, authInfo.Exec.Command, t)
   618  		}
   619  	}
   620  	if foundAuthInfoCount != 7 {
   621  		t.Errorf("Expected 7 users, found %v: %v", foundAuthInfoCount, mergedConfig.AuthInfos)
   622  	}
   623  
   624  }
   625  
   626  func TestMigratingFile(t *testing.T) {
   627  	sourceFile, _ := os.CreateTemp("", "")
   628  	defer utiltesting.CloseAndRemove(t, sourceFile)
   629  	destinationFile, _ := os.CreateTemp("", "")
   630  	// delete the file so that we'll write to it
   631  	os.Remove(destinationFile.Name())
   632  
   633  	WriteToFile(testConfigAlfa, sourceFile.Name())
   634  
   635  	loadingRules := ClientConfigLoadingRules{
   636  		MigrationRules: map[string]string{destinationFile.Name(): sourceFile.Name()},
   637  	}
   638  
   639  	if _, err := loadingRules.Load(); err != nil {
   640  		t.Errorf("unexpected error %v", err)
   641  	}
   642  	// the load should have recreated this file
   643  	defer utiltesting.CloseAndRemove(t, destinationFile)
   644  
   645  	sourceContent, err := os.ReadFile(sourceFile.Name())
   646  	if err != nil {
   647  		t.Errorf("unexpected error %v", err)
   648  	}
   649  	destinationContent, err := os.ReadFile(destinationFile.Name())
   650  	if err != nil {
   651  		t.Errorf("unexpected error %v", err)
   652  	}
   653  
   654  	if !reflect.DeepEqual(sourceContent, destinationContent) {
   655  		t.Errorf("source and destination do not match")
   656  	}
   657  }
   658  
   659  func TestMigratingFileLeaveExistingFileAlone(t *testing.T) {
   660  	sourceFile, _ := os.CreateTemp("", "")
   661  	destinationFile, _ := os.CreateTemp("", "")
   662  	defer utiltesting.CloseAndRemove(t, sourceFile, destinationFile)
   663  
   664  	WriteToFile(testConfigAlfa, sourceFile.Name())
   665  
   666  	loadingRules := ClientConfigLoadingRules{
   667  		MigrationRules: map[string]string{destinationFile.Name(): sourceFile.Name()},
   668  	}
   669  
   670  	if _, err := loadingRules.Load(); err != nil {
   671  		t.Errorf("unexpected error %v", err)
   672  	}
   673  
   674  	destinationContent, err := os.ReadFile(destinationFile.Name())
   675  	if err != nil {
   676  		t.Errorf("unexpected error %v", err)
   677  	}
   678  
   679  	if len(destinationContent) > 0 {
   680  		t.Errorf("destination should not have been touched")
   681  	}
   682  }
   683  
   684  func TestMigratingFileSourceMissingSkip(t *testing.T) {
   685  	sourceFilename := "some-missing-file"
   686  	destinationFile, _ := os.CreateTemp("", "")
   687  	// delete the file so that we'll write to it
   688  	utiltesting.CloseAndRemove(t, destinationFile)
   689  
   690  	loadingRules := ClientConfigLoadingRules{
   691  		MigrationRules: map[string]string{destinationFile.Name(): sourceFilename},
   692  	}
   693  
   694  	if _, err := loadingRules.Load(); err != nil {
   695  		t.Errorf("unexpected error %v", err)
   696  	}
   697  
   698  	if _, err := os.Stat(destinationFile.Name()); !os.IsNotExist(err) {
   699  		t.Errorf("destination should not exist")
   700  	}
   701  }
   702  
   703  func TestFileLocking(t *testing.T) {
   704  	f, _ := os.CreateTemp("", "")
   705  	defer utiltesting.CloseAndRemove(t, f)
   706  
   707  	err := lockFile(f.Name())
   708  	if err != nil {
   709  		t.Errorf("unexpected error while locking file: %v", err)
   710  	}
   711  	defer unlockFile(f.Name())
   712  
   713  	err = lockFile(f.Name())
   714  	if err == nil {
   715  		t.Error("expected error while locking file.")
   716  	}
   717  }
   718  
   719  func Example_noMergingOnExplicitPaths() {
   720  	commandLineFile, _ := os.CreateTemp("", "")
   721  	envVarFile, _ := os.CreateTemp("", "")
   722  	defer utiltesting.CloseAndRemove(&testing.T{}, commandLineFile, envVarFile)
   723  
   724  	WriteToFile(testConfigAlfa, commandLineFile.Name())
   725  	WriteToFile(testConfigConflictAlfa, envVarFile.Name())
   726  
   727  	loadingRules := ClientConfigLoadingRules{
   728  		ExplicitPath: commandLineFile.Name(),
   729  		Precedence:   []string{envVarFile.Name()},
   730  	}
   731  
   732  	mergedConfig, err := loadingRules.Load()
   733  	if err != nil {
   734  		fmt.Printf("Unexpected error: %v", err)
   735  	}
   736  	json, err := runtime.Encode(clientcmdlatest.Codec, mergedConfig)
   737  	if err != nil {
   738  		fmt.Printf("Unexpected error: %v", err)
   739  	}
   740  	output, err := yaml.JSONToYAML(json)
   741  	if err != nil {
   742  		fmt.Printf("Unexpected error: %v", err)
   743  	}
   744  
   745  	fmt.Printf("%v", string(output))
   746  	// Output:
   747  	// apiVersion: v1
   748  	// clusters:
   749  	// - cluster:
   750  	//     server: http://cow.org:8080
   751  	//   name: cow-cluster
   752  	// contexts:
   753  	// - context:
   754  	//     cluster: cow-cluster
   755  	//     namespace: hammer-ns
   756  	//     user: red-user
   757  	//   name: federal-context
   758  	// current-context: ""
   759  	// kind: Config
   760  	// preferences: {}
   761  	// users:
   762  	// - name: red-user
   763  	//   user:
   764  	//     token: red-token
   765  }
   766  
   767  func Example_mergingSomeWithConflict() {
   768  	commandLineFile, _ := os.CreateTemp("", "")
   769  	envVarFile, _ := os.CreateTemp("", "")
   770  	defer utiltesting.CloseAndRemove(&testing.T{}, commandLineFile, envVarFile)
   771  
   772  	WriteToFile(testConfigAlfa, commandLineFile.Name())
   773  	WriteToFile(testConfigConflictAlfa, envVarFile.Name())
   774  
   775  	loadingRules := ClientConfigLoadingRules{
   776  		Precedence: []string{commandLineFile.Name(), envVarFile.Name()},
   777  	}
   778  
   779  	mergedConfig, err := loadingRules.Load()
   780  	if err != nil {
   781  		fmt.Printf("Unexpected error: %v", err)
   782  	}
   783  	json, err := runtime.Encode(clientcmdlatest.Codec, mergedConfig)
   784  	if err != nil {
   785  		fmt.Printf("Unexpected error: %v", err)
   786  	}
   787  	output, err := yaml.JSONToYAML(json)
   788  	if err != nil {
   789  		fmt.Printf("Unexpected error: %v", err)
   790  	}
   791  
   792  	fmt.Printf("%v", string(output))
   793  	// Output:
   794  	// apiVersion: v1
   795  	// clusters:
   796  	// - cluster:
   797  	//     server: http://cow.org:8080
   798  	//   name: cow-cluster
   799  	// - cluster:
   800  	//     disable-compression: true
   801  	//     insecure-skip-tls-verify: true
   802  	//     server: http://donkey.org:8080
   803  	//   name: donkey-cluster
   804  	// contexts:
   805  	// - context:
   806  	//     cluster: cow-cluster
   807  	//     namespace: hammer-ns
   808  	//     user: red-user
   809  	//   name: federal-context
   810  	// current-context: federal-context
   811  	// kind: Config
   812  	// preferences: {}
   813  	// users:
   814  	// - name: red-user
   815  	//   user:
   816  	//     token: red-token
   817  	// - name: yellow-user
   818  	//   user:
   819  	//     token: yellow-token
   820  }
   821  
   822  func Example_mergingEverythingNoConflicts() {
   823  	commandLineFile, _ := os.CreateTemp("", "")
   824  	envVarFile, _ := os.CreateTemp("", "")
   825  	currentDirFile, _ := os.CreateTemp("", "")
   826  	homeDirFile, _ := os.CreateTemp("", "")
   827  	defer utiltesting.CloseAndRemove(&testing.T{}, commandLineFile, envVarFile, currentDirFile, homeDirFile)
   828  
   829  	WriteToFile(testConfigAlfa, commandLineFile.Name())
   830  	WriteToFile(testConfigBravo, envVarFile.Name())
   831  	WriteToFile(testConfigCharlie, currentDirFile.Name())
   832  	WriteToFile(testConfigDelta, homeDirFile.Name())
   833  
   834  	loadingRules := ClientConfigLoadingRules{
   835  		Precedence: []string{commandLineFile.Name(), envVarFile.Name(), currentDirFile.Name(), homeDirFile.Name()},
   836  	}
   837  
   838  	mergedConfig, err := loadingRules.Load()
   839  	if err != nil {
   840  		fmt.Printf("Unexpected error: %v", err)
   841  	}
   842  	json, err := runtime.Encode(clientcmdlatest.Codec, mergedConfig)
   843  	if err != nil {
   844  		fmt.Printf("Unexpected error: %v", err)
   845  	}
   846  	output, err := yaml.JSONToYAML(json)
   847  	if err != nil {
   848  		fmt.Printf("Unexpected error: %v", err)
   849  	}
   850  
   851  	fmt.Printf("%v", string(output))
   852  	// Output:
   853  	// 	apiVersion: v1
   854  	// clusters:
   855  	// - cluster:
   856  	//     server: http://chicken.org:8080
   857  	//   name: chicken-cluster
   858  	// - cluster:
   859  	//     server: http://cow.org:8080
   860  	//   name: cow-cluster
   861  	// - cluster:
   862  	//     server: http://horse.org:8080
   863  	//   name: horse-cluster
   864  	// - cluster:
   865  	//     server: http://pig.org:8080
   866  	//   name: pig-cluster
   867  	// contexts:
   868  	// - context:
   869  	//     cluster: cow-cluster
   870  	//     namespace: hammer-ns
   871  	//     user: red-user
   872  	//   name: federal-context
   873  	// - context:
   874  	//     cluster: chicken-cluster
   875  	//     namespace: plane-ns
   876  	//     user: blue-user
   877  	//   name: gothic-context
   878  	// - context:
   879  	//     cluster: pig-cluster
   880  	//     namespace: saw-ns
   881  	//     user: black-user
   882  	//   name: queen-anne-context
   883  	// - context:
   884  	//     cluster: horse-cluster
   885  	//     namespace: chisel-ns
   886  	//     user: green-user
   887  	//   name: shaker-context
   888  	// current-context: ""
   889  	// kind: Config
   890  	// preferences: {}
   891  	// users:
   892  	// - name: black-user
   893  	//   user:
   894  	//     token: black-token
   895  	// - name: blue-user
   896  	//   user:
   897  	//     token: blue-token
   898  	// - name: green-user
   899  	//   user:
   900  	//     token: green-token
   901  	// - name: red-user
   902  	//   user:
   903  	//     token: red-token
   904  }
   905  
   906  func TestDeduplicate(t *testing.T) {
   907  	testCases := []struct {
   908  		src    []string
   909  		expect []string
   910  	}{
   911  		{
   912  			src:    []string{"a", "b", "c", "d", "e", "f"},
   913  			expect: []string{"a", "b", "c", "d", "e", "f"},
   914  		},
   915  		{
   916  			src:    []string{"a", "b", "c", "b", "e", "f"},
   917  			expect: []string{"a", "b", "c", "e", "f"},
   918  		},
   919  		{
   920  			src:    []string{"a", "a", "b", "b", "c", "b"},
   921  			expect: []string{"a", "b", "c"},
   922  		},
   923  	}
   924  
   925  	for _, testCase := range testCases {
   926  		get := deduplicate(testCase.src)
   927  		if !reflect.DeepEqual(get, testCase.expect) {
   928  			t.Errorf("expect: %v, get: %v", testCase.expect, get)
   929  		}
   930  	}
   931  }
   932  
   933  func TestLoadingGetLoadingPrecedence(t *testing.T) {
   934  	testCases := map[string]struct {
   935  		rules      *ClientConfigLoadingRules
   936  		env        string
   937  		precedence []string
   938  	}{
   939  		"default": {
   940  			precedence: []string{filepath.Join(os.Getenv("HOME"), ".kube/config")},
   941  		},
   942  		"explicit": {
   943  			rules: &ClientConfigLoadingRules{
   944  				ExplicitPath: "/explicit/kubeconfig",
   945  			},
   946  			precedence: []string{"/explicit/kubeconfig"},
   947  		},
   948  		"envvar-single": {
   949  			env:        "/env/kubeconfig",
   950  			precedence: []string{"/env/kubeconfig"},
   951  		},
   952  		"envvar-multiple": {
   953  			env:        "/env/kubeconfig:/other/kubeconfig",
   954  			precedence: []string{"/env/kubeconfig", "/other/kubeconfig"},
   955  		},
   956  	}
   957  
   958  	for name, test := range testCases {
   959  		t.Run(name, func(t *testing.T) {
   960  			t.Setenv("KUBECONFIG", test.env)
   961  			rules := test.rules
   962  			if rules == nil {
   963  				rules = NewDefaultClientConfigLoadingRules()
   964  			}
   965  			actual := rules.GetLoadingPrecedence()
   966  			if !reflect.DeepEqual(actual, test.precedence) {
   967  				t.Errorf("expect %v, got %v", test.precedence, actual)
   968  			}
   969  		})
   970  	}
   971  }