github.com/oam-dev/kubevela@v1.9.11/references/cli/cluster.go (about) 1 /* 2 Copyright 2021 The KubeVela Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package cli 18 19 import ( 20 "context" 21 "fmt" 22 "sort" 23 "strings" 24 25 "github.com/crossplane/crossplane-runtime/pkg/meta" 26 "github.com/fatih/color" 27 "github.com/kubevela/pkg/util/runtime" 28 "github.com/kubevela/pkg/util/slices" 29 clustergatewayapi "github.com/oam-dev/cluster-gateway/pkg/apis/cluster/v1alpha1" 30 "github.com/oam-dev/cluster-gateway/pkg/config" 31 "github.com/pkg/errors" 32 "github.com/spf13/cobra" 33 "k8s.io/apimachinery/pkg/labels" 34 "k8s.io/client-go/tools/clientcmd" 35 "k8s.io/kubectl/pkg/util/i18n" 36 "k8s.io/kubectl/pkg/util/templates" 37 "k8s.io/utils/pointer" 38 "sigs.k8s.io/controller-runtime/pkg/client" 39 40 "github.com/oam-dev/kubevela/apis/types" 41 velacmd "github.com/oam-dev/kubevela/pkg/cmd" 42 "github.com/oam-dev/kubevela/pkg/multicluster" 43 "github.com/oam-dev/kubevela/pkg/utils/common" 44 cmdutil "github.com/oam-dev/kubevela/pkg/utils/util" 45 ) 46 47 const ( 48 // FlagClusterName specifies the cluster name 49 FlagClusterName = "name" 50 // FlagClusterManagementEngine specifies the cluster management type, eg: ocm 51 FlagClusterManagementEngine = "engine" 52 // FlagKubeConfigPath specifies the kubeconfig path 53 FlagKubeConfigPath = "kubeconfig-path" 54 // FlagInClusterBootstrap prescribes the cluster registration to use the internal 55 // IP from the kube-public/cluster-info configmap, otherwise the endpoint in the 56 // hub kubeconfig will be used for registration. 57 FlagInClusterBootstrap = "in-cluster-boostrap" 58 59 // CreateNamespace specifies the namespace need to create in managedCluster 60 CreateNamespace = "create-namespace" 61 62 // CreateLabel specifies the labels need to create in managedCluster 63 CreateLabel = "labels" 64 ) 65 66 // ClusterCommandGroup create a group of cluster command 67 func ClusterCommandGroup(f velacmd.Factory, order string, c common.Args, ioStreams cmdutil.IOStreams) *cobra.Command { 68 cmd := &cobra.Command{ 69 Use: "cluster", 70 Short: "Manage Kubernetes clusters.", 71 Long: "Manage Kubernetes clusters for continuous delivery.", 72 Annotations: map[string]string{ 73 types.TagCommandType: types.TypePlatform, 74 types.TagCommandOrder: order, 75 }, 76 // check if cluster-gateway is ready 77 PersistentPreRunE: func(cmd *cobra.Command, args []string) error { 78 k8sClient, err := c.GetClient() 79 if err != nil { 80 return errors.Wrapf(err, "failed to get k8s client") 81 } 82 svc, err := multicluster.GetClusterGatewayService(context.Background(), k8sClient) 83 if err != nil { 84 return errors.Wrapf(err, "failed to get cluster secret namespace, please ensure cluster gateway is correctly deployed") 85 } 86 multicluster.ClusterGatewaySecretNamespace = svc.Namespace 87 return nil 88 }, 89 } 90 cmd.SetOut(ioStreams.Out) 91 cmd.AddCommand( 92 NewClusterListCommand(&c), 93 NewClusterJoinCommand(&c, ioStreams), 94 NewClusterRenameCommand(&c), 95 NewClusterDetachCommand(&c), 96 NewClusterProbeCommand(&c), 97 NewClusterLabelCommandGroup(&c), 98 NewClusterAliasCommand(&c), 99 NewClusterExportConfigCommand(f, ioStreams), 100 ) 101 return cmd 102 } 103 104 // NewClusterListCommand create cluster list command 105 func NewClusterListCommand(c *common.Args) *cobra.Command { 106 cmd := &cobra.Command{ 107 Use: "list", 108 Aliases: []string{"ls"}, 109 Short: "list managed clusters.", 110 Long: "list worker clusters managed by KubeVela.", 111 Args: cobra.ExactArgs(0), 112 RunE: func(cmd *cobra.Command, args []string) error { 113 table := newUITable().AddRow("CLUSTER", "ALIAS", "TYPE", "ENDPOINT", "ACCEPTED", "LABELS") 114 clsClient, err := c.GetClient() 115 if err != nil { 116 return err 117 } 118 clusters, err := multicluster.NewClusterClient(clsClient).List(context.Background()) 119 if err != nil { 120 return errors.Wrap(err, "fail to get registered cluster") 121 } 122 for _, cluster := range clusters.Items { 123 var labels []string 124 for k, v := range cluster.Labels { 125 if !strings.HasPrefix(k, config.MetaApiGroupName) { 126 labels = append(labels, color.CyanString(k)+"="+color.GreenString(v)) 127 } 128 } 129 sort.Strings(labels) 130 if len(labels) == 0 { 131 labels = append(labels, "") 132 } 133 for i, l := range labels { 134 if i == 0 { 135 table.AddRow(cluster.Name, cluster.Spec.Alias, cluster.Spec.CredentialType, cluster.Spec.Endpoint, fmt.Sprintf("%v", cluster.Spec.Accepted), l) 136 } else { 137 table.AddRow("", "", "", "", "", l) 138 } 139 } 140 } 141 if len(table.Rows) == 1 { 142 cmd.Println("No cluster found.") 143 } else { 144 cmd.Println(table.String()) 145 } 146 return nil 147 }, 148 } 149 return cmd 150 } 151 152 // NewClusterJoinCommand create command to help user join cluster to multicluster management 153 func NewClusterJoinCommand(c *common.Args, ioStreams cmdutil.IOStreams) *cobra.Command { 154 cmd := &cobra.Command{ 155 Use: "join [KUBECONFIG]", 156 Short: "join managed cluster.", 157 Long: "join managed cluster by kubeconfig.", 158 Example: "# Join cluster declared in my-child-cluster.kubeconfig\n" + 159 "> vela cluster join my-child-cluster.kubeconfig --name example-cluster\n" + 160 "> vela cluster join my-child-cluster.kubeconfig --name example-cluster --labels project=kubevela,owner=oam-dev", 161 Args: cobra.ExactArgs(1), 162 RunE: func(cmd *cobra.Command, args []string) error { 163 // get ClusterName from flag or config 164 clusterName, err := cmd.Flags().GetString(FlagClusterName) 165 if err != nil { 166 return errors.Wrapf(err, "failed to get cluster name flag") 167 } 168 clusterManagementType, err := cmd.Flags().GetString(FlagClusterManagementEngine) 169 if err != nil { 170 return errors.Wrapf(err, "failed to get cluster management type flag") 171 } 172 // get need created namespace in managed cluster 173 createNamespace, err := cmd.Flags().GetString(CreateNamespace) 174 if err != nil { 175 return errors.Wrapf(err, "failed to get create namespace") 176 } 177 labels, err := cmd.Flags().GetString(CreateLabel) 178 if err != nil { 179 return errors.Wrapf(err, "failed to get label") 180 } 181 client, err := c.GetClient() 182 if err != nil { 183 return err 184 } 185 restConfig, err := c.GetConfig() 186 if err != nil { 187 return err 188 } 189 190 var inClusterBootstrap *bool 191 if _inClusterBootstrap, err := cmd.Flags().GetBool(FlagInClusterBootstrap); err == nil { 192 inClusterBootstrap = pointer.Bool(_inClusterBootstrap) 193 } 194 195 managedClusterKubeConfig := args[0] 196 ctx := context.WithValue(context.Background(), multicluster.KubeConfigContext, restConfig) 197 clusterConfig, err := multicluster.JoinClusterByKubeConfig(ctx, client, managedClusterKubeConfig, clusterName, 198 multicluster.JoinClusterCreateNamespaceOption(createNamespace), 199 multicluster.JoinClusterEngineOption(clusterManagementType), 200 multicluster.JoinClusterOCMOptions{ 201 InClusterBootstrap: inClusterBootstrap, 202 IoStreams: ioStreams, 203 HubConfig: restConfig, 204 TrackingSpinnerFactory: newTrackingSpinner, 205 }, 206 multicluster.JoinClusterAlreadyExistCallback(func(name string) bool { 207 if !NewUserInput().AskBool(fmt.Sprintf("Cluster %s already exists, do you want to overwrite it?", name), &UserInputOptions{AssumeYes: assumeYes}) { 208 _, _ = fmt.Fprintf(cmd.OutOrStdout(), "Terminated.\n") 209 return false 210 } 211 return true 212 })) 213 if err != nil { 214 return err 215 } 216 cmd.Printf("Successfully add cluster %s, endpoint: %s.\n", clusterName, clusterConfig.Cluster.Server) 217 218 if len(labels) > 0 { 219 return addClusterLabels(cmd, c, clusterName, labels) 220 } 221 return nil 222 }, 223 } 224 cmd.Flags().StringP(FlagClusterName, "n", "", "Specify the cluster name. If empty, it will use the cluster name in config file. Default to be empty.") 225 cmd.Flags().StringP(FlagClusterManagementEngine, "t", multicluster.ClusterGateWayEngine, "Specify the cluster management engine. If empty, it will use cluster-gateway cluster management solution. Default to be empty.") 226 cmd.Flags().StringP(CreateNamespace, "", types.DefaultKubeVelaNS, "Specifies the namespace need to create in managedCluster") 227 cmd.Flags().BoolP(FlagInClusterBootstrap, "", true, "If true, the registering managed cluster "+ 228 `will use the internal endpoint prescribed in the hub cluster's configmap "kube-public/cluster-info to register "`+ 229 "itself to the hub cluster. Otherwise use the original endpoint from the hub kubeconfig.") 230 cmd.Flags().StringP(CreateLabel, "", "", "Specifies the labels need to create in managedCluster") 231 232 return cmd 233 } 234 235 // NewClusterRenameCommand create command to help user rename cluster 236 func NewClusterRenameCommand(c *common.Args) *cobra.Command { 237 cmd := &cobra.Command{ 238 Use: "rename [OLD_NAME] [NEW_NAME]", 239 Short: "rename managed cluster.", 240 Long: "rename managed cluster.", 241 Args: cobra.ExactArgs(2), 242 RunE: func(cmd *cobra.Command, args []string) error { 243 oldClusterName := args[0] 244 newClusterName := args[1] 245 k8sClient, err := c.GetClient() 246 if err != nil { 247 return err 248 } 249 if err := multicluster.RenameCluster(context.Background(), k8sClient, oldClusterName, newClusterName); err != nil { 250 return err 251 } 252 cmd.Printf("Rename cluster %s to %s successfully.\n", oldClusterName, newClusterName) 253 return nil 254 }, 255 } 256 return cmd 257 } 258 259 // NewClusterDetachCommand create command to help user detach existing cluster 260 func NewClusterDetachCommand(c *common.Args) *cobra.Command { 261 cmd := &cobra.Command{ 262 Use: "detach [CLUSTER_NAME]", 263 Short: "detach managed cluster.", 264 Long: "detach managed cluster.", 265 Args: cobra.ExactArgs(1), 266 RunE: func(cmd *cobra.Command, args []string) error { 267 clusterName := args[0] 268 configPath, _ := cmd.Flags().GetString(FlagKubeConfigPath) 269 cli, err := c.GetClient() 270 if err != nil { 271 return err 272 } 273 err = multicluster.DetachCluster(context.Background(), cli, clusterName, 274 multicluster.DetachClusterManagedClusterKubeConfigPathOption(configPath)) 275 if err != nil { 276 return err 277 } 278 cmd.Printf("Detach cluster %s successfully.\n", clusterName) 279 return nil 280 }, 281 } 282 cmd.Flags().StringP(FlagKubeConfigPath, "p", "", "Specify the kubeconfig path of managed cluster. If you use ocm to manage your cluster, you must set the kubeconfig-path.") 283 return cmd 284 } 285 286 // NewClusterAliasCommand create an alias to the named cluster 287 func NewClusterAliasCommand(c *common.Args) *cobra.Command { 288 cmd := &cobra.Command{ 289 Use: "alias CLUSTER_NAME ALIAS", 290 Short: "alias a named cluster.", 291 Long: "alias a named cluster.", 292 Args: cobra.ExactArgs(2), 293 RunE: func(cmd *cobra.Command, args []string) error { 294 clusterName, aliasName := args[0], args[1] 295 k8sClient, err := c.GetClient() 296 if err != nil { 297 return err 298 } 299 if err = multicluster.AliasCluster(context.Background(), k8sClient, clusterName, aliasName); err != nil { 300 return err 301 } 302 cmd.Printf("Alias cluster %s as %s.\n", clusterName, aliasName) 303 return nil 304 }, 305 } 306 return cmd 307 } 308 309 // NewClusterProbeCommand create command to help user try health probe for existing cluster 310 // TODO(somefive): move prob logic into cluster management 311 func NewClusterProbeCommand(c *common.Args) *cobra.Command { 312 cmd := &cobra.Command{ 313 Use: "probe [CLUSTER_NAME]", 314 Short: "health probe managed cluster.", 315 Long: "health probe managed cluster.", 316 Args: cobra.ExactArgs(1), 317 RunE: func(cmd *cobra.Command, args []string) error { 318 clusterName := args[0] 319 config, err := c.GetConfig() 320 if err != nil { 321 return err 322 } 323 content, err := multicluster.RequestRawK8sAPIForCluster(context.TODO(), "healthz", clusterName, config) 324 if err != nil { 325 return errors.Wrapf(err, "failed connect cluster %s", clusterName) 326 } 327 cmd.Printf("Connect to cluster %s successfully.\n%s\n", clusterName, string(content)) 328 return nil 329 }, 330 } 331 return cmd 332 } 333 334 // NewClusterLabelCommandGroup create a group of commands to manage cluster labels 335 func NewClusterLabelCommandGroup(c *common.Args) *cobra.Command { 336 cmd := &cobra.Command{ 337 Use: "labels", 338 Short: "Manage Kubernetes Cluster Labels.", 339 Long: "Manage Kubernetes Cluster Labels for Continuous Delivery.", 340 } 341 cmd.AddCommand( 342 NewClusterAddLabelsCommand(c), 343 NewClusterDelLabelsCommand(c), 344 ) 345 return cmd 346 } 347 348 func updateClusterLabelAndPrint(cmd *cobra.Command, cli client.Client, vc *multicluster.VirtualCluster, clusterName string) (err error) { 349 if err = cli.Update(context.Background(), vc.Object); err != nil { 350 return errors.Errorf("failed to update labels for cluster %s, type: %s", vc.FullName(), vc.Type) 351 } 352 if vc, err = multicluster.GetVirtualCluster(context.Background(), cli, clusterName); err != nil { 353 return errors.Wrapf(err, "failed to get updated cluster %s", clusterName) 354 } 355 cmd.Printf("Successfully update labels for cluster %s, type: %s.\n", vc.FullName(), vc.Type) 356 if len(vc.Labels) == 0 { 357 cmd.Println("No valid label exists.") 358 } 359 var keys []string 360 for k := range vc.Labels { 361 if !strings.HasPrefix(k, config.MetaApiGroupName) { 362 keys = append(keys, k) 363 } 364 } 365 sort.Strings(keys) 366 for _, k := range keys { 367 cmd.Println(color.CyanString(k) + "=" + color.GreenString(vc.Labels[k])) 368 } 369 return nil 370 } 371 372 // NewClusterAddLabelsCommand create command to add labels for managed cluster 373 func NewClusterAddLabelsCommand(c *common.Args) *cobra.Command { 374 cmd := &cobra.Command{ 375 Use: "add CLUSTER_NAME LABELS", 376 Short: "add labels to managed cluster.", 377 Long: "add labels to managed cluster.", 378 Example: "vela cluster labels add my-cluster project=kubevela,owner=oam-dev", 379 Args: cobra.ExactArgs(2), 380 RunE: func(cmd *cobra.Command, args []string) error { 381 clusterName := args[0] 382 labels := args[1] 383 return addClusterLabels(cmd, c, clusterName, labels) 384 }, 385 } 386 return cmd 387 } 388 389 func addClusterLabels(cmd *cobra.Command, c *common.Args, clusterName, labels string) error { 390 addLabels := map[string]string{} 391 for _, kv := range strings.Split(labels, ",") { 392 parts := strings.Split(kv, "=") 393 if len(parts) != 2 { 394 return errors.Errorf("invalid label key-value pair %s, should use the format LABEL_KEY=LABEL_VAL", kv) 395 } 396 addLabels[parts[0]] = parts[1] 397 } 398 399 cli, err := c.GetClient() 400 if err != nil { 401 return err 402 } 403 vc, err := multicluster.GetVirtualCluster(context.Background(), cli, clusterName) 404 if err != nil { 405 return errors.Wrapf(err, "failed to get cluster %s", clusterName) 406 } 407 if vc.Object == nil { 408 return errors.Errorf("cluster type %s do not support add labels now", vc.Type) 409 } 410 meta.AddLabels(vc.Object, addLabels) 411 return updateClusterLabelAndPrint(cmd, cli, vc, clusterName) 412 } 413 414 // NewClusterDelLabelsCommand create command to delete labels for managed cluster 415 func NewClusterDelLabelsCommand(c *common.Args) *cobra.Command { 416 cmd := &cobra.Command{ 417 Use: "del CLUSTER_NAME LABELS", 418 Aliases: []string{"delete", "remove"}, 419 Short: "Delete labels for managed cluster.", 420 Long: "Delete labels for managed cluster.", 421 Args: cobra.ExactArgs(2), 422 Example: "vela cluster labels del my-cluster project,owner", 423 RunE: func(cmd *cobra.Command, args []string) error { 424 clusterName := args[0] 425 removeLabels := strings.Split(args[1], ",") 426 cli, err := c.GetClient() 427 if err != nil { 428 return err 429 } 430 vc, err := multicluster.GetVirtualCluster(context.Background(), cli, clusterName) 431 if err != nil { 432 return errors.Wrapf(err, "failed to get cluster %s", clusterName) 433 } 434 if vc.Object == nil { 435 return errors.Errorf("cluster type %s do not support delete labels now", vc.Type) 436 } 437 for _, l := range removeLabels { 438 if _, found := vc.Labels[l]; !found { 439 return errors.Errorf("no such label %s", l) 440 } 441 } 442 meta.RemoveLabels(vc.Object, removeLabels...) 443 return updateClusterLabelAndPrint(cmd, cli, vc, clusterName) 444 }, 445 } 446 return cmd 447 } 448 449 // NewClusterExportConfigCommand create command to export multi-cluster config 450 func NewClusterExportConfigCommand(f velacmd.Factory, ioStreams cmdutil.IOStreams) *cobra.Command { 451 var labelSelector string 452 cmd := &cobra.Command{ 453 Use: "export-config", 454 Short: i18n.T("Export multi-cluster kubeconfig."), 455 Long: templates.LongDesc(i18n.T(` 456 Export multi-cluster kubeconfig 457 458 Load existing cluster kubeconfig and list clusters registered in 459 KubeVela. Export the proxy access of these clusters to KubeConfig 460 and print it out. 461 `)), 462 Example: templates.Examples(i18n.T(` 463 # Export all clusters to kubeconfig 464 vela cluster export-config 465 466 # Export clusters with specified kubeconfig 467 KUBECONFIG=./my-hub-cluster.kubeconfig vela cluster export-config 468 469 # Export clusters with specified labels 470 vela cluster export-config -l gpu-cluster=true 471 472 # Export clusters to kubeconfig and save in file 473 vela cluster export-config > my-vela.kubeconfig 474 475 # Use the exported kubeconfig in kubectl 476 KUBECONFIG=my-vela.kubeconfig kubectl get namespaces --cluster c2 477 `)), 478 RunE: func(cmd *cobra.Command, args []string) error { 479 cfg := runtime.Must(clientcmd.NewDefaultClientConfigLoadingRules().Load()) 480 ctx, ok := cfg.Contexts[cfg.CurrentContext] 481 if !ok { 482 return fmt.Errorf("cannot find current context %s in given config", cfg.CurrentContext) 483 } 484 baseCluster, ok := cfg.Clusters[ctx.Cluster] 485 if !ok { 486 return fmt.Errorf("cannot find base cluster %s in given config", ctx.Cluster) 487 } 488 selector, err := labels.Parse(labelSelector) 489 if err != nil { 490 return fmt.Errorf("invalid selector %s: %w", labelSelector, err) 491 } 492 clusters, err := multicluster.NewClusterClient(f.Client()).List(cmd.Context(), client.MatchingLabelsSelector{Selector: selector}) 493 if err != nil { 494 return fmt.Errorf("failed to load clusters: %w", err) 495 } 496 clusterNames := slices.Filter( 497 slices.Map(clusters.Items, func(cluster clustergatewayapi.VirtualCluster) string { return cluster.Name }), 498 func(s string) bool { return s != multicluster.ClusterLocalName }) 499 500 if len(clusterNames) == 0 { 501 return fmt.Errorf("no cluster found") 502 } 503 _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "%d cluster loaded: [%s]\n", len(clusterNames), strings.Join(clusterNames, ", ")) 504 505 delete(cfg.Clusters, ctx.Cluster) 506 ctx.Cluster = types.ClusterLocalName 507 cfg.Clusters[types.ClusterLocalName] = baseCluster.DeepCopy() 508 for _, clusterName := range clusterNames { 509 cls := baseCluster.DeepCopy() 510 cls.LocationOfOrigin = "" 511 cls.Server = strings.Join([]string{cls.Server, "apis", 512 clustergatewayapi.SchemeGroupVersion.Group, 513 clustergatewayapi.SchemeGroupVersion.Version, 514 "clustergateways", clusterName, "proxy"}, "/") 515 cfg.Clusters[clusterName] = cls 516 } 517 bs, err := clientcmd.Write(*cfg) 518 if err != nil { 519 return fmt.Errorf("failed to marshal generated kubeconfig: %w", err) 520 } 521 _, _ = ioStreams.Out.Write(bs) 522 _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "kubeconfig generated.\n") 523 return nil 524 }, 525 } 526 cmd.Flags().StringVarP(&labelSelector, "selector", "l", labelSelector, "LabelSelector for select clusters to export.") 527 528 return velacmd.NewCommandBuilder(f, cmd).WithResponsiveWriter().Build() 529 }