istio.io/istio@v0.0.0-20240520182934-d79c90f27776/istioctl/pkg/multicluster/remote_secret_test.go (about)

     1  // Copyright Istio Authors.
     2  //
     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  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package multicluster
    16  
    17  import (
    18  	"bytes"
    19  	"errors"
    20  	"fmt"
    21  	"path/filepath"
    22  	"strings"
    23  	"testing"
    24  	"time"
    25  
    26  	"github.com/google/go-cmp/cmp"
    27  	. "github.com/onsi/gomega"
    28  	"github.com/spf13/pflag"
    29  	v1 "k8s.io/api/core/v1"
    30  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    31  	"k8s.io/apimachinery/pkg/runtime"
    32  	"k8s.io/apimachinery/pkg/types"
    33  	"k8s.io/client-go/tools/clientcmd/api"
    34  
    35  	"istio.io/istio/istioctl/pkg/cli"
    36  	"istio.io/istio/operator/pkg/object"
    37  	"istio.io/istio/pkg/kube"
    38  	"istio.io/istio/pkg/kube/multicluster"
    39  	"istio.io/istio/pkg/test"
    40  	"istio.io/istio/pkg/test/env"
    41  )
    42  
    43  var (
    44  	kubeSystemNamespaceUID = types.UID("54643f96-eca0-11e9-bb97-42010a80000a")
    45  	kubeSystemNamespace    = &v1.Namespace{
    46  		ObjectMeta: metav1.ObjectMeta{
    47  			Name: "kube-system",
    48  			UID:  kubeSystemNamespaceUID,
    49  		},
    50  	}
    51  )
    52  
    53  const (
    54  	testNamespace          = "istio-system-test"
    55  	testServiceAccountName = "test-service-account"
    56  )
    57  
    58  func makeServiceAccount(secrets ...string) *v1.ServiceAccount {
    59  	sa := &v1.ServiceAccount{
    60  		ObjectMeta: metav1.ObjectMeta{
    61  			Name:      testServiceAccountName,
    62  			Namespace: testNamespace,
    63  		},
    64  	}
    65  
    66  	for _, secret := range secrets {
    67  		sa.Secrets = append(sa.Secrets, v1.ObjectReference{
    68  			Name:      secret,
    69  			Namespace: testNamespace,
    70  		})
    71  	}
    72  
    73  	return sa
    74  }
    75  
    76  func makeSecret(name, caData, token string) *v1.Secret {
    77  	out := &v1.Secret{
    78  		ObjectMeta: metav1.ObjectMeta{
    79  			Name:        name,
    80  			Namespace:   testNamespace,
    81  			Annotations: map[string]string{v1.ServiceAccountNameKey: testServiceAccountName},
    82  		},
    83  		Data: map[string][]byte{},
    84  		Type: v1.SecretTypeServiceAccountToken,
    85  	}
    86  	if len(caData) > 0 {
    87  		out.Data[v1.ServiceAccountRootCAKey] = []byte(caData)
    88  	}
    89  	if len(token) > 0 {
    90  		out.Data[v1.ServiceAccountTokenKey] = []byte(token)
    91  	}
    92  	return out
    93  }
    94  
    95  type fakeOutputWriter struct {
    96  	b           bytes.Buffer
    97  	injectError error
    98  	failAfter   int
    99  }
   100  
   101  func (w *fakeOutputWriter) Write(p []byte) (n int, err error) {
   102  	w.failAfter--
   103  	if w.failAfter <= 0 && w.injectError != nil {
   104  		return 0, w.injectError
   105  	}
   106  	return w.b.Write(p)
   107  }
   108  func (w *fakeOutputWriter) String() string { return w.b.String() }
   109  
   110  func TestCreateRemoteSecrets(t *testing.T) {
   111  	prevOutputWriterStub := makeOutputWriterTestHook
   112  	defer func() { makeOutputWriterTestHook = prevOutputWriterStub }()
   113  
   114  	prevTokenWaitBackoff := tokenWaitBackoff
   115  	defer func() { tokenWaitBackoff = prevTokenWaitBackoff }()
   116  
   117  	sa := makeServiceAccount("saSecret")
   118  	sa2 := makeServiceAccount("saSecret", "saSecret2")
   119  	saSecret := makeSecret("saSecret", "caData", "token")
   120  	saSecret2 := makeSecret("saSecret2", "caData", "token")
   121  	saSecretMissingToken := makeSecret("saSecret", "caData", "")
   122  
   123  	tokenWaitBackoff = 10 * time.Millisecond
   124  
   125  	cases := []struct {
   126  		testName string
   127  
   128  		objs       []runtime.Object
   129  		name       string
   130  		secType    SecretType
   131  		secretName string
   132  
   133  		// inject errors
   134  		badStartingConfig bool
   135  		outputWriterError error
   136  
   137  		want            string
   138  		wantErrStr      string
   139  		k8sMinorVersion string
   140  	}{
   141  		{
   142  			testName:   "fail to create remote secret token",
   143  			objs:       []runtime.Object{kubeSystemNamespace, sa, saSecretMissingToken},
   144  			wantErrStr: `no "token" data found`,
   145  		},
   146  		{
   147  			testName:          "fail to encode secret",
   148  			objs:              []runtime.Object{kubeSystemNamespace, sa, saSecret},
   149  			outputWriterError: errors.New("injected encode error"),
   150  			wantErrStr:        "injected encode error",
   151  		},
   152  		{
   153  			testName: "success",
   154  			objs:     []runtime.Object{kubeSystemNamespace, sa, saSecret},
   155  			name:     "cluster-foo",
   156  			want:     "cal-want",
   157  		},
   158  		{
   159  			testName: "success with type defined",
   160  			objs:     []runtime.Object{kubeSystemNamespace, sa, saSecret},
   161  			name:     "cluster-foo",
   162  			secType:  "config",
   163  			want:     "cal-want",
   164  		},
   165  		{
   166  			testName:   "failure due to multiple secrets",
   167  			objs:       []runtime.Object{kubeSystemNamespace, sa2, saSecret, saSecret2},
   168  			name:       "cluster-foo",
   169  			wantErrStr: "wrong number of secrets (2) in serviceaccount",
   170  			// for k8s 1.24+ we auto-create a secret instead of relying on a reference in service account
   171  			k8sMinorVersion: "23",
   172  		},
   173  		{
   174  			testName:   "success when specific secret name provided",
   175  			objs:       []runtime.Object{kubeSystemNamespace, sa2, saSecret, saSecret2},
   176  			secretName: saSecret.Name,
   177  			name:       "cluster-foo",
   178  			want:       "cal-want",
   179  		},
   180  		{
   181  			testName:        "fail when non-existing secret name provided",
   182  			objs:            []runtime.Object{kubeSystemNamespace, sa2, saSecret, saSecret2},
   183  			secretName:      "nonexistingSecret",
   184  			name:            "cluster-foo",
   185  			want:            "cal-want",
   186  			wantErrStr:      "provided secret does not exist",
   187  			k8sMinorVersion: "23",
   188  		},
   189  	}
   190  
   191  	for i := range cases {
   192  		c := &cases[i]
   193  		t.Run(fmt.Sprintf("[%v] %v", i, c.testName), func(tt *testing.T) {
   194  			makeOutputWriterTestHook = func() writer {
   195  				return &fakeOutputWriter{injectError: c.outputWriterError}
   196  			}
   197  			if c.secType != SecretTypeConfig {
   198  				c.secType = SecretTypeRemote
   199  			}
   200  			opts := RemoteSecretOptions{
   201  				ServiceAccountName: testServiceAccountName,
   202  				AuthType:           RemoteSecretAuthTypeBearerToken,
   203  				// ClusterName: testCluster,
   204  				KubeOptions: KubeOptions{
   205  					Namespace: testNamespace,
   206  				},
   207  				Type:       c.secType,
   208  				SecretName: c.secretName,
   209  			}
   210  
   211  			ctx := cli.NewFakeContext(&cli.NewFakeContextOption{
   212  				IstioNamespace: "istio-system",
   213  				Objects:        c.objs,
   214  				Namespace:      testNamespace,
   215  				Version:        c.k8sMinorVersion,
   216  			})
   217  			client, err := ctx.CLIClient()
   218  			if err != nil {
   219  				tt.Fatalf("failed to create client: %v", err)
   220  			}
   221  			got, _, err := CreateRemoteSecret(client, opts)
   222  			if c.wantErrStr != "" {
   223  				if err == nil {
   224  					tt.Fatalf("wanted error including %q but got none", c.wantErrStr)
   225  				} else if !strings.Contains(err.Error(), c.wantErrStr) {
   226  					tt.Fatalf("wanted error including %q but got %v", c.wantErrStr, err)
   227  				}
   228  			} else if c.wantErrStr == "" && err != nil {
   229  				tt.Fatalf("wanted non-error but got %q", err)
   230  			} else if c.want != "" {
   231  				var secretName, key string
   232  				switch c.secType {
   233  				case SecretTypeConfig:
   234  					secretName = configSecretName
   235  					key = configSecretKey
   236  				default:
   237  					secretName = remoteSecretPrefix + string(kubeSystemNamespaceUID)
   238  					key = "54643f96-eca0-11e9-bb97-42010a80000a"
   239  				}
   240  				wantOutput := fmt.Sprintf(`# This file is autogenerated, do not edit.
   241  apiVersion: v1
   242  kind: Secret
   243  metadata:
   244    annotations:
   245      %s: 54643f96-eca0-11e9-bb97-42010a80000a
   246    creationTimestamp: null
   247    labels:
   248      istio/multiCluster: "true"
   249    name: %s
   250    namespace: istio-system-test
   251  stringData:
   252    %s: |
   253      apiVersion: v1
   254      clusters:
   255      - cluster:
   256          certificate-authority-data: Y2FEYXRh
   257          server: server
   258        name: 54643f96-eca0-11e9-bb97-42010a80000a
   259      contexts:
   260      - context:
   261          cluster: 54643f96-eca0-11e9-bb97-42010a80000a
   262          user: 54643f96-eca0-11e9-bb97-42010a80000a
   263        name: 54643f96-eca0-11e9-bb97-42010a80000a
   264      current-context: 54643f96-eca0-11e9-bb97-42010a80000a
   265      kind: Config
   266      preferences: {}
   267      users:
   268      - name: 54643f96-eca0-11e9-bb97-42010a80000a
   269        user:
   270          token: token
   271  ---
   272  `, clusterNameAnnotationKey, secretName, key)
   273  
   274  				if diff := cmp.Diff(got, wantOutput); diff != "" {
   275  					tt.Errorf("got\n%v\nwant\n%vdiff %v", got, c.want, diff)
   276  				}
   277  			}
   278  		})
   279  	}
   280  }
   281  
   282  func TestGetServiceAccountSecretToken(t *testing.T) {
   283  	secret := makeSecret("secret", "caData", "token")
   284  
   285  	type tc struct {
   286  		name string
   287  		opts RemoteSecretOptions
   288  		objs []runtime.Object
   289  
   290  		want       *v1.Secret
   291  		wantErrStr string
   292  	}
   293  
   294  	commonCases := []tc{
   295  		{
   296  			name: "missing service account",
   297  			opts: RemoteSecretOptions{
   298  				ServiceAccountName: testServiceAccountName,
   299  				KubeOptions: KubeOptions{
   300  					Namespace: testNamespace,
   301  				},
   302  				ManifestsPath: filepath.Join(env.IstioSrc, "manifests"),
   303  			},
   304  			wantErrStr: fmt.Sprintf("serviceaccounts %q not found", testServiceAccountName),
   305  		},
   306  	}
   307  
   308  	legacyCases := append([]tc{
   309  		{
   310  			name: "wrong number of secrets",
   311  			opts: RemoteSecretOptions{
   312  				ServiceAccountName:   testServiceAccountName,
   313  				CreateServiceAccount: false,
   314  				KubeOptions: KubeOptions{
   315  					Namespace: testNamespace,
   316  				},
   317  				ManifestsPath: filepath.Join(env.IstioSrc, "manifests"),
   318  			},
   319  			objs: []runtime.Object{
   320  				makeServiceAccount("secret", "extra-secret"),
   321  			},
   322  			wantErrStr: "wrong number of secrets",
   323  		},
   324  		{
   325  			name: "missing service account token secret",
   326  			opts: RemoteSecretOptions{
   327  				ServiceAccountName: testServiceAccountName,
   328  				KubeOptions: KubeOptions{
   329  					Namespace: testNamespace,
   330  				},
   331  				ManifestsPath: filepath.Join(env.IstioSrc, "manifests"),
   332  			},
   333  			objs: []runtime.Object{
   334  				makeServiceAccount("wrong-secret"),
   335  				secret,
   336  			},
   337  			wantErrStr: `secrets "wrong-secret" not found`,
   338  		},
   339  		{
   340  			name: "success",
   341  			opts: RemoteSecretOptions{
   342  				ServiceAccountName: testServiceAccountName,
   343  				KubeOptions: KubeOptions{
   344  					Namespace: testNamespace,
   345  				},
   346  				ManifestsPath: filepath.Join(env.IstioSrc, "manifests"),
   347  			},
   348  			objs: []runtime.Object{
   349  				makeServiceAccount("secret"),
   350  				secret,
   351  			},
   352  			want: secret,
   353  		},
   354  	}, commonCases...)
   355  
   356  	cases := append([]tc{
   357  		{
   358  			name: "success",
   359  			opts: RemoteSecretOptions{
   360  				ServiceAccountName: testServiceAccountName,
   361  				KubeOptions: KubeOptions{
   362  					Namespace: testNamespace,
   363  				},
   364  				ManifestsPath: filepath.Join(env.IstioSrc, "manifests"),
   365  			},
   366  			objs: []runtime.Object{
   367  				makeServiceAccount(tokenSecretName(testServiceAccountName)),
   368  			},
   369  			want: &v1.Secret{
   370  				ObjectMeta: metav1.ObjectMeta{
   371  					Name:        tokenSecretName(testServiceAccountName),
   372  					Namespace:   testNamespace,
   373  					Annotations: map[string]string{v1.ServiceAccountNameKey: testServiceAccountName},
   374  				},
   375  				Type: v1.SecretTypeServiceAccountToken,
   376  			},
   377  		},
   378  	}, commonCases...)
   379  
   380  	doCase := func(t *testing.T, c tc, k8sMinorVer string) {
   381  		t.Run(fmt.Sprintf("%v", c.name), func(tt *testing.T) {
   382  			client := kube.NewFakeClientWithVersion(k8sMinorVer, c.objs...)
   383  			got, err := getServiceAccountSecret(client, c.opts)
   384  			if c.wantErrStr != "" {
   385  				if err == nil {
   386  					tt.Fatalf("wanted error including %q but got none", c.wantErrStr)
   387  				} else if !strings.Contains(err.Error(), c.wantErrStr) {
   388  					tt.Fatalf("wanted error including %q but got %v", c.wantErrStr, err)
   389  				}
   390  			} else if c.wantErrStr == "" && err != nil {
   391  				tt.Fatalf("wanted non-error but got %q", err)
   392  			} else if diff := cmp.Diff(got, c.want); diff != "" {
   393  				tt.Errorf("got\n%v\nwant\n%vdiff %v", got, c.want, diff)
   394  			}
   395  		})
   396  	}
   397  
   398  	t.Run("kubernetes created secret (legacy)", func(t *testing.T) {
   399  		for _, c := range legacyCases {
   400  			doCase(t, c, "23")
   401  		}
   402  	})
   403  	t.Run("istioctl created secret", func(t *testing.T) {
   404  		for _, c := range cases {
   405  			doCase(t, c, "")
   406  		}
   407  	})
   408  }
   409  
   410  func TestGenerateServiceAccount(t *testing.T) {
   411  	opts := RemoteSecretOptions{
   412  		CreateServiceAccount: true,
   413  		ManifestsPath:        filepath.Join(env.IstioSrc, "manifests"),
   414  		KubeOptions: KubeOptions{
   415  			Namespace: "istio-system",
   416  		},
   417  	}
   418  	yaml, err := generateServiceAccountYAML(opts)
   419  	if err != nil {
   420  		t.Fatalf("failed to generate service account YAML: %v", err)
   421  	}
   422  	objs, err := object.ParseK8sObjectsFromYAMLManifest(yaml)
   423  	if err != nil {
   424  		t.Fatalf("could not parse k8s objects from generated YAML: %v", err)
   425  	}
   426  
   427  	mustFindObject(t, objs, "istio-reader-service-account", "ServiceAccount")
   428  	mustFindObject(t, objs, "istio-reader-clusterrole-istio-system", "ClusterRole")
   429  	mustFindObject(t, objs, "istio-reader-clusterrole-istio-system", "ClusterRoleBinding")
   430  }
   431  
   432  func mustFindObject(t test.Failer, objs object.K8sObjects, name, kind string) {
   433  	t.Helper()
   434  	var obj *object.K8sObject
   435  	for _, o := range objs {
   436  		if o.Kind == kind && o.Name == name {
   437  			obj = o
   438  			break
   439  		}
   440  	}
   441  	if obj == nil {
   442  		t.Fatalf("expected %v/%v", name, kind)
   443  	}
   444  }
   445  
   446  func TestCreateRemoteKubeconfig(t *testing.T) {
   447  	fakeClusterName := "fake-clusterName-0"
   448  	kubeconfig := strings.ReplaceAll(`apiVersion: v1
   449  clusters:
   450  - cluster:
   451      certificate-authority-data: Y2FEYXRh
   452      server: https://1.2.3.4
   453    name: {cluster}
   454  contexts:
   455  - context:
   456      cluster: {cluster}
   457      user: {cluster}
   458    name: {cluster}
   459  current-context: {cluster}
   460  kind: Config
   461  preferences: {}
   462  users:
   463  - name: {cluster}
   464    user:
   465      token: token
   466  `, "{cluster}", fakeClusterName)
   467  
   468  	cases := []struct {
   469  		name               string
   470  		clusterName        string
   471  		context            string
   472  		server             string
   473  		haveTokenSecret    *v1.Secret
   474  		updatedTokenSecret *v1.Secret
   475  		wantRemoteSecret   *v1.Secret
   476  		wantErrStr         string
   477  	}{
   478  		{
   479  			name:            "missing caData",
   480  			haveTokenSecret: makeSecret("", "", "token"),
   481  			context:         "c0",
   482  			clusterName:     fakeClusterName,
   483  			wantErrStr:      errMissingRootCAKey.Error(),
   484  		},
   485  		{
   486  			name:            "missing token",
   487  			haveTokenSecret: makeSecret("", "caData", ""),
   488  			context:         "c0",
   489  			clusterName:     fakeClusterName,
   490  			wantErrStr:      errMissingTokenKey.Error(),
   491  		},
   492  		{
   493  			name:            "bad server name",
   494  			haveTokenSecret: makeSecret("", "caData", "token"),
   495  			context:         "c0",
   496  			clusterName:     fakeClusterName,
   497  			server:          "",
   498  			wantErrStr:      "invalid kubeconfig:",
   499  		},
   500  		{
   501  			name:               "success after wait",
   502  			haveTokenSecret:    makeSecret("", "caData", ""),
   503  			updatedTokenSecret: makeSecret("", "caData", "token"), // token is populated later
   504  			context:            "c0",
   505  			clusterName:        fakeClusterName,
   506  			server:             "https://1.2.3.4",
   507  			wantRemoteSecret: &v1.Secret{
   508  				ObjectMeta: metav1.ObjectMeta{
   509  					Name: remoteSecretNameFromClusterName(fakeClusterName),
   510  					Annotations: map[string]string{
   511  						clusterNameAnnotationKey: fakeClusterName,
   512  					},
   513  					Labels: map[string]string{
   514  						multicluster.MultiClusterSecretLabel: "true",
   515  					},
   516  				},
   517  				Data: map[string][]byte{
   518  					fakeClusterName: []byte(kubeconfig),
   519  				},
   520  			},
   521  		},
   522  		{
   523  			name:            "success",
   524  			haveTokenSecret: makeSecret("", "caData", "token"),
   525  			context:         "c0",
   526  			clusterName:     fakeClusterName,
   527  			server:          "https://1.2.3.4",
   528  			wantRemoteSecret: &v1.Secret{
   529  				ObjectMeta: metav1.ObjectMeta{
   530  					Name: remoteSecretNameFromClusterName(fakeClusterName),
   531  					Annotations: map[string]string{
   532  						clusterNameAnnotationKey: fakeClusterName,
   533  					},
   534  					Labels: map[string]string{
   535  						multicluster.MultiClusterSecretLabel: "true",
   536  					},
   537  				},
   538  				Data: map[string][]byte{
   539  					fakeClusterName: []byte(kubeconfig),
   540  				},
   541  			},
   542  		},
   543  	}
   544  	oldBackoff := tokenWaitBackoff
   545  	tokenWaitBackoff = time.Millisecond
   546  	t.Cleanup(func() {
   547  		tokenWaitBackoff = oldBackoff
   548  	})
   549  	for i := range cases {
   550  		c := &cases[i]
   551  		secName := remoteSecretNameFromClusterName(c.clusterName)
   552  		t.Run(fmt.Sprintf("[%v] %v", i, c.name), func(tt *testing.T) {
   553  			// no updateTokenSecret means re-fetching yields the same result
   554  			obj := []runtime.Object{c.haveTokenSecret}
   555  			if c.updatedTokenSecret != nil {
   556  				// fetching should give a different result than the token secret we pass in
   557  				obj = []runtime.Object{c.updatedTokenSecret}
   558  			}
   559  			client := kube.NewFakeClient(obj...)
   560  
   561  			got, err := createRemoteSecretFromTokenAndServer(client, c.haveTokenSecret, c.clusterName, c.server, secName)
   562  			if c.wantErrStr != "" {
   563  				if err == nil {
   564  					tt.Fatalf("wanted error including %q but none", c.wantErrStr)
   565  				} else if !strings.Contains(err.Error(), c.wantErrStr) {
   566  					tt.Fatalf("wanted error including %q but %v", c.wantErrStr, err)
   567  				}
   568  			} else if c.wantErrStr == "" && err != nil {
   569  				tt.Fatalf("wanted non-error but got %q", err)
   570  			} else if diff := cmp.Diff(got, c.wantRemoteSecret); diff != "" {
   571  				tt.Fatalf(" got %v\nwant %v\ndiff %v", got, c.wantRemoteSecret, diff)
   572  			}
   573  		})
   574  	}
   575  }
   576  
   577  func TestWriteEncodedSecret(t *testing.T) {
   578  	s := &v1.Secret{
   579  		ObjectMeta: metav1.ObjectMeta{
   580  			Name: "foo",
   581  		},
   582  	}
   583  
   584  	w := &fakeOutputWriter{failAfter: 0, injectError: errors.New("error")}
   585  	if err := writeEncodedObject(w, s); err == nil {
   586  		t.Error("want error on local write failure")
   587  	}
   588  
   589  	w = &fakeOutputWriter{failAfter: 1, injectError: errors.New("error")}
   590  	if err := writeEncodedObject(w, s); err == nil {
   591  		t.Error("want error on remote write failure")
   592  	}
   593  
   594  	w = &fakeOutputWriter{failAfter: 2, injectError: errors.New("error")}
   595  	if err := writeEncodedObject(w, s); err == nil {
   596  		t.Error("want error on third write failure")
   597  	}
   598  
   599  	w = &fakeOutputWriter{}
   600  	if err := writeEncodedObject(w, s); err != nil {
   601  		t.Errorf("unexpected error: %v", err)
   602  	}
   603  
   604  	want := `# This file is autogenerated, do not edit.
   605  apiVersion: v1
   606  kind: Secret
   607  metadata:
   608    creationTimestamp: null
   609    name: foo
   610  ---
   611  `
   612  	if w.String() != want {
   613  		t.Errorf("got\n%q\nwant\n%q", w.String(), want)
   614  	}
   615  }
   616  
   617  func TestCreateRemoteSecretFromPlugin(t *testing.T) {
   618  	fakeClusterName := "fake-clusterName-0"
   619  	kubeconfig := strings.ReplaceAll(`apiVersion: v1
   620  clusters:
   621  - cluster:
   622      certificate-authority-data: Y2FEYXRh
   623      server: https://1.2.3.4
   624    name: {cluster}
   625  contexts:
   626  - context:
   627      cluster: {cluster}
   628      user: {cluster}
   629    name: {cluster}
   630  current-context: {cluster}
   631  kind: Config
   632  preferences: {}
   633  users:
   634  - name: {cluster}
   635    user:
   636      auth-provider:
   637        config:
   638          k1: v1
   639        name: foobar
   640  `, "{cluster}", fakeClusterName)
   641  
   642  	cases := []struct {
   643  		name               string
   644  		in                 *v1.Secret
   645  		context            string
   646  		clusterName        string
   647  		server             string
   648  		authProviderConfig *api.AuthProviderConfig
   649  		want               *v1.Secret
   650  		wantErrStr         string
   651  	}{
   652  		{
   653  			name:        "error on missing caData",
   654  			in:          makeSecret("", "", "token"),
   655  			context:     "c0",
   656  			clusterName: fakeClusterName,
   657  			wantErrStr:  errMissingRootCAKey.Error(),
   658  		},
   659  		{
   660  			name:        "success on missing token",
   661  			in:          makeSecret("", "caData", ""),
   662  			context:     "c0",
   663  			clusterName: fakeClusterName,
   664  			server:      "https://1.2.3.4",
   665  			authProviderConfig: &api.AuthProviderConfig{
   666  				Name: "foobar",
   667  				Config: map[string]string{
   668  					"k1": "v1",
   669  				},
   670  			},
   671  			want: &v1.Secret{
   672  				ObjectMeta: metav1.ObjectMeta{
   673  					Name: remoteSecretNameFromClusterName(fakeClusterName),
   674  					Annotations: map[string]string{
   675  						clusterNameAnnotationKey: fakeClusterName,
   676  					},
   677  					Labels: map[string]string{
   678  						multicluster.MultiClusterSecretLabel: "true",
   679  					},
   680  				},
   681  				Data: map[string][]byte{
   682  					fakeClusterName: []byte(kubeconfig),
   683  				},
   684  			},
   685  		},
   686  		{
   687  			name:        "success",
   688  			in:          makeSecret("", "caData", "token"),
   689  			context:     "c0",
   690  			clusterName: fakeClusterName,
   691  			server:      "https://1.2.3.4",
   692  			authProviderConfig: &api.AuthProviderConfig{
   693  				Name: "foobar",
   694  				Config: map[string]string{
   695  					"k1": "v1",
   696  				},
   697  			},
   698  			want: &v1.Secret{
   699  				ObjectMeta: metav1.ObjectMeta{
   700  					Name: remoteSecretNameFromClusterName(fakeClusterName),
   701  					Annotations: map[string]string{
   702  						clusterNameAnnotationKey: fakeClusterName,
   703  					},
   704  					Labels: map[string]string{
   705  						multicluster.MultiClusterSecretLabel: "true",
   706  					},
   707  				},
   708  				Data: map[string][]byte{
   709  					fakeClusterName: []byte(kubeconfig),
   710  				},
   711  			},
   712  		},
   713  	}
   714  
   715  	for i := range cases {
   716  		c := &cases[i]
   717  		secName := remoteSecretNameFromClusterName(c.clusterName)
   718  		t.Run(fmt.Sprintf("[%v] %v", i, c.name), func(tt *testing.T) {
   719  			got, err := createRemoteSecretFromPlugin(c.in, c.server, c.clusterName, secName, c.authProviderConfig)
   720  			if c.wantErrStr != "" {
   721  				if err == nil {
   722  					tt.Fatalf("wanted error including %q but none", c.wantErrStr)
   723  				} else if !strings.Contains(err.Error(), c.wantErrStr) {
   724  					tt.Fatalf("wanted error including %q but %v", c.wantErrStr, err)
   725  				}
   726  			} else if c.wantErrStr == "" && err != nil {
   727  				tt.Fatalf("wanted non-error but got %q", err)
   728  			} else if diff := cmp.Diff(got, c.want); diff != "" {
   729  				tt.Fatalf(" got %v\nwant %v\ndiff %v", got, c.want, diff)
   730  			}
   731  		})
   732  	}
   733  }
   734  
   735  func TestRemoteSecretOptions(t *testing.T) {
   736  	g := NewWithT(t)
   737  
   738  	ctx := cli.NewFakeContext(nil)
   739  	o := RemoteSecretOptions{}
   740  	flags := pflag.NewFlagSet("test", pflag.ContinueOnError)
   741  	o.addFlags(flags)
   742  	g.Expect(flags.Parse([]string{
   743  		"--name",
   744  		"valid-name",
   745  	})).Should(Succeed())
   746  	g.Expect(o.prepare(ctx)).Should(Succeed())
   747  
   748  	o = RemoteSecretOptions{}
   749  	flags = pflag.NewFlagSet("test", pflag.ContinueOnError)
   750  	o.addFlags(flags)
   751  	g.Expect(flags.Parse([]string{
   752  		"--name",
   753  		"?-invalid-name",
   754  	})).Should(Succeed())
   755  	g.Expect(o.prepare(ctx)).Should(Not(Succeed()))
   756  }