github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/cli/cmd/cluster/connect.go (about) 1 /* 2 Copyright (C) 2022-2023 ApeCloud Co., Ltd 3 4 This file is part of KubeBlocks project 5 6 This program is free software: you can redistribute it and/or modify 7 it under the terms of the GNU Affero General Public License as published by 8 the Free Software Foundation, either version 3 of the License, or 9 (at your option) any later version. 10 11 This program is distributed in the hope that it will be useful 12 but WITHOUT ANY WARRANTY; without even the implied warranty of 13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 GNU Affero General Public License for more details. 15 16 You should have received a copy of the GNU Affero General Public License 17 along with this program. If not, see <http://www.gnu.org/licenses/>. 18 */ 19 20 package cluster 21 22 import ( 23 "context" 24 "fmt" 25 "os" 26 "strings" 27 28 "github.com/spf13/cobra" 29 "golang.org/x/crypto/ssh/terminal" 30 corev1 "k8s.io/api/core/v1" 31 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 32 "k8s.io/cli-runtime/pkg/genericiooptions" 33 "k8s.io/klog/v2" 34 cmdutil "k8s.io/kubectl/pkg/cmd/util" 35 "k8s.io/kubectl/pkg/util/templates" 36 37 appsv1alpha1 "github.com/1aal/kubeblocks/apis/apps/v1alpha1" 38 "github.com/1aal/kubeblocks/pkg/cli/cluster" 39 "github.com/1aal/kubeblocks/pkg/cli/exec" 40 "github.com/1aal/kubeblocks/pkg/cli/types" 41 "github.com/1aal/kubeblocks/pkg/cli/util" 42 "github.com/1aal/kubeblocks/pkg/cli/util/flags" 43 "github.com/1aal/kubeblocks/pkg/constant" 44 "github.com/1aal/kubeblocks/pkg/lorry/engines" 45 "github.com/1aal/kubeblocks/pkg/lorry/engines/models" 46 "github.com/1aal/kubeblocks/pkg/lorry/engines/register" 47 ) 48 49 var connectExample = templates.Examples(` 50 # connect to a specified cluster, default connect to the leader/primary instance 51 kbcli cluster connect mycluster 52 53 # connect to cluster as user 54 kbcli cluster connect mycluster --as-user myuser 55 56 # connect to a specified instance 57 kbcli cluster connect -i mycluster-instance-0 58 59 # connect to a specified component 60 kbcli cluster connect mycluster --component mycomponent 61 62 # show cli connection example with password mask 63 kbcli cluster connect mycluster --show-example --client=cli 64 65 # show java connection example with password mask 66 kbcli cluster connect mycluster --show-example --client=java 67 68 # show all connection examples with password mask 69 kbcli cluster connect mycluster --show-example 70 71 # show cli connection examples with real password 72 kbcli cluster connect mycluster --show-example --client=cli --show-password`) 73 74 const passwordMask = "******" 75 76 type ConnectOptions struct { 77 clusterName string 78 componentName string 79 80 clientType string 81 showExample bool 82 showPassword bool 83 engine engines.ClusterCommands 84 85 privateEndPoint bool 86 svc *corev1.Service 87 88 component *appsv1alpha1.ClusterComponentSpec 89 componentDef *appsv1alpha1.ClusterComponentDefinition 90 targetCluster *appsv1alpha1.Cluster 91 targetClusterDef *appsv1alpha1.ClusterDefinition 92 93 userName string 94 userPasswd string 95 96 *exec.ExecOptions 97 } 98 99 // NewConnectCmd returns the cmd of connecting to a cluster 100 func NewConnectCmd(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command { 101 o := &ConnectOptions{ExecOptions: exec.NewExecOptions(f, streams)} 102 cmd := &cobra.Command{ 103 Use: "connect (NAME | -i INSTANCE-NAME)", 104 Short: "Connect to a cluster or instance.", 105 Example: connectExample, 106 ValidArgsFunction: util.ResourceNameCompletionFunc(f, types.ClusterGVR()), 107 Run: func(cmd *cobra.Command, args []string) { 108 util.CheckErr(o.validate(args)) 109 util.CheckErr(o.complete()) 110 if o.showExample { 111 util.CheckErr(o.runShowExample()) 112 } else { 113 util.CheckErr(o.connect()) 114 } 115 }, 116 } 117 cmd.Flags().StringVarP(&o.PodName, "instance", "i", "", "The instance name to connect.") 118 flags.AddComponentFlag(f, cmd, &o.componentName, "The component to connect. If not specified, pick up the first one.") 119 cmd.Flags().BoolVar(&o.showExample, "show-example", false, "Show how to connect to cluster/instance from different clients.") 120 cmd.Flags().BoolVar(&o.showPassword, "show-password", false, "Show password in example.") 121 122 cmd.Flags().StringVar(&o.clientType, "client", "", "Which client connection example should be output, only valid if --show-example is true.") 123 124 cmd.Flags().StringVar(&o.userName, "as-user", "", "Connect to cluster as user") 125 126 util.CheckErr(cmd.RegisterFlagCompletionFunc("client", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 127 var types []string 128 for _, t := range models.ClientTypes() { 129 if strings.HasPrefix(t, toComplete) { 130 types = append(types, t) 131 } 132 } 133 return types, cobra.ShellCompDirectiveNoFileComp 134 })) 135 return cmd 136 } 137 138 func (o *ConnectOptions) runShowExample() error { 139 // get connection info 140 info, err := o.getConnectionInfo() 141 if err != nil { 142 return err 143 } 144 // make sure engine is initialized 145 if o.engine == nil { 146 return fmt.Errorf("engine is not initialized yet") 147 } 148 149 // if cluster does not have public endpoints, prompts to use port-forward command and 150 // connect cluster from localhost 151 if o.privateEndPoint { 152 fmt.Fprintf(o.Out, "# cluster %s does not have public endpoints, you can run following command and connect cluster from localhost\n"+ 153 "kubectl port-forward service/%s %s:%s\n\n", o.clusterName, o.svc.Name, info.Port, info.Port) 154 info.Host = "127.0.0.1" 155 } 156 157 fmt.Fprint(o.Out, o.engine.ConnectExample(info, o.clientType)) 158 return nil 159 } 160 161 func (o *ConnectOptions) validate(args []string) error { 162 if len(args) > 1 { 163 return fmt.Errorf("only support to connect one cluster") 164 } 165 166 // cluster name and pod instance are mutual exclusive 167 if len(o.PodName) > 0 { 168 if len(args) > 0 { 169 return fmt.Errorf("specify either cluster name or instance name, they are exclusive") 170 } 171 if len(o.componentName) > 0 { 172 return fmt.Errorf("component name is valid only when cluster name is specified") 173 } 174 } else if len(args) == 0 { 175 return fmt.Errorf("either cluster name or instance name should be specified") 176 } 177 178 // set custer name 179 if len(args) > 0 { 180 o.clusterName = args[0] 181 } 182 183 // validate user name and password 184 if len(o.userName) > 0 { 185 // read password from stdin 186 fmt.Print("Password: ") 187 if bytePassword, err := terminal.ReadPassword(int(os.Stdin.Fd())); err != nil { 188 return err 189 } else { 190 o.userPasswd = string(bytePassword) 191 } 192 } 193 return nil 194 } 195 196 func (o *ConnectOptions) complete() error { 197 var err error 198 if err = o.ExecOptions.Complete(); err != nil { 199 return err 200 } 201 // opt 1. specified pod name 202 // 1.1 get pod by name 203 if len(o.PodName) > 0 { 204 if o.Pod, err = o.Client.CoreV1().Pods(o.Namespace).Get(context.Background(), o.PodName, metav1.GetOptions{}); err != nil { 205 return err 206 } 207 o.clusterName = cluster.GetPodClusterName(o.Pod) 208 o.componentName = cluster.GetPodComponentName(o.Pod) 209 } 210 211 // cannot infer characterType from pod directly (neither from pod annotation nor pod label) 212 // so we have to get cluster definition first to get characterType 213 // opt 2. specified cluster name 214 // 2.1 get cluster by name 215 if o.targetCluster, err = cluster.GetClusterByName(o.Dynamic, o.clusterName, o.Namespace); err != nil { 216 return err 217 } 218 // get cluster def 219 if o.targetClusterDef, err = cluster.GetClusterDefByName(o.Dynamic, o.targetCluster.Spec.ClusterDefRef); err != nil { 220 return err 221 } 222 223 // 2.2 fill component name, use the first component by default 224 if len(o.componentName) == 0 { 225 o.component = &o.targetCluster.Spec.ComponentSpecs[0] 226 o.componentName = o.component.Name 227 } else { 228 // verify component 229 if o.component = o.targetCluster.Spec.GetComponentByName(o.componentName); o.component == nil { 230 return fmt.Errorf("failed to get component %s. Check the list of components use: \n\tkbcli cluster list-components %s -n %s", o.componentName, o.clusterName, o.Namespace) 231 } 232 } 233 234 // 2.3 get character type 235 if o.componentDef = o.targetClusterDef.GetComponentDefByName(o.component.ComponentDefRef); o.componentDef == nil { 236 return fmt.Errorf("failed to get component def :%s", o.component.ComponentDefRef) 237 } 238 239 // 2.4. get pod to connect, make sure o.clusterName, o.componentName are set before this step 240 if len(o.PodName) == 0 { 241 if err = o.getTargetPod(); err != nil { 242 return err 243 } 244 if o.Pod, err = o.Client.CoreV1().Pods(o.Namespace).Get(context.TODO(), o.PodName, metav1.GetOptions{}); err != nil { 245 return err 246 } 247 } 248 return nil 249 } 250 251 // connect creates connection string and connects to cluster 252 func (o *ConnectOptions) connect() error { 253 if o.componentDef == nil { 254 return fmt.Errorf("component def is not initialized") 255 } 256 257 var err error 258 259 if o.engine, err = register.NewClusterCommands(o.componentDef.CharacterType); err != nil { 260 return err 261 } 262 263 var authInfo *engines.AuthInfo 264 if len(o.userName) > 0 { 265 authInfo = &engines.AuthInfo{} 266 authInfo.UserName = o.userName 267 authInfo.UserPasswd = o.userPasswd 268 } else if authInfo, err = o.getAuthInfo(); err != nil { 269 return err 270 } 271 272 o.ExecOptions.ContainerName = o.engine.Container() 273 o.ExecOptions.Command = o.engine.ConnectCommand(authInfo) 274 if klog.V(1).Enabled() { 275 fmt.Fprintf(o.Out, "connect with cmd: %s", o.ExecOptions.Command) 276 } 277 return o.ExecOptions.Run() 278 } 279 280 func (o *ConnectOptions) getAuthInfo() (*engines.AuthInfo, error) { 281 // select secrets by labels, admin account is preferred 282 labels := fmt.Sprintf("%s=%s,%s=%s,%s=%s", 283 constant.AppInstanceLabelKey, o.clusterName, 284 constant.KBAppComponentLabelKey, o.componentName, 285 constant.ClusterAccountLabelKey, (string)(appsv1alpha1.AdminAccount), 286 ) 287 288 secrets, err := o.Client.CoreV1().Secrets(o.Namespace).List(context.Background(), metav1.ListOptions{LabelSelector: labels}) 289 if err != nil { 290 return nil, fmt.Errorf("failed to list secrets for cluster %s, component %s, err %v", o.clusterName, o.componentName, err) 291 } 292 if len(secrets.Items) == 0 { 293 return nil, nil 294 } 295 return &engines.AuthInfo{ 296 UserName: string(secrets.Items[0].Data["username"]), 297 UserPasswd: string(secrets.Items[0].Data["password"]), 298 }, nil 299 } 300 301 func (o *ConnectOptions) getTargetPod() error { 302 // make sure cluster name and component name are set 303 if len(o.clusterName) == 0 { 304 return fmt.Errorf("cluster name is not set yet") 305 } 306 if len(o.componentName) == 0 { 307 return fmt.Errorf("component name is not set yet") 308 } 309 310 // get instances for given cluster name and component name 311 infos := cluster.GetSimpleInstanceInfosForComponent(o.Dynamic, o.clusterName, o.componentName, o.Namespace) 312 if len(infos) == 0 || infos[0].Name == constant.ComponentStatusDefaultPodName { 313 return fmt.Errorf("failed to find the instance to connect, please check cluster status") 314 } 315 316 o.PodName = infos[0].Name 317 318 // print instance info that we connect 319 if len(infos) == 1 { 320 fmt.Fprintf(o.Out, "Connect to instance %s\n", o.PodName) 321 return nil 322 } 323 324 // output all instance infos 325 var nameRoles = make([]string, len(infos)) 326 for i, info := range infos { 327 if len(info.Role) == 0 { 328 nameRoles[i] = info.Name 329 } else { 330 nameRoles[i] = fmt.Sprintf("%s(%s)", info.Name, info.Role) 331 } 332 } 333 fmt.Fprintf(o.Out, "Connect to instance %s: out of %s\n", o.PodName, strings.Join(nameRoles, ", ")) 334 return nil 335 } 336 337 func (o *ConnectOptions) getConnectionInfo() (*engines.ConnectionInfo, error) { 338 // make sure component and componentDef are set before this step 339 if o.component == nil || o.componentDef == nil { 340 return nil, fmt.Errorf("failed to get component or component def") 341 } 342 343 info := &engines.ConnectionInfo{} 344 getter := cluster.ObjectsGetter{ 345 Client: o.Client, 346 Dynamic: o.Dynamic, 347 Name: o.clusterName, 348 Namespace: o.Namespace, 349 GetOptions: cluster.GetOptions{ 350 WithClusterDef: true, 351 WithService: true, 352 WithSecret: true, 353 }, 354 } 355 356 objs, err := getter.Get() 357 if err != nil { 358 return nil, err 359 } 360 361 info.ClusterName = o.clusterName 362 info.ComponentName = o.componentName 363 info.HeadlessEndpoint = getOneHeadlessEndpoint(objs.ClusterDef, objs.Secrets) 364 // get username and password 365 if info.User, info.Password, err = getUserAndPassword(objs.ClusterDef, objs.Secrets); err != nil { 366 return nil, err 367 } 368 if !o.showPassword { 369 info.Password = passwordMask 370 } 371 // get host and port, use external endpoints first, if external endpoints are empty, 372 // use internal endpoints 373 374 // TODO: now the primary component is the first component, that may not be correct, 375 // maybe show all components connection info in the future. 376 internalSvcs, externalSvcs := cluster.GetComponentServices(objs.Services, o.component) 377 switch { 378 case len(externalSvcs) > 0: 379 // cluster has public endpoints 380 o.svc = externalSvcs[0] 381 info.Host = cluster.GetExternalAddr(o.svc) 382 info.Port = fmt.Sprintf("%d", o.svc.Spec.Ports[0].Port) 383 case len(internalSvcs) > 0: 384 // cluster does not have public endpoints 385 o.svc = internalSvcs[0] 386 info.Host = o.svc.Spec.ClusterIP 387 info.Port = fmt.Sprintf("%d", o.svc.Spec.Ports[0].Port) 388 o.privateEndPoint = true 389 default: 390 // find no endpoints 391 return nil, fmt.Errorf("failed to find any cluster endpoints") 392 } 393 394 if o.engine, err = register.NewClusterCommands(o.componentDef.CharacterType); err != nil { 395 return nil, err 396 } 397 398 return info, nil 399 } 400 401 // getUserAndPassword gets cluster user and password from secrets 402 func getUserAndPassword(clusterDef *appsv1alpha1.ClusterDefinition, secrets *corev1.SecretList) (string, string, error) { 403 var ( 404 user, password = "", "" 405 err error 406 ) 407 408 if len(secrets.Items) == 0 { 409 return user, password, fmt.Errorf("failed to find the cluster username and password") 410 } 411 412 getPasswordKey := func(connectionCredential map[string]string) string { 413 for k := range connectionCredential { 414 if strings.Contains(k, "password") { 415 return k 416 } 417 } 418 return "password" 419 } 420 421 getSecretVal := func(secret *corev1.Secret, key string) (string, error) { 422 val, ok := secret.Data[key] 423 if !ok { 424 return "", fmt.Errorf("failed to find the cluster %s", key) 425 } 426 return string(val), nil 427 } 428 429 // now, we only use the first secret 430 var secret corev1.Secret 431 for i, s := range secrets.Items { 432 if strings.Contains(s.Name, "conn-credential") { 433 secret = secrets.Items[i] 434 break 435 } 436 } 437 user, err = getSecretVal(&secret, "username") 438 if err != nil { 439 return user, password, err 440 } 441 442 passwordKey := getPasswordKey(clusterDef.Spec.ConnectionCredential) 443 password, err = getSecretVal(&secret, passwordKey) 444 return user, password, err 445 } 446 447 // getOneHeadlessEndpoint gets cluster headlessEndpoint from secrets 448 func getOneHeadlessEndpoint(clusterDef *appsv1alpha1.ClusterDefinition, secrets *corev1.SecretList) string { 449 if len(secrets.Items) == 0 { 450 return "" 451 } 452 val, ok := secrets.Items[0].Data["headlessEndpoint"] 453 if !ok { 454 return "" 455 } 456 return string(val) 457 }