istio.io/istio@v0.0.0-20240520182934-d79c90f27776/istioctl/pkg/multicluster/remote_secret.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  	"context"
    20  	"fmt"
    21  	"io"
    22  	"strings"
    23  	"time"
    24  
    25  	"github.com/cenkalti/backoff/v4"
    26  	"github.com/spf13/cobra"
    27  	"github.com/spf13/pflag"
    28  	v1 "k8s.io/api/core/v1"
    29  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    30  	"k8s.io/apimachinery/pkg/runtime"
    31  	"k8s.io/apimachinery/pkg/runtime/serializer/json"
    32  	"k8s.io/apimachinery/pkg/runtime/serializer/versioning"
    33  	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
    34  	_ "k8s.io/client-go/plugin/pkg/client/auth" //  to avoid 'No Auth Provider found for name "gcp"'
    35  	"k8s.io/client-go/tools/clientcmd"
    36  	"k8s.io/client-go/tools/clientcmd/api"
    37  	"k8s.io/client-go/tools/clientcmd/api/latest"
    38  
    39  	"istio.io/istio/istioctl/pkg/cli"
    40  	"istio.io/istio/istioctl/pkg/util"
    41  	"istio.io/istio/operator/pkg/helm"
    42  	"istio.io/istio/pkg/config/constants"
    43  	"istio.io/istio/pkg/config/labels"
    44  	"istio.io/istio/pkg/kube"
    45  	"istio.io/istio/pkg/kube/multicluster"
    46  	"istio.io/istio/pkg/log"
    47  )
    48  
    49  var (
    50  	codec  runtime.Codec
    51  	scheme *runtime.Scheme
    52  
    53  	tokenWaitBackoff = time.Second
    54  )
    55  
    56  func init() {
    57  	scheme = runtime.NewScheme()
    58  	utilruntime.Must(v1.AddToScheme(scheme))
    59  	opt := json.SerializerOptions{
    60  		Yaml:   true,
    61  		Pretty: false,
    62  		Strict: false,
    63  	}
    64  	yamlSerializer := json.NewSerializerWithOptions(json.DefaultMetaFactory, scheme, scheme, opt)
    65  	codec = versioning.NewDefaultingCodecForScheme(
    66  		scheme,
    67  		yamlSerializer,
    68  		yamlSerializer,
    69  		v1.SchemeGroupVersion,
    70  		runtime.InternalGroupVersioner,
    71  	)
    72  }
    73  
    74  const (
    75  	remoteSecretPrefix = "istio-remote-secret-"
    76  	configSecretName   = "istio-kubeconfig"
    77  	configSecretKey    = "config"
    78  )
    79  
    80  func remoteSecretNameFromClusterName(clusterName string) string {
    81  	return remoteSecretPrefix + clusterName
    82  }
    83  
    84  // NewCreateRemoteSecretCommand creates a new command for joining two contexts
    85  // together in a multi-cluster mesh.
    86  func NewCreateRemoteSecretCommand(ctx cli.Context) *cobra.Command {
    87  	opts := RemoteSecretOptions{
    88  		AuthType:         RemoteSecretAuthTypeBearerToken,
    89  		AuthPluginConfig: make(map[string]string),
    90  		Type:             SecretTypeRemote,
    91  	}
    92  	c := &cobra.Command{
    93  		Use:   "create-remote-secret",
    94  		Short: "Create a secret with credentials to allow Istio to access remote Kubernetes apiservers",
    95  		Example: `  # Create a secret to access cluster c0's apiserver and install it in cluster c1.
    96    istioctl --kubeconfig=c0.yaml create-remote-secret --name c0 \
    97      | kubectl --kubeconfig=c1.yaml apply -f -
    98  
    99    # Delete a secret that was previously installed in c1
   100    istioctl --kubeconfig=c0.yaml create-remote-secret --name c0 \
   101      | kubectl --kubeconfig=c1.yaml delete -f -
   102  
   103    # Create a secret access a remote cluster with an auth plugin
   104    istioctl --kubeconfig=c0.yaml create-remote-secret --name c0 --auth-type=plugin --auth-plugin-name=gcp \
   105      | kubectl --kubeconfig=c1.yaml apply -f -`,
   106  		Args: cobra.NoArgs,
   107  		RunE: func(c *cobra.Command, args []string) error {
   108  			if err := opts.prepare(ctx); err != nil {
   109  				return err
   110  			}
   111  			client, err := ctx.CLIClient()
   112  			if err != nil {
   113  				return err
   114  			}
   115  			out, warn, err := CreateRemoteSecret(client, opts)
   116  			if err != nil {
   117  				_, _ = fmt.Fprintf(c.OutOrStderr(), "error: %v\n", err)
   118  				return err
   119  			}
   120  			if warn != nil {
   121  				_, _ = fmt.Fprintf(c.OutOrStderr(), "warn: %v\n", warn)
   122  			}
   123  			_, _ = fmt.Fprint(c.OutOrStdout(), out)
   124  			return nil
   125  		},
   126  	}
   127  	opts.addFlags(c.PersistentFlags())
   128  	return c
   129  }
   130  
   131  func createRemoteServiceAccountSecret(kubeconfig *api.Config, clusterName, secName string) (*v1.Secret, error) { // nolint:interfacer
   132  	var data bytes.Buffer
   133  	if err := latest.Codec.Encode(kubeconfig, &data); err != nil {
   134  		return nil, err
   135  	}
   136  	key := clusterName
   137  	if secName == configSecretName {
   138  		key = configSecretKey
   139  	}
   140  	out := &v1.Secret{
   141  		ObjectMeta: metav1.ObjectMeta{
   142  			Name: secName,
   143  			Annotations: map[string]string{
   144  				clusterNameAnnotationKey: clusterName,
   145  			},
   146  			Labels: map[string]string{
   147  				multicluster.MultiClusterSecretLabel: "true",
   148  			},
   149  		},
   150  		Data: map[string][]byte{
   151  			key: data.Bytes(),
   152  		},
   153  	}
   154  	return out, nil
   155  }
   156  
   157  func createBaseKubeconfig(caData []byte, clusterName, server string) *api.Config {
   158  	return &api.Config{
   159  		Clusters: map[string]*api.Cluster{
   160  			clusterName: {
   161  				CertificateAuthorityData: caData,
   162  				Server:                   server,
   163  			},
   164  		},
   165  		AuthInfos: map[string]*api.AuthInfo{},
   166  		Contexts: map[string]*api.Context{
   167  			clusterName: {
   168  				Cluster:  clusterName,
   169  				AuthInfo: clusterName,
   170  			},
   171  		},
   172  		CurrentContext: clusterName,
   173  	}
   174  }
   175  
   176  func createBearerTokenKubeconfig(caData, token []byte, clusterName, server string) *api.Config {
   177  	c := createBaseKubeconfig(caData, clusterName, server)
   178  	c.AuthInfos[c.CurrentContext] = &api.AuthInfo{
   179  		Token: string(token),
   180  	}
   181  	return c
   182  }
   183  
   184  func createPluginKubeconfig(caData []byte, clusterName, server string, authProviderConfig *api.AuthProviderConfig) *api.Config {
   185  	c := createBaseKubeconfig(caData, clusterName, server)
   186  	c.AuthInfos[c.CurrentContext] = &api.AuthInfo{
   187  		AuthProvider: authProviderConfig,
   188  	}
   189  	return c
   190  }
   191  
   192  func createRemoteSecretFromPlugin(
   193  	tokenSecret *v1.Secret,
   194  	server, clusterName, secName string,
   195  	authProviderConfig *api.AuthProviderConfig,
   196  ) (*v1.Secret, error) {
   197  	caData, ok := tokenSecret.Data[v1.ServiceAccountRootCAKey]
   198  	if !ok {
   199  		return nil, errMissingRootCAKey
   200  	}
   201  
   202  	// Create a Kubeconfig to access the remote cluster using the auth provider plugin.
   203  	kubeconfig := createPluginKubeconfig(caData, clusterName, server, authProviderConfig)
   204  	if err := clientcmd.Validate(*kubeconfig); err != nil {
   205  		return nil, fmt.Errorf("invalid kubeconfig: %v", err)
   206  	}
   207  
   208  	// Encode the Kubeconfig in a secret that can be loaded by Istio to dynamically discover and access the remote cluster.
   209  	return createRemoteServiceAccountSecret(kubeconfig, clusterName, secName)
   210  }
   211  
   212  var (
   213  	errMissingRootCAKey = fmt.Errorf("no %q data found", v1.ServiceAccountRootCAKey)
   214  	errMissingTokenKey  = fmt.Errorf("no %q data found", v1.ServiceAccountTokenKey)
   215  )
   216  
   217  func createRemoteSecretFromTokenAndServer(client kube.CLIClient, tokenSecret *v1.Secret, clusterName, server, secName string) (*v1.Secret, error) {
   218  	caData, token, err := waitForTokenData(client, tokenSecret)
   219  	if err != nil {
   220  		return nil, err
   221  	}
   222  
   223  	// Create a Kubeconfig to access the remote cluster using the remote service account credentials.
   224  	kubeconfig := createBearerTokenKubeconfig(caData, token, clusterName, server)
   225  	if err := clientcmd.Validate(*kubeconfig); err != nil {
   226  		return nil, fmt.Errorf("invalid kubeconfig: %v", err)
   227  	}
   228  
   229  	// Encode the Kubeconfig in a secret that can be loaded by Istio to dynamically discover and access the remote cluster.
   230  	return createRemoteServiceAccountSecret(kubeconfig, clusterName, secName)
   231  }
   232  
   233  func waitForTokenData(client kube.CLIClient, secret *v1.Secret) (ca, token []byte, err error) {
   234  	ca, token, err = tokenDataFromSecret(secret)
   235  	if err == nil {
   236  		return
   237  	}
   238  
   239  	log.Infof("Waiting for data to be populated in %s", secret.Name)
   240  	err = backoff.Retry(
   241  		func() error {
   242  			secret, err = client.Kube().CoreV1().Secrets(secret.Namespace).Get(context.TODO(), secret.Name, metav1.GetOptions{})
   243  			if err != nil {
   244  				return err
   245  			}
   246  			ca, token, err = tokenDataFromSecret(secret)
   247  			return err
   248  		},
   249  		backoff.WithMaxRetries(backoff.NewConstantBackOff(tokenWaitBackoff), 5))
   250  	return
   251  }
   252  
   253  func tokenDataFromSecret(tokenSecret *v1.Secret) (ca, token []byte, err error) {
   254  	var ok bool
   255  	ca, ok = tokenSecret.Data[v1.ServiceAccountRootCAKey]
   256  	if !ok {
   257  		err = errMissingRootCAKey
   258  		return
   259  	}
   260  	token, ok = tokenSecret.Data[v1.ServiceAccountTokenKey]
   261  	if !ok {
   262  		err = errMissingTokenKey
   263  		return
   264  	}
   265  	return
   266  }
   267  
   268  func getServiceAccountSecret(client kube.CLIClient, opt RemoteSecretOptions) (*v1.Secret, error) {
   269  	// Create the service account if it doesn't exist.
   270  	serviceAccount, err := getOrCreateServiceAccount(client, opt)
   271  	if err != nil {
   272  		return nil, err
   273  	}
   274  
   275  	if !kube.IsAtLeastVersion(client, 24) {
   276  		return legacyGetServiceAccountSecret(serviceAccount, client, opt)
   277  	}
   278  	return getOrCreateServiceAccountSecret(serviceAccount, client, opt)
   279  }
   280  
   281  // In Kubernetes 1.24+ we can't assume the secrets will be referenced in the ServiceAccount or be created automatically.
   282  // See https://github.com/istio/istio/issues/38246
   283  func getOrCreateServiceAccountSecret(
   284  	serviceAccount *v1.ServiceAccount,
   285  	client kube.CLIClient,
   286  	opt RemoteSecretOptions,
   287  ) (*v1.Secret, error) {
   288  	ctx := context.TODO()
   289  
   290  	// manually specified secret, make sure it references the ServiceAccount
   291  	if opt.SecretName != "" {
   292  		secret, err := client.Kube().CoreV1().Secrets(opt.Namespace).Get(ctx, opt.SecretName, metav1.GetOptions{})
   293  		if err != nil {
   294  			return nil, fmt.Errorf("could not get specified secret %s/%s: %v",
   295  				opt.Namespace, opt.SecretName, err)
   296  		}
   297  		if err := secretReferencesServiceAccount(serviceAccount, secret); err != nil {
   298  			return nil, err
   299  		}
   300  		return secret, nil
   301  	}
   302  
   303  	// first try to find an existing secret that references the SA
   304  	// TODO will the SA have any reference to secrets anymore, can we avoid this list?
   305  	allSecrets, err := client.Kube().CoreV1().Secrets(opt.Namespace).List(ctx, metav1.ListOptions{})
   306  	if err != nil {
   307  		return nil, fmt.Errorf("failed listing secrets in %s: %v", opt.Namespace, err)
   308  	}
   309  	for _, item := range allSecrets.Items {
   310  		secret := item
   311  		if secretReferencesServiceAccount(serviceAccount, &secret) == nil {
   312  			return &secret, nil
   313  		}
   314  	}
   315  
   316  	// finally, create the sa token secret manually
   317  	// https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/#manually-create-a-service-account-api-token
   318  	// TODO ephemeral time-based tokens are preferred; we should re-think this
   319  	log.Infof("Creating token secret for service account %q", serviceAccount.Name)
   320  	secretName := tokenSecretName(serviceAccount.Name)
   321  	return client.Kube().CoreV1().Secrets(opt.Namespace).Create(ctx, &v1.Secret{
   322  		ObjectMeta: metav1.ObjectMeta{
   323  			Name:        secretName,
   324  			Annotations: map[string]string{v1.ServiceAccountNameKey: serviceAccount.Name},
   325  		},
   326  		Type: v1.SecretTypeServiceAccountToken,
   327  	}, metav1.CreateOptions{})
   328  }
   329  
   330  func tokenSecretName(saName string) string {
   331  	return saName + "-istio-remote-secret-token"
   332  }
   333  
   334  func secretReferencesServiceAccount(serviceAccount *v1.ServiceAccount, secret *v1.Secret) error {
   335  	if secret.Type != v1.SecretTypeServiceAccountToken ||
   336  		secret.Annotations[v1.ServiceAccountNameKey] != serviceAccount.Name {
   337  		return fmt.Errorf("secret %s/%s does not reference ServiceAccount %s",
   338  			secret.Namespace, secret.Name, serviceAccount.Name)
   339  	}
   340  	return nil
   341  }
   342  
   343  func legacyGetServiceAccountSecret(
   344  	serviceAccount *v1.ServiceAccount,
   345  	client kube.CLIClient,
   346  	opt RemoteSecretOptions,
   347  ) (*v1.Secret, error) {
   348  	if len(serviceAccount.Secrets) == 0 {
   349  		return nil, fmt.Errorf("no secret found in the service account: %s", serviceAccount)
   350  	}
   351  
   352  	secretName := ""
   353  	secretNamespace := ""
   354  	if opt.SecretName != "" {
   355  		found := false
   356  		for _, secret := range serviceAccount.Secrets {
   357  			if secret.Name == opt.SecretName {
   358  				found = true
   359  				secretName = secret.Name
   360  				secretNamespace = secret.Namespace
   361  				break
   362  			}
   363  		}
   364  		if !found {
   365  			return nil, fmt.Errorf("provided secret does not exist: %s", opt.SecretName)
   366  		}
   367  	} else {
   368  		if len(serviceAccount.Secrets) == 1 {
   369  			secretName = serviceAccount.Secrets[0].Name
   370  			secretNamespace = serviceAccount.Secrets[0].Namespace
   371  		} else {
   372  			return nil, fmt.Errorf("wrong number of secrets (%v) in serviceaccount %s/%s, please use --secret-name to specify one",
   373  				len(serviceAccount.Secrets), opt.Namespace, opt.ServiceAccountName)
   374  		}
   375  	}
   376  
   377  	if secretNamespace == "" {
   378  		secretNamespace = opt.Namespace
   379  	}
   380  	return client.Kube().CoreV1().Secrets(secretNamespace).Get(context.TODO(), secretName, metav1.GetOptions{})
   381  }
   382  
   383  func getOrCreateServiceAccount(client kube.CLIClient, opt RemoteSecretOptions) (*v1.ServiceAccount, error) {
   384  	if sa, err := client.Kube().CoreV1().ServiceAccounts(opt.Namespace).Get(
   385  		context.TODO(), opt.ServiceAccountName, metav1.GetOptions{}); err == nil {
   386  		return sa, nil
   387  	} else if !opt.CreateServiceAccount {
   388  		// User chose not to automatically create the service account.
   389  		return nil, fmt.Errorf("failed retrieving service account %s.%s required for creating "+
   390  			"the remote secret (hint: try installing a minimal Istio profile on the cluster first, "+
   391  			"or run with '--create-service-account=true'): %v",
   392  			opt.ServiceAccountName,
   393  			opt.Namespace,
   394  			err)
   395  	}
   396  
   397  	if err := createServiceAccount(client, opt); err != nil {
   398  		return nil, err
   399  	}
   400  
   401  	// Return the newly created service account.
   402  	sa, err := client.Kube().CoreV1().ServiceAccounts(opt.Namespace).Get(
   403  		context.TODO(), opt.ServiceAccountName, metav1.GetOptions{})
   404  	if err != nil {
   405  		return nil, fmt.Errorf("failed retrieving service account %s.%s after creating it: %v",
   406  			opt.ServiceAccountName, opt.Namespace, err)
   407  	}
   408  	return sa, nil
   409  }
   410  
   411  func createServiceAccount(client kube.CLIClient, opt RemoteSecretOptions) error {
   412  	yaml, err := generateServiceAccountYAML(opt)
   413  	if err != nil {
   414  		return err
   415  	}
   416  
   417  	// Before we can apply the yaml, we have to ensure the system namespace exists.
   418  	if err := createNamespaceIfNotExist(client, opt.Namespace); err != nil {
   419  		return err
   420  	}
   421  
   422  	// Apply the YAML to the cluster.
   423  	return client.ApplyYAMLContents(opt.Namespace, yaml)
   424  }
   425  
   426  func generateServiceAccountYAML(opt RemoteSecretOptions) (string, error) {
   427  	// Create a renderer for the base installation.
   428  	baseRenderer := helm.NewHelmRenderer(opt.ManifestsPath, "base", "Base", opt.Namespace, nil)
   429  	discoveryRenderer := helm.NewHelmRenderer(opt.ManifestsPath, "istio-control/istio-discovery", "Pilot", opt.Namespace, nil)
   430  
   431  	baseTemplates := []string{"reader-serviceaccount.yaml"}
   432  	discoveryTemplates := []string{"clusterrole.yaml", "clusterrolebinding.yaml"}
   433  
   434  	if err := baseRenderer.Run(); err != nil {
   435  		return "", fmt.Errorf("failed running base Helm renderer: %w", err)
   436  	}
   437  	if err := discoveryRenderer.Run(); err != nil {
   438  		return "", fmt.Errorf("failed running base discovery Helm renderer: %w", err)
   439  	}
   440  
   441  	values := fmt.Sprintf(`
   442  global:
   443    istioNamespace: %s
   444  `, opt.Namespace)
   445  
   446  	// Render the templates required for the service account and role bindings.
   447  	baseContent, err := baseRenderer.RenderManifestFiltered(values, func(template string) bool {
   448  		for _, t := range baseTemplates {
   449  			if strings.Contains(template, t) {
   450  				return true
   451  			}
   452  		}
   453  		return false
   454  	})
   455  	if err != nil {
   456  		return "", fmt.Errorf("failed rendering base manifest: %w", err)
   457  	}
   458  	discoveryContent, err := discoveryRenderer.RenderManifestFiltered(values, func(template string) bool {
   459  		for _, t := range discoveryTemplates {
   460  			if strings.Contains(template, t) {
   461  				return true
   462  			}
   463  		}
   464  		return false
   465  	})
   466  	if err != nil {
   467  		return "", fmt.Errorf("failed rendering discovery manifest: %w", err)
   468  	}
   469  
   470  	aggregateContent := fmt.Sprintf(`
   471  %s
   472  ---
   473  %s
   474  `, baseContent, discoveryContent)
   475  	return aggregateContent, nil
   476  }
   477  
   478  func createNamespaceIfNotExist(client kube.Client, ns string) error {
   479  	if _, err := client.Kube().CoreV1().Namespaces().Get(context.TODO(), ns, metav1.GetOptions{}); err != nil {
   480  		if _, err := client.Kube().CoreV1().Namespaces().Create(context.TODO(), &v1.Namespace{
   481  			ObjectMeta: metav1.ObjectMeta{
   482  				Name: ns,
   483  			},
   484  		}, metav1.CreateOptions{}); err != nil {
   485  			return fmt.Errorf("failed creating namespace %s: %v", ns, err)
   486  		}
   487  	}
   488  	return nil
   489  }
   490  
   491  func getServerFromKubeconfig(client kube.CLIClient) (string, Warning, error) {
   492  	restCfg := client.RESTConfig()
   493  	if restCfg == nil {
   494  		return "", nil, fmt.Errorf("failed getting REST config from client")
   495  	}
   496  	server := restCfg.Host
   497  	if strings.Contains(server, "127.0.0.1") || strings.Contains(server, "localhost") {
   498  		return server, fmt.Errorf(
   499  			"server in Kubeconfig is %s. This is likely not reachable from inside the cluster, "+
   500  				"if you're using Kubernetes in Docker, pass --server with the container IP for the API Server",
   501  			server), nil
   502  	}
   503  	return server, nil, nil
   504  }
   505  
   506  const (
   507  	outputHeader  = "# This file is autogenerated, do not edit.\n"
   508  	outputTrailer = "---\n"
   509  )
   510  
   511  func writeEncodedObject(out io.Writer, in runtime.Object) error {
   512  	if _, err := fmt.Fprint(out, outputHeader); err != nil {
   513  		return err
   514  	}
   515  	if err := codec.Encode(in, out); err != nil {
   516  		return err
   517  	}
   518  	if _, err := fmt.Fprint(out, outputTrailer); err != nil {
   519  		return err
   520  	}
   521  	return nil
   522  }
   523  
   524  type writer interface {
   525  	io.Writer
   526  	String() string
   527  }
   528  
   529  func makeOutputWriter() writer {
   530  	return &bytes.Buffer{}
   531  }
   532  
   533  var makeOutputWriterTestHook = makeOutputWriter
   534  
   535  // RemoteSecretAuthType is a strongly typed authentication type suitable for use with pflags.Var().
   536  type (
   537  	RemoteSecretAuthType string
   538  	SecretType           string
   539  )
   540  
   541  var _ pflag.Value = (*RemoteSecretAuthType)(nil)
   542  
   543  func (at *RemoteSecretAuthType) String() string { return string(*at) }
   544  func (at *RemoteSecretAuthType) Type() string   { return "RemoteSecretAuthType" }
   545  func (at *RemoteSecretAuthType) Set(in string) error {
   546  	*at = RemoteSecretAuthType(in)
   547  	return nil
   548  }
   549  
   550  func (at *SecretType) String() string { return string(*at) }
   551  func (at *SecretType) Type() string   { return "SecretType" }
   552  func (at *SecretType) Set(in string) error {
   553  	*at = SecretType(in)
   554  	return nil
   555  }
   556  
   557  const (
   558  	// Use a bearer token for authentication to the remote kubernetes cluster.
   559  	RemoteSecretAuthTypeBearerToken RemoteSecretAuthType = "bearer-token"
   560  
   561  	// Use a custom authentication plugin for the remote kubernetes cluster.
   562  	RemoteSecretAuthTypePlugin RemoteSecretAuthType = "plugin"
   563  
   564  	// Secret generated from remote cluster
   565  	SecretTypeRemote SecretType = "remote"
   566  
   567  	// Secret generated from config cluster
   568  	SecretTypeConfig SecretType = "config"
   569  )
   570  
   571  // RemoteSecretOptions contains the options for creating a remote secret.
   572  type RemoteSecretOptions struct {
   573  	KubeOptions
   574  
   575  	// Name of the local cluster whose credentials are stored in the secret. Must be
   576  	// DNS1123 label as it will be used for the k8s secret name.
   577  	ClusterName string
   578  
   579  	// Create a secret with this service account's credentials.
   580  	ServiceAccountName string
   581  
   582  	// CreateServiceAccount if true, the service account specified by ServiceAccountName
   583  	// will be created if it doesn't exist.
   584  	CreateServiceAccount bool
   585  
   586  	// Authentication method for the remote Kubernetes cluster.
   587  	AuthType RemoteSecretAuthType
   588  	// Authenticator plugin configuration
   589  	AuthPluginName   string
   590  	AuthPluginConfig map[string]string
   591  
   592  	// Type of the generated secret
   593  	Type SecretType
   594  
   595  	// ManifestsPath is a path to a manifestsPath and profiles directory in the local filesystem,
   596  	// or URL with a release tgz. This is only used when no reader service account exists and has
   597  	// to be created.
   598  	ManifestsPath string
   599  
   600  	// ServerOverride overrides the server IP/hostname field from the Kubeconfig
   601  	ServerOverride string
   602  
   603  	// SecretName selects a specific secret from the remote service account, if there are multiple
   604  	SecretName string
   605  }
   606  
   607  func (o *RemoteSecretOptions) addFlags(flagset *pflag.FlagSet) {
   608  	flagset.StringVar(&o.ServiceAccountName, "service-account", "",
   609  		"Create a secret with this service account's credentials. Default value is \""+
   610  			constants.DefaultServiceAccountName+"\" if --type is \"remote\", \""+
   611  			constants.DefaultConfigServiceAccountName+"\" if --type is \"config\".")
   612  	flagset.BoolVar(&o.CreateServiceAccount, "create-service-account", true,
   613  		"If true, the service account needed for creating the remote secret will be created "+
   614  			"if it doesn't exist.")
   615  	flagset.StringVar(&o.ClusterName, "name", "",
   616  		"Name of the local cluster whose credentials are stored "+
   617  			"in the secret. If a name is not specified the kube-system namespace's UUID of "+
   618  			"the local cluster will be used.")
   619  	flagset.StringVar(&o.ServerOverride, "server", "",
   620  		"The address and port of the Kubernetes API server.")
   621  	flagset.StringVar(&o.SecretName, "secret-name", "",
   622  		"The name of the specific secret to use from the service-account. Needed when there are multiple secrets in the service account.")
   623  	var supportedAuthType []string
   624  	for _, at := range []RemoteSecretAuthType{RemoteSecretAuthTypeBearerToken, RemoteSecretAuthTypePlugin} {
   625  		supportedAuthType = append(supportedAuthType, string(at))
   626  	}
   627  	var supportedSecretType []string
   628  	for _, at := range []SecretType{SecretTypeRemote, SecretTypeConfig} {
   629  		supportedSecretType = append(supportedSecretType, string(at))
   630  	}
   631  
   632  	flagset.Var(&o.AuthType, "auth-type",
   633  		fmt.Sprintf("Type of authentication to use. supported values = %v", supportedAuthType))
   634  	flagset.StringVar(&o.AuthPluginName, "auth-plugin-name", o.AuthPluginName,
   635  		fmt.Sprintf("Authenticator plug-in name. --auth-type=%v must be set with this option",
   636  			RemoteSecretAuthTypePlugin))
   637  	flagset.StringToString("auth-plugin-config", o.AuthPluginConfig,
   638  		fmt.Sprintf("Authenticator plug-in configuration. --auth-type=%v must be set with this option",
   639  			RemoteSecretAuthTypePlugin))
   640  	flagset.Var(&o.Type, "type",
   641  		fmt.Sprintf("Type of the generated secret. supported values = %v", supportedSecretType))
   642  	flagset.StringVarP(&o.ManifestsPath, "manifests", "d", "", util.ManifestsFlagHelpStr)
   643  }
   644  
   645  func (o *RemoteSecretOptions) prepare(ctx cli.Context) error {
   646  	o.KubeOptions.prepare(ctx)
   647  
   648  	if o.ClusterName != "" {
   649  		if !labels.IsDNS1123Label(o.ClusterName) {
   650  			return fmt.Errorf("%v is not a valid DNS 1123 label", o.ClusterName)
   651  		}
   652  	}
   653  	return nil
   654  }
   655  
   656  type Warning error
   657  
   658  func createRemoteSecret(opt RemoteSecretOptions, client kube.CLIClient) (*v1.Secret, Warning, error) {
   659  	// generate the clusterName if not specified
   660  	if opt.ClusterName == "" {
   661  		uid, err := clusterUID(client.Kube())
   662  		if err != nil {
   663  			return nil, nil, err
   664  		}
   665  		opt.ClusterName = string(uid)
   666  	}
   667  
   668  	var secretName string
   669  	switch opt.Type {
   670  	case SecretTypeRemote:
   671  		secretName = remoteSecretNameFromClusterName(opt.ClusterName)
   672  		if opt.ServiceAccountName == "" {
   673  			opt.ServiceAccountName = constants.DefaultServiceAccountName
   674  		}
   675  	case SecretTypeConfig:
   676  		secretName = configSecretName
   677  		if opt.ServiceAccountName == "" {
   678  			opt.ServiceAccountName = constants.DefaultConfigServiceAccountName
   679  		}
   680  	default:
   681  		return nil, nil, fmt.Errorf("unsupported type: %v", opt.Type)
   682  	}
   683  	tokenSecret, err := getServiceAccountSecret(client, opt)
   684  	if err != nil {
   685  		return nil, nil, fmt.Errorf("could not get access token to read resources from local kube-apiserver: %v", err)
   686  	}
   687  
   688  	var server string
   689  	var warn Warning
   690  	if opt.ServerOverride != "" {
   691  		server = opt.ServerOverride
   692  	} else {
   693  		server, warn, err = getServerFromKubeconfig(client)
   694  		if err != nil {
   695  			return nil, warn, err
   696  		}
   697  	}
   698  
   699  	var remoteSecret *v1.Secret
   700  	switch opt.AuthType {
   701  	case RemoteSecretAuthTypeBearerToken:
   702  		remoteSecret, err = createRemoteSecretFromTokenAndServer(client, tokenSecret, opt.ClusterName, server, secretName)
   703  	case RemoteSecretAuthTypePlugin:
   704  		authProviderConfig := &api.AuthProviderConfig{
   705  			Name:   opt.AuthPluginName,
   706  			Config: opt.AuthPluginConfig,
   707  		}
   708  		remoteSecret, err = createRemoteSecretFromPlugin(tokenSecret, server, opt.ClusterName, secretName,
   709  			authProviderConfig)
   710  	default:
   711  		err = fmt.Errorf("unsupported authentication type: %v", opt.AuthType)
   712  	}
   713  	if err != nil {
   714  		return nil, warn, err
   715  	}
   716  
   717  	remoteSecret.Namespace = opt.Namespace
   718  	return remoteSecret, warn, nil
   719  }
   720  
   721  // CreateRemoteSecret creates a remote secret with credentials of the specified service account.
   722  // This is useful for providing a cluster access to a remote apiserver.
   723  func CreateRemoteSecret(client kube.CLIClient, opt RemoteSecretOptions) (string, Warning, error) {
   724  	remoteSecret, warn, err := createRemoteSecret(opt, client)
   725  	if err != nil {
   726  		return "", warn, err
   727  	}
   728  
   729  	// convert any binary data to the string equivalent for easier review. The
   730  	// kube-apiserver will convert this to binary before it persists it to storage.
   731  	remoteSecret.StringData = make(map[string]string, len(remoteSecret.Data))
   732  	for k, v := range remoteSecret.Data {
   733  		remoteSecret.StringData[k] = string(v)
   734  	}
   735  	remoteSecret.Data = nil
   736  
   737  	w := makeOutputWriterTestHook()
   738  	if err := writeEncodedObject(w, remoteSecret); err != nil {
   739  		return "", warn, err
   740  	}
   741  	return w.String(), warn, nil
   742  }