istioctl/pkg/multicluster/remote_secret.go (633 lines of code) (raw):

// Copyright Istio Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package multicluster import ( "bytes" "context" "fmt" "io" "os" "strings" "time" ) import ( "github.com/cenkalti/backoff/v4" "github.com/spf13/cobra" "github.com/spf13/pflag" "istio.io/pkg/log" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/serializer/json" "k8s.io/apimachinery/pkg/runtime/serializer/versioning" utilruntime "k8s.io/apimachinery/pkg/util/runtime" _ "k8s.io/client-go/plugin/pkg/client/auth" "k8s.io/client-go/tools/clientcmd" "k8s.io/client-go/tools/clientcmd/api" "k8s.io/client-go/tools/clientcmd/api/latest" ) import ( "github.com/apache/dubbo-go-pixiu/operator/cmd/mesh" "github.com/apache/dubbo-go-pixiu/operator/pkg/helm" "github.com/apache/dubbo-go-pixiu/pkg/config/constants" "github.com/apache/dubbo-go-pixiu/pkg/config/labels" "github.com/apache/dubbo-go-pixiu/pkg/kube" "github.com/apache/dubbo-go-pixiu/pkg/kube/multicluster" ) var ( codec runtime.Codec scheme *runtime.Scheme tokenWaitBackoff = time.Second ) func init() { scheme = runtime.NewScheme() utilruntime.Must(v1.AddToScheme(scheme)) opt := json.SerializerOptions{ Yaml: true, Pretty: false, Strict: false, } yamlSerializer := json.NewSerializerWithOptions(json.DefaultMetaFactory, scheme, scheme, opt) codec = versioning.NewDefaultingCodecForScheme( scheme, yamlSerializer, yamlSerializer, v1.SchemeGroupVersion, runtime.InternalGroupVersioner, ) } const ( remoteSecretPrefix = "istio-remote-secret-" configSecretName = "istio-kubeconfig" configSecretKey = "config" ) func remoteSecretNameFromClusterName(clusterName string) string { return remoteSecretPrefix + clusterName } // NewCreateRemoteSecretCommand creates a new command for joining two contexts // together in a multi-cluster mesh. func NewCreateRemoteSecretCommand() *cobra.Command { opts := RemoteSecretOptions{ AuthType: RemoteSecretAuthTypeBearerToken, AuthPluginConfig: make(map[string]string), Type: SecretTypeRemote, } c := &cobra.Command{ Use: "create-remote-secret", Short: "Create a secret with credentials to allow Istio to access remote Kubernetes apiservers", Example: ` # Create a secret to access cluster c0's apiserver and install it in cluster c1. istioctl --kubeconfig=c0.yaml x create-remote-secret --name c0 \ | kubectl --kubeconfig=c1.yaml apply -f - # Delete a secret that was previously installed in c1 istioctl --kubeconfig=c0.yaml x create-remote-secret --name c0 \ | kubectl --kubeconfig=c1.yaml delete -f - # Create a secret access a remote cluster with an auth plugin istioctl --kubeconfig=c0.yaml x create-remote-secret --name c0 --auth-type=plugin --auth-plugin-name=gcp \ | kubectl --kubeconfig=c1.yaml apply -f -`, Args: cobra.NoArgs, RunE: func(c *cobra.Command, args []string) error { if err := opts.prepare(c.Flags()); err != nil { return err } env, err := NewEnvironmentFromCobra(opts.Kubeconfig, opts.Context, c) if err != nil { return err } out, warn, err := CreateRemoteSecret(opts, env) if err != nil { _, _ = fmt.Fprintf(c.OutOrStderr(), "error: %v\n", err) return err } if warn != nil { _, _ = fmt.Fprintf(c.OutOrStderr(), "warn: %v\n", warn) } _, _ = fmt.Fprint(c.OutOrStdout(), out) return nil }, } opts.addFlags(c.PersistentFlags()) return c } func createRemoteServiceAccountSecret(kubeconfig *api.Config, clusterName, secName string) (*v1.Secret, error) { // nolint:interfacer var data bytes.Buffer if err := latest.Codec.Encode(kubeconfig, &data); err != nil { return nil, err } key := clusterName if secName == configSecretName { key = configSecretKey } out := &v1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: secName, Annotations: map[string]string{ clusterNameAnnotationKey: clusterName, }, Labels: map[string]string{ multicluster.MultiClusterSecretLabel: "true", }, }, Data: map[string][]byte{ key: data.Bytes(), }, } return out, nil } func createBaseKubeconfig(caData []byte, clusterName, server string) *api.Config { return &api.Config{ Clusters: map[string]*api.Cluster{ clusterName: { CertificateAuthorityData: caData, Server: server, }, }, AuthInfos: map[string]*api.AuthInfo{}, Contexts: map[string]*api.Context{ clusterName: { Cluster: clusterName, AuthInfo: clusterName, }, }, CurrentContext: clusterName, } } func createBearerTokenKubeconfig(caData, token []byte, clusterName, server string) *api.Config { c := createBaseKubeconfig(caData, clusterName, server) c.AuthInfos[c.CurrentContext] = &api.AuthInfo{ Token: string(token), } return c } func createPluginKubeconfig(caData []byte, clusterName, server string, authProviderConfig *api.AuthProviderConfig) *api.Config { c := createBaseKubeconfig(caData, clusterName, server) c.AuthInfos[c.CurrentContext] = &api.AuthInfo{ AuthProvider: authProviderConfig, } return c } func createRemoteSecretFromPlugin( tokenSecret *v1.Secret, server, clusterName, secName string, authProviderConfig *api.AuthProviderConfig, ) (*v1.Secret, error) { caData, ok := tokenSecret.Data[v1.ServiceAccountRootCAKey] if !ok { return nil, errMissingRootCAKey } // Create a Kubeconfig to access the remote cluster using the auth provider plugin. kubeconfig := createPluginKubeconfig(caData, clusterName, server, authProviderConfig) if err := clientcmd.Validate(*kubeconfig); err != nil { return nil, fmt.Errorf("invalid kubeconfig: %v", err) } // Encode the Kubeconfig in a secret that can be loaded by Istio to dynamically discover and access the remote cluster. return createRemoteServiceAccountSecret(kubeconfig, clusterName, secName) } var ( errMissingRootCAKey = fmt.Errorf("no %q data found", v1.ServiceAccountRootCAKey) errMissingTokenKey = fmt.Errorf("no %q data found", v1.ServiceAccountTokenKey) ) func createRemoteSecretFromTokenAndServer(client kube.ExtendedClient, tokenSecret *v1.Secret, clusterName, server, secName string) (*v1.Secret, error) { caData, token, err := waitForTokenData(client, tokenSecret) if err != nil { return nil, err } // Create a Kubeconfig to access the remote cluster using the remote service account credentials. kubeconfig := createBearerTokenKubeconfig(caData, token, clusterName, server) if err := clientcmd.Validate(*kubeconfig); err != nil { return nil, fmt.Errorf("invalid kubeconfig: %v", err) } // Encode the Kubeconfig in a secret that can be loaded by Istio to dynamically discover and access the remote cluster. return createRemoteServiceAccountSecret(kubeconfig, clusterName, secName) } func waitForTokenData(client kube.ExtendedClient, secret *v1.Secret) (ca, token []byte, err error) { ca, token, err = tokenDataFromSecret(secret) if err == nil { return } log.Infof("Waiting for data to be populated in %s", secret.Name) err = backoff.Retry( func() error { secret, err = client.Kube().CoreV1().Secrets(secret.Namespace).Get(context.TODO(), secret.Name, metav1.GetOptions{}) if err != nil { return err } ca, token, err = tokenDataFromSecret(secret) return err }, backoff.WithMaxRetries(backoff.NewConstantBackOff(tokenWaitBackoff), 5)) return } func tokenDataFromSecret(tokenSecret *v1.Secret) (ca, token []byte, err error) { var ok bool ca, ok = tokenSecret.Data[v1.ServiceAccountRootCAKey] if !ok { err = errMissingRootCAKey return } token, ok = tokenSecret.Data[v1.ServiceAccountTokenKey] if !ok { err = errMissingTokenKey return } return } func getServiceAccountSecret(client kube.ExtendedClient, opt RemoteSecretOptions) (*v1.Secret, error) { // Create the service account if it doesn't exist. serviceAccount, err := getOrCreateServiceAccount(client, opt) if err != nil { return nil, err } if !kube.IsAtLeastVersion(client, 24) { return legacyGetServiceAccountSecret(serviceAccount, client, opt) } return getOrCreateServiceAccountSecret(serviceAccount, client, opt) } // In Kubernetes 1.24+ we can't assume the secrets will be referenced in the ServiceAccount or be created automatically. // See https://github.com/istio/istio/issues/38246 func getOrCreateServiceAccountSecret( serviceAccount *v1.ServiceAccount, client kube.ExtendedClient, opt RemoteSecretOptions, ) (*v1.Secret, error) { ctx := context.TODO() // manually specified secret, make sure it references the ServiceAccount if opt.SecretName != "" { secret, err := client.Kube().CoreV1().Secrets(opt.Namespace).Get(ctx, opt.SecretName, metav1.GetOptions{}) if err != nil { return nil, fmt.Errorf("could not get specified secret %s/%s: %v", opt.Namespace, opt.SecretName, err) } if err := secretReferencesServiceAccount(serviceAccount, secret); err != nil { return nil, err } return secret, nil } // first try to find an existing secret that references the SA // TODO will the SA have any reference to secrets anymore, can we avoid this list? allSecrets, err := client.Kube().CoreV1().Secrets(opt.Namespace).List(ctx, metav1.ListOptions{}) if err != nil { return nil, fmt.Errorf("failed listing secrets in %s: %v", opt.Namespace, err) } for _, item := range allSecrets.Items { secret := item if secretReferencesServiceAccount(serviceAccount, &secret) == nil { return &secret, nil } } // finally, create the sa token secret manually // https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/#manually-create-a-service-account-api-token // TODO ephemeral time-based tokens are preferred; we should re-think this log.Infof("Creating token secret for service account %q", serviceAccount.Name) secretName := tokenSecretName(serviceAccount.Name) return client.Kube().CoreV1().Secrets(opt.Namespace).Create(ctx, &v1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: secretName, Annotations: map[string]string{v1.ServiceAccountNameKey: serviceAccount.Name}, }, Type: v1.SecretTypeServiceAccountToken, }, metav1.CreateOptions{}) } func tokenSecretName(saName string) string { return saName + "-istio-remote-secret-token" } func secretReferencesServiceAccount(serviceAccount *v1.ServiceAccount, secret *v1.Secret) error { if secret.Type != v1.SecretTypeServiceAccountToken || secret.Annotations[v1.ServiceAccountNameKey] != serviceAccount.Name { return fmt.Errorf("secret %s/%s does not reference ServiceAccount %s", secret.Namespace, secret.Name, serviceAccount.Name) } return nil } func legacyGetServiceAccountSecret( serviceAccount *v1.ServiceAccount, client kube.ExtendedClient, opt RemoteSecretOptions, ) (*v1.Secret, error) { if len(serviceAccount.Secrets) == 0 { return nil, fmt.Errorf("no secret found in the service account: %s", serviceAccount) } secretName := "" secretNamespace := "" if opt.SecretName != "" { found := false for _, secret := range serviceAccount.Secrets { if secret.Name == opt.SecretName { found = true secretName = secret.Name secretNamespace = secret.Namespace break } } if !found { return nil, fmt.Errorf("provided secret does not exist: %s", opt.SecretName) } } else { if len(serviceAccount.Secrets) == 1 { secretName = serviceAccount.Secrets[0].Name secretNamespace = serviceAccount.Secrets[0].Namespace } else { return nil, fmt.Errorf("wrong number of secrets (%v) in serviceaccount %s/%s, please use --secret-name to specify one", len(serviceAccount.Secrets), opt.Namespace, opt.ServiceAccountName) } } if secretNamespace == "" { secretNamespace = opt.Namespace } return client.Kube().CoreV1().Secrets(secretNamespace).Get(context.TODO(), secretName, metav1.GetOptions{}) } func getOrCreateServiceAccount(client kube.ExtendedClient, opt RemoteSecretOptions) (*v1.ServiceAccount, error) { if sa, err := client.Kube().CoreV1().ServiceAccounts(opt.Namespace).Get( context.TODO(), opt.ServiceAccountName, metav1.GetOptions{}); err == nil { return sa, nil } else if !opt.CreateServiceAccount { // User chose not to automatically create the service account. return nil, fmt.Errorf("failed retrieving service account %s.%s required for creating "+ "the remote secret (hint: try installing a minimal Istio profile on the cluster first, "+ "or run with '--create-service-account=true'): %v", opt.ServiceAccountName, opt.Namespace, err) } if err := createServiceAccount(client, opt); err != nil { return nil, err } // Return the newly created service account. sa, err := client.Kube().CoreV1().ServiceAccounts(opt.Namespace).Get( context.TODO(), opt.ServiceAccountName, metav1.GetOptions{}) if err != nil { return nil, fmt.Errorf("failed retrieving service account %s.%s after creating it: %v", opt.ServiceAccountName, opt.Namespace, err) } return sa, nil } func createServiceAccount(client kube.ExtendedClient, opt RemoteSecretOptions) error { yaml, err := generateServiceAccountYAML(opt) if err != nil { return err } // Before we can apply the yaml, we have to ensure the system namespace exists. if err := createNamespaceIfNotExist(client, opt.Namespace); err != nil { return err } // Apply the YAML to the cluster. return applyYAML(client, yaml, opt.Namespace) } func generateServiceAccountYAML(opt RemoteSecretOptions) (string, error) { // Create a renderer for the base installation. baseRenderer := helm.NewHelmRenderer(opt.ManifestsPath, "base", "Base", opt.Namespace, nil) discoveryRenderer := helm.NewHelmRenderer(opt.ManifestsPath, "istio-control/istio-discovery", "Pilot", opt.Namespace, nil) baseTemplates := []string{"reader-serviceaccount.yaml"} discoveryTemplates := []string{"clusterrole.yaml", "clusterrolebinding.yaml"} if err := baseRenderer.Run(); err != nil { return "", fmt.Errorf("failed running base Helm renderer: %w", err) } if err := discoveryRenderer.Run(); err != nil { return "", fmt.Errorf("failed running base discovery Helm renderer: %w", err) } values := fmt.Sprintf(` global: istioNamespace: %s `, opt.Namespace) // Render the templates required for the service account and role bindings. baseContent, err := baseRenderer.RenderManifestFiltered(values, func(template string) bool { for _, t := range baseTemplates { if strings.Contains(template, t) { return true } } return false }) if err != nil { return "", fmt.Errorf("failed rendering base manifest: %w", err) } discoveryContent, err := discoveryRenderer.RenderManifestFiltered(values, func(template string) bool { for _, t := range discoveryTemplates { if strings.Contains(template, t) { return true } } return false }) if err != nil { return "", fmt.Errorf("failed rendering discovery manifest: %w", err) } aggregateContent := fmt.Sprintf(` %s --- %s `, baseContent, discoveryContent) return aggregateContent, nil } func applyYAML(client kube.ExtendedClient, yamlContent, ns string) error { yamlFile, err := writeToTempFile(yamlContent) if err != nil { return fmt.Errorf("failed creating manifest file: %v", err) } // Apply the YAML to the cluster. if err := client.ApplyYAMLFiles(ns, yamlFile); err != nil { return fmt.Errorf("failed applying manifest %s: %v", yamlFile, err) } return nil } func createNamespaceIfNotExist(client kube.Client, ns string) error { if _, err := client.Kube().CoreV1().Namespaces().Get(context.TODO(), ns, metav1.GetOptions{}); err != nil { if _, err := client.Kube().CoreV1().Namespaces().Create(context.TODO(), &v1.Namespace{ ObjectMeta: metav1.ObjectMeta{ Name: ns, }, }, metav1.CreateOptions{}); err != nil { return fmt.Errorf("failed creating namespace %s: %v", ns, err) } } return nil } func writeToTempFile(content string) (string, error) { outFile, err := os.CreateTemp("", "remote-secret-manifest-*") if err != nil { return "", fmt.Errorf("failed creating temp file for manifest: %v", err) } defer func() { _ = outFile.Close() }() if _, err := outFile.Write([]byte(content)); err != nil { return "", fmt.Errorf("failed writing manifest file: %v", err) } return outFile.Name(), nil } func getServerFromKubeconfig(context string, config *api.Config) (string, Warning, error) { if context == "" { context = config.CurrentContext } configContext, ok := config.Contexts[context] if !ok { return "", nil, fmt.Errorf("could not find cluster for context %q", context) } cluster, ok := config.Clusters[configContext.Cluster] if !ok { return "", nil, fmt.Errorf("could not find server for context %q", context) } if strings.Contains(cluster.Server, "127.0.0.1") || strings.Contains(cluster.Server, "localhost") { return cluster.Server, fmt.Errorf( "server in Kubeconfig is %s. This is likely not reachable from inside the cluster, "+ "if you're using Kubernetes in Docker, pass --server with the container IP for the API Server", cluster.Server), nil } return cluster.Server, nil, nil } const ( outputHeader = "# This file is autogenerated, do not edit.\n" outputTrailer = "---\n" ) func writeEncodedObject(out io.Writer, in runtime.Object) error { if _, err := fmt.Fprint(out, outputHeader); err != nil { return err } if err := codec.Encode(in, out); err != nil { return err } if _, err := fmt.Fprint(out, outputTrailer); err != nil { return err } return nil } type writer interface { io.Writer String() string } func makeOutputWriter() writer { return &bytes.Buffer{} } var makeOutputWriterTestHook = makeOutputWriter // RemoteSecretAuthType is a strongly typed authentication type suitable for use with pflags.Var(). type ( RemoteSecretAuthType string SecretType string ) var _ pflag.Value = (*RemoteSecretAuthType)(nil) func (at *RemoteSecretAuthType) String() string { return string(*at) } func (at *RemoteSecretAuthType) Type() string { return "RemoteSecretAuthType" } func (at *RemoteSecretAuthType) Set(in string) error { *at = RemoteSecretAuthType(in) return nil } func (at *SecretType) String() string { return string(*at) } func (at *SecretType) Type() string { return "SecretType" } func (at *SecretType) Set(in string) error { *at = SecretType(in) return nil } const ( // Use a bearer token for authentication to the remote kubernetes cluster. RemoteSecretAuthTypeBearerToken RemoteSecretAuthType = "bearer-token" // Use a custom authentication plugin for the remote kubernetes cluster. RemoteSecretAuthTypePlugin RemoteSecretAuthType = "plugin" // Secret generated from remote cluster SecretTypeRemote SecretType = "remote" // Secret generated from config cluster SecretTypeConfig SecretType = "config" ) // RemoteSecretOptions contains the options for creating a remote secret. type RemoteSecretOptions struct { KubeOptions // Name of the local cluster whose credentials are stored in the secret. Must be // DNS1123 label as it will be used for the k8s secret name. ClusterName string // Create a secret with this service account's credentials. ServiceAccountName string // CreateServiceAccount if true, the service account specified by ServiceAccountName // will be created if it doesn't exist. CreateServiceAccount bool // Authentication method for the remote Kubernetes cluster. AuthType RemoteSecretAuthType // Authenticator plugin configuration AuthPluginName string AuthPluginConfig map[string]string // Type of the generated secret Type SecretType // ManifestsPath is a path to a manifestsPath and profiles directory in the local filesystem, // or URL with a release tgz. This is only used when no reader service account exists and has // to be created. ManifestsPath string // ServerOverride overrides the server IP/hostname field from the Kubeconfig ServerOverride string // SecretName selects a specific secret from the remote service account, if there are multiple SecretName string } func (o *RemoteSecretOptions) addFlags(flagset *pflag.FlagSet) { flagset.StringVar(&o.ServiceAccountName, "service-account", "", "Create a secret with this service account's credentials. Default value is \""+ constants.DefaultServiceAccountName+"\" if --type is \"remote\", \""+ constants.DefaultConfigServiceAccountName+"\" if --type is \"config\".") flagset.BoolVar(&o.CreateServiceAccount, "create-service-account", true, "If true, the service account needed for creating the remote secret will be created "+ "if it doesn't exist.") flagset.StringVar(&o.ClusterName, "name", "", "Name of the local cluster whose credentials are stored "+ "in the secret. If a name is not specified the kube-system namespace's UUID of "+ "the local cluster will be used.") flagset.StringVar(&o.ServerOverride, "server", "", "The address and port of the Kubernetes API server.") flagset.StringVar(&o.SecretName, "secret-name", "", "The name of the specific secret to use from the service-account. Needed when there are multiple secrets in the service account.") var supportedAuthType []string for _, at := range []RemoteSecretAuthType{RemoteSecretAuthTypeBearerToken, RemoteSecretAuthTypePlugin} { supportedAuthType = append(supportedAuthType, string(at)) } var supportedSecretType []string for _, at := range []SecretType{SecretTypeRemote, SecretTypeConfig} { supportedSecretType = append(supportedSecretType, string(at)) } flagset.Var(&o.AuthType, "auth-type", fmt.Sprintf("Type of authentication to use. supported values = %v", supportedAuthType)) flagset.StringVar(&o.AuthPluginName, "auth-plugin-name", o.AuthPluginName, fmt.Sprintf("Authenticator plug-in name. --auth-type=%v must be set with this option", RemoteSecretAuthTypePlugin)) flagset.StringToString("auth-plugin-config", o.AuthPluginConfig, fmt.Sprintf("Authenticator plug-in configuration. --auth-type=%v must be set with this option", RemoteSecretAuthTypePlugin)) flagset.Var(&o.Type, "type", fmt.Sprintf("Type of the generated secret. supported values = %v", supportedSecretType)) flagset.StringVarP(&o.ManifestsPath, "manifests", "d", "", mesh.ManifestsFlagHelpStr) } func (o *RemoteSecretOptions) prepare(flags *pflag.FlagSet) error { o.KubeOptions.prepare(flags) if o.ClusterName != "" { if !labels.IsDNS1123Label(o.ClusterName) { return fmt.Errorf("%v is not a valid DNS 1123 label", o.ClusterName) } } return nil } type Warning error func createRemoteSecret(opt RemoteSecretOptions, client kube.ExtendedClient, env Environment) (*v1.Secret, Warning, error) { // generate the clusterName if not specified if opt.ClusterName == "" { uid, err := clusterUID(client) if err != nil { return nil, nil, err } opt.ClusterName = string(uid) } var secretName string switch opt.Type { case SecretTypeRemote: secretName = remoteSecretNameFromClusterName(opt.ClusterName) if opt.ServiceAccountName == "" { opt.ServiceAccountName = constants.DefaultServiceAccountName } case SecretTypeConfig: secretName = configSecretName if opt.ServiceAccountName == "" { opt.ServiceAccountName = constants.DefaultConfigServiceAccountName } default: return nil, nil, fmt.Errorf("unsupported type: %v", opt.Type) } tokenSecret, err := getServiceAccountSecret(client, opt) if err != nil { return nil, nil, fmt.Errorf("could not get access token to read resources from local kube-apiserver: %v", err) } var server string var warn Warning if opt.ServerOverride != "" { server = opt.ServerOverride } else { server, warn, err = getServerFromKubeconfig(opt.Context, env.GetConfig()) if err != nil { return nil, warn, err } } var remoteSecret *v1.Secret switch opt.AuthType { case RemoteSecretAuthTypeBearerToken: remoteSecret, err = createRemoteSecretFromTokenAndServer(client, tokenSecret, opt.ClusterName, server, secretName) case RemoteSecretAuthTypePlugin: authProviderConfig := &api.AuthProviderConfig{ Name: opt.AuthPluginName, Config: opt.AuthPluginConfig, } remoteSecret, err = createRemoteSecretFromPlugin(tokenSecret, server, opt.ClusterName, secretName, authProviderConfig) default: err = fmt.Errorf("unsupported authentication type: %v", opt.AuthType) } if err != nil { return nil, warn, err } remoteSecret.Namespace = opt.Namespace return remoteSecret, warn, nil } // CreateRemoteSecret creates a remote secret with credentials of the specified service account. // This is useful for providing a cluster access to a remote apiserver. func CreateRemoteSecret(opt RemoteSecretOptions, env Environment) (string, Warning, error) { client, err := env.CreateClient(opt.Context) if err != nil { return "", nil, err } remoteSecret, warn, err := createRemoteSecret(opt, client, env) if err != nil { return "", warn, err } // convert any binary data to the string equivalent for easier review. The // kube-apiserver will convert this to binary before it persists it to storage. remoteSecret.StringData = make(map[string]string, len(remoteSecret.Data)) for k, v := range remoteSecret.Data { remoteSecret.StringData[k] = string(v) } remoteSecret.Data = nil w := makeOutputWriterTestHook() if err := writeEncodedObject(w, remoteSecret); err != nil { return "", warn, err } return w.String(), warn, nil }