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