1// Copyright 2019 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
15package multicluster
16
17import (
18	"bytes"
19	context2 "context"
20	"fmt"
21	"io"
22	"os"
23	"strings"
24
25	"github.com/spf13/cobra"
26	"github.com/spf13/pflag"
27	v1 "k8s.io/api/core/v1"
28	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
29	"k8s.io/apimachinery/pkg/runtime"
30	"k8s.io/apimachinery/pkg/runtime/serializer/json"
31	"k8s.io/apimachinery/pkg/runtime/serializer/versioning"
32	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
33	"k8s.io/client-go/kubernetes"
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/api"
36	"k8s.io/client-go/tools/clientcmd/api/latest"
37
38	"istio.io/istio/pkg/config/labels"
39	"istio.io/istio/pkg/kube/secretcontroller"
40)
41
42var (
43	codec  runtime.Codec
44	scheme *runtime.Scheme
45)
46
47func init() {
48	scheme = runtime.NewScheme()
49	utilruntime.Must(v1.AddToScheme(scheme))
50	opt := json.SerializerOptions{true, false, false}
51	yamlSerializer := json.NewSerializerWithOptions(json.DefaultMetaFactory, scheme, scheme, opt)
52	codec = versioning.NewDefaultingCodecForScheme(
53		scheme,
54		yamlSerializer,
55		yamlSerializer,
56		v1.SchemeGroupVersion,
57		runtime.InternalGroupVersioner,
58	)
59}
60
61const (
62	// default service account to use for remote cluster access.
63	DefaultServiceAccountName = "istio-reader-service-account"
64
65	remoteSecretPrefix = "istio-remote-secret-"
66)
67
68func remoteSecretNameFromClusterName(clusterName string) string {
69	return remoteSecretPrefix + clusterName
70}
71
72func clusterNameFromRemoteSecretName(name string) string {
73	return strings.TrimPrefix(name, remoteSecretPrefix)
74}
75
76// NewCreateRemoteSecretCommand creates a new command for joining two contexts
77// together in a multi-cluster mesh.
78func NewCreateRemoteSecretCommand() *cobra.Command {
79	opts := RemoteSecretOptions{
80		ServiceAccountName: DefaultServiceAccountName,
81		AuthType:           RemoteSecretAuthTypeBearerToken,
82		AuthPluginConfig:   make(map[string]string),
83	}
84	c := &cobra.Command{
85		Use:   "create-remote-secret",
86		Short: "Create a secret with credentials to allow Istio to access remote Kubernetes apiservers",
87		Example: `
88# Create a secret to access cluster c0's apiserver and install it in cluster c1.
89istioctl --Kubeconfig=c0.yaml x create-remote-secret --name c0 \
90    | kubectl --Kubeconfig=c1.yaml apply -f -
91
92# Delete a secret that was previously installed in c1
93istioctl --Kubeconfig=c0.yaml x create-remote-secret --name c0 \
94    | kubectl --Kubeconfig=c1.yaml delete -f -
95
96# Create a secret access a remote cluster with an auth plugin
97istioctl --Kubeconfig=c0.yaml x create-remote-secret --name c0 --auth-type=plugin --auth-plugin-name=gcp \
98    | kubectl --Kubeconfig=c1.yaml apply -f -
99`,
100		Args: cobra.NoArgs,
101		RunE: func(c *cobra.Command, args []string) error {
102			if err := opts.prepare(c.Flags()); err != nil {
103				return err
104			}
105			env, err := NewEnvironmentFromCobra(opts.Kubeconfig, opts.Context, c)
106			if err != nil {
107				return err
108			}
109			out, err := CreateRemoteSecret(opts, env)
110			if err != nil {
111				fmt.Fprintf(c.OutOrStderr(), "error: %v\n", err)
112				os.Exit(1)
113			}
114			fmt.Fprint(c.OutOrStdout(), out)
115			return nil
116		},
117	}
118	opts.addFlags(c.PersistentFlags())
119	return c
120}
121
122func createRemoteServiceAccountSecret(kubeconfig *api.Config, clusterName, context string) (*v1.Secret, error) { // nolint:interfacer
123	var data bytes.Buffer
124	if err := latest.Codec.Encode(kubeconfig, &data); err != nil {
125		return nil, err
126	}
127	out := &v1.Secret{
128		ObjectMeta: metav1.ObjectMeta{
129			Name: remoteSecretNameFromClusterName(clusterName),
130			Annotations: map[string]string{
131				clusterContextAnnotationKey: context,
132			},
133			Labels: map[string]string{
134				secretcontroller.MultiClusterSecretLabel: "true",
135			},
136		},
137		Data: map[string][]byte{
138			clusterName: data.Bytes(),
139		},
140	}
141	return out, nil
142}
143
144func createBaseKubeconfig(caData []byte, context, server string) *api.Config {
145	return &api.Config{
146		Clusters: map[string]*api.Cluster{
147			context: {
148				CertificateAuthorityData: caData,
149				Server:                   server,
150			},
151		},
152		AuthInfos: map[string]*api.AuthInfo{},
153		Contexts: map[string]*api.Context{
154			context: {
155				Cluster:  context,
156				AuthInfo: context,
157			},
158		},
159		CurrentContext: context,
160	}
161}
162
163func createBearerTokenKubeconfig(caData, token []byte, context, server string) *api.Config {
164	c := createBaseKubeconfig(caData, context, server)
165	c.AuthInfos[context] = &api.AuthInfo{
166		Token: string(token),
167	}
168	return c
169}
170
171func createPluginKubeconfig(caData []byte, context, server string, authProviderConfig *api.AuthProviderConfig) *api.Config {
172	c := createBaseKubeconfig(caData, context, server)
173	c.AuthInfos[context] = &api.AuthInfo{
174		AuthProvider: authProviderConfig,
175	}
176	return c
177}
178
179func createRemoteSecretFromPlugin(
180	tokenSecret *v1.Secret,
181	context, server, clusterName string,
182	authProviderConfig *api.AuthProviderConfig,
183) (*v1.Secret, error) {
184	caData, ok := tokenSecret.Data[v1.ServiceAccountRootCAKey]
185	if !ok {
186		return nil, errMissingRootCAKey
187	}
188
189	// Create a Kubeconfig to access the remote cluster using the auth provider plugin.
190	kubeconfig := createPluginKubeconfig(caData, context, server, authProviderConfig)
191
192	// Encode the Kubeconfig in a secret that can be loaded by Istio to dynamically discover and access the remote cluster.
193	return createRemoteServiceAccountSecret(kubeconfig, clusterName, context)
194}
195
196var (
197	errMissingRootCAKey = fmt.Errorf("no %q data found", v1.ServiceAccountRootCAKey)
198	errMissingTokenKey  = fmt.Errorf("no %q data found", v1.ServiceAccountTokenKey)
199)
200
201func createRemoteSecretFromTokenAndServer(tokenSecret *v1.Secret, clusterName, context, server string) (*v1.Secret, error) {
202	caData, ok := tokenSecret.Data[v1.ServiceAccountRootCAKey]
203	if !ok {
204		return nil, errMissingRootCAKey
205	}
206	token, ok := tokenSecret.Data[v1.ServiceAccountTokenKey]
207	if !ok {
208		return nil, errMissingTokenKey
209	}
210
211	// Create a Kubeconfig to access the remote cluster using the remote service account credentials.
212	kubeconfig := createBearerTokenKubeconfig(caData, token, context, server)
213
214	// Encode the Kubeconfig in a secret that can be loaded by Istio to dynamically discover and access the remote cluster.
215	return createRemoteServiceAccountSecret(kubeconfig, clusterName, context)
216}
217
218func getServiceAccountSecretToken(kube kubernetes.Interface, saName, saNamespace string) (*v1.Secret, error) {
219	serviceAccount, err := kube.CoreV1().ServiceAccounts(saNamespace).Get(context2.TODO(), saName, metav1.GetOptions{})
220	if err != nil {
221		return nil, err
222	}
223	if len(serviceAccount.Secrets) != 1 {
224		return nil, fmt.Errorf("wrong number of secrets (%v) in serviceaccount %s/%s",
225			len(serviceAccount.Secrets), saNamespace, saName)
226	}
227	secretName := serviceAccount.Secrets[0].Name
228	secretNamespace := serviceAccount.Secrets[0].Namespace
229	if secretNamespace == "" {
230		secretNamespace = saNamespace
231	}
232	return kube.CoreV1().Secrets(secretNamespace).Get(context2.TODO(), secretName, metav1.GetOptions{})
233}
234
235func getCurrentContextAndClusterServerFromKubeconfig(context string, config *api.Config) (string, string, error) {
236	if context == "" {
237		context = config.CurrentContext
238	}
239
240	configContext, ok := config.Contexts[context]
241	if !ok {
242		return "", "", fmt.Errorf("could not find cluster for context %q", context)
243	}
244	cluster, ok := config.Clusters[configContext.Cluster]
245	if !ok {
246		return "", "", fmt.Errorf("could not find server for context %q", context)
247	}
248	return context, cluster.Server, nil
249}
250
251const (
252	outputHeader  = "# This file is autogenerated, do not edit.\n"
253	outputTrailer = "---\n"
254)
255
256func writeEncodedObject(out io.Writer, in runtime.Object) error {
257	if _, err := fmt.Fprint(out, outputHeader); err != nil {
258		return err
259	}
260	if err := codec.Encode(in, out); err != nil {
261		return err
262	}
263	if _, err := fmt.Fprint(out, outputTrailer); err != nil {
264		return err
265	}
266	return nil
267}
268
269type writer interface {
270	io.Writer
271	String() string
272}
273
274func makeOutputWriter() writer {
275	return &bytes.Buffer{}
276}
277
278var makeOutputWriterTestHook = makeOutputWriter
279
280// RemoteSecretAuthType is a strongly typed authentication type suitable for use with pflags.Var().
281type RemoteSecretAuthType string
282
283var _ pflag.Value = (*RemoteSecretAuthType)(nil)
284
285func (at *RemoteSecretAuthType) String() string { return string(*at) }
286func (at *RemoteSecretAuthType) Type() string   { return "RemoteSecretAuthType" }
287func (at *RemoteSecretAuthType) Set(in string) error {
288	*at = RemoteSecretAuthType(in)
289	return nil
290}
291
292const (
293	// Use a bearer token for authentication to the remote kubernetes cluster.
294	RemoteSecretAuthTypeBearerToken RemoteSecretAuthType = "bearer-token"
295
296	// User a custom custom authentication plugin for the remote kubernetes cluster.
297	RemoteSecretAuthTypePlugin RemoteSecretAuthType = "plugin"
298)
299
300// RemoteSecretOptions contains the options for creating a remote secret.
301type RemoteSecretOptions struct {
302	KubeOptions
303
304	// Name of the local cluster whose credentials are stored in the secret. Must be
305	// DNS1123 label as it will be used for the k8s secret name.
306	ClusterName string
307
308	// Create a secret with this service account's credentials.
309	ServiceAccountName string
310
311	// Authentication method for the remote Kubernetes cluster.
312	AuthType RemoteSecretAuthType
313
314	// Authenticator plugin configuration
315	AuthPluginName   string
316	AuthPluginConfig map[string]string
317}
318
319func (o *RemoteSecretOptions) addFlags(flagset *pflag.FlagSet) {
320	flagset.StringVar(&o.ServiceAccountName, "service-account", o.ServiceAccountName,
321		"create a secret with this service account's credentials.")
322	flagset.StringVar(&o.ClusterName, "name", "",
323		"Name of the local cluster whose credentials are stored "+
324			"in the secret. If a name is not specified the kube-system namespace's UUID of "+
325			"the local cluster will be used.")
326	var supportedAuthType []string
327	for _, at := range []RemoteSecretAuthType{RemoteSecretAuthTypeBearerToken, RemoteSecretAuthTypePlugin} {
328		supportedAuthType = append(supportedAuthType, string(at))
329	}
330	flagset.Var(&o.AuthType, "auth-type",
331		fmt.Sprintf("type of authentication to use. supported values = %v", supportedAuthType))
332	flagset.StringVar(&o.AuthPluginName, "auth-plugin-name", o.AuthPluginName,
333		fmt.Sprintf("authenticator plug-in name. --auth-type=%v must be set with this option",
334			RemoteSecretAuthTypePlugin))
335	flagset.StringToString("auth-plugin-config", o.AuthPluginConfig,
336		fmt.Sprintf("authenticator plug-in configuration. --auth-type=%v must be set with this option",
337			RemoteSecretAuthTypePlugin))
338}
339
340func (o *RemoteSecretOptions) prepare(flags *pflag.FlagSet) error {
341	o.KubeOptions.prepare(flags)
342
343	if o.ClusterName != "" {
344		if !labels.IsDNS1123Label(o.ClusterName) {
345			return fmt.Errorf("%v is not a valid DNS 1123 label", o.ClusterName)
346		}
347	}
348	return nil
349}
350
351func createRemoteSecret(opt RemoteSecretOptions, client kubernetes.Interface, env Environment) (*v1.Secret, error) {
352	// generate the clusterName if not specified
353	if opt.ClusterName == "" {
354		uid, err := clusterUID(client)
355		if err != nil {
356			return nil, err
357		}
358		opt.ClusterName = string(uid)
359	}
360
361	tokenSecret, err := getServiceAccountSecretToken(client, opt.ServiceAccountName, opt.Namespace)
362	if err != nil {
363		return nil, fmt.Errorf("could not get access token to read resources from local kube-apiserver: %v", err)
364	}
365
366	currentContext, server, err := getCurrentContextAndClusterServerFromKubeconfig(opt.Context, env.GetConfig())
367	if err != nil {
368		return nil, err
369	}
370
371	var remoteSecret *v1.Secret
372	switch opt.AuthType {
373	case RemoteSecretAuthTypeBearerToken:
374		remoteSecret, err = createRemoteSecretFromTokenAndServer(tokenSecret, opt.ClusterName, currentContext, server)
375	case RemoteSecretAuthTypePlugin:
376		authProviderConfig := &api.AuthProviderConfig{
377			Name:   opt.AuthPluginName,
378			Config: opt.AuthPluginConfig,
379		}
380		remoteSecret, err = createRemoteSecretFromPlugin(tokenSecret, currentContext, server, opt.ClusterName, authProviderConfig)
381	default:
382		err = fmt.Errorf("unsupported authentication type: %v", opt.AuthType)
383	}
384	if err != nil {
385		return nil, err
386	}
387
388	remoteSecret.Namespace = opt.Namespace
389	return remoteSecret, nil
390}
391
392// CreateRemoteSecret creates a remote secret with credentials of the specified service account.
393// This is useful for providing a cluster access to a remote apiserver.
394func CreateRemoteSecret(opt RemoteSecretOptions, env Environment) (string, error) {
395	client, err := env.CreateClientSet(opt.Context)
396	if err != nil {
397		return "", err
398	}
399
400	remoteSecret, err := createRemoteSecret(opt, client, env)
401	if err != nil {
402		return "", err
403	}
404
405	// convert any binary data to the string equivalent for easier review. The
406	// kube-apiserver will convert this to binary before it persists it to storage.
407	remoteSecret.StringData = make(map[string]string, len(remoteSecret.Data))
408	for k, v := range remoteSecret.Data {
409		remoteSecret.StringData[k] = string(v)
410	}
411	remoteSecret.Data = nil
412
413	w := makeOutputWriterTestHook()
414	if err := writeEncodedObject(w, remoteSecret); err != nil {
415		return "", err
416	}
417	return w.String(), nil
418}
419