github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/cli/cmd/infrastructure/cluster.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 infrastructure
    21  
    22  import (
    23  	"fmt"
    24  	"os"
    25  	"os/user"
    26  	"path/filepath"
    27  	"strings"
    28  
    29  	"github.com/StudioSol/set"
    30  	kubekeyapiv1alpha2 "github.com/kubesphere/kubekey/v3/cmd/kk/apis/kubekey/v1alpha2"
    31  	"github.com/kubesphere/kubekey/v3/cmd/kk/pkg/common"
    32  	"github.com/spf13/cobra"
    33  	"golang.org/x/exp/slices"
    34  	"k8s.io/apimachinery/pkg/util/rand"
    35  	"k8s.io/cli-runtime/pkg/genericiooptions"
    36  
    37  	"github.com/1aal/kubeblocks/pkg/cli/cmd/infrastructure/builder"
    38  	"github.com/1aal/kubeblocks/pkg/cli/cmd/infrastructure/types"
    39  	"github.com/1aal/kubeblocks/pkg/cli/printer"
    40  	"github.com/1aal/kubeblocks/pkg/cli/util/prompt"
    41  	cfgcore "github.com/1aal/kubeblocks/pkg/configuration/core"
    42  	cfgutil "github.com/1aal/kubeblocks/pkg/configuration/util"
    43  )
    44  
    45  type clusterOptions struct {
    46  	types.Cluster
    47  	IOStreams genericiooptions.IOStreams
    48  
    49  	clusterConfig string
    50  	clusterName   string
    51  	timeout       int64
    52  	nodes         []string
    53  }
    54  
    55  func buildCommonFlags(cmd *cobra.Command, o *clusterOptions) {
    56  	cmd.Flags().StringVarP(&o.clusterConfig, "config", "c", "", "Specify infra cluster config file. [option]")
    57  	cmd.Flags().StringVarP(&o.clusterName, "name", "", "", "Specify kubernetes cluster name")
    58  	cmd.Flags().StringSliceVarP(&o.nodes, "nodes", "", nil, "List of machines on which kubernetes is installed. [require]")
    59  
    60  	// for user
    61  	cmd.Flags().StringVarP(&o.User.Name, "user", "u", "", "Specify the account to access the remote server. [require]")
    62  	cmd.Flags().Int64VarP(&o.timeout, "timeout", "t", 30, "Specify the ssh timeout.[option]")
    63  	cmd.Flags().StringVarP(&o.User.Password, "password", "p", "", "Specify the password for the account to execute sudo. [option]")
    64  	cmd.Flags().StringVarP(&o.User.PrivateKey, "private-key", "", "", "The PrimaryKey for ssh to the remote machine. [option]")
    65  	cmd.Flags().StringVarP(&o.User.PrivateKeyPath, "private-key-path", "", "", "Specify the file PrimaryKeyPath of ssh to the remote machine. default ~/.ssh/id_rsa.")
    66  
    67  	cmd.Flags().StringSliceVarP(&o.RoleGroup.ETCD, "etcd", "", nil, "Specify etcd nodes")
    68  	cmd.Flags().StringSliceVarP(&o.RoleGroup.Master, "master", "", nil, "Specify master nodes")
    69  	cmd.Flags().StringSliceVarP(&o.RoleGroup.Worker, "worker", "", nil, "Specify worker nodes")
    70  }
    71  
    72  func (o *clusterOptions) Complete() error {
    73  	if o.clusterName == "" && o.clusterConfig == "" {
    74  		o.clusterName = "kubeblocks-" + rand.String(6)
    75  		fmt.Printf("The cluster name is not set, auto generate cluster name: %s\n", o.clusterName)
    76  	}
    77  
    78  	if o.clusterConfig != "" {
    79  		return o.fillClusterConfig(o.clusterConfig)
    80  	}
    81  
    82  	if o.User.Name == "" {
    83  		currentUser, err := user.Current()
    84  		if err != nil {
    85  			return err
    86  		}
    87  		o.User.Name = currentUser.Username
    88  		fmt.Printf("The user is not set, use current user %s\n", o.User.Name)
    89  	}
    90  	if o.User.Password == "" && o.User.PrivateKey == "" && o.User.PrivateKeyPath == "" {
    91  		home, err := os.UserHomeDir()
    92  		if err != nil {
    93  			return err
    94  		}
    95  		o.User.PrivateKeyPath = filepath.Join(home, ".ssh", "id_rsa")
    96  	}
    97  	if len(o.nodes) == 0 {
    98  		return cfgcore.MakeError("The list of machines where kubernetes is installed must be specified.")
    99  	}
   100  	o.Nodes = make([]types.ClusterNode, len(o.nodes))
   101  	for i, node := range o.nodes {
   102  		fields := strings.SplitN(node, ":", 3)
   103  		if len(fields) < 2 {
   104  			return cfgcore.MakeError("The node format is incorrect, require: [name:address:internalAddress].")
   105  		}
   106  		n := types.ClusterNode{
   107  			Name:            fields[0],
   108  			Address:         fields[1],
   109  			InternalAddress: fields[1],
   110  		}
   111  		if len(fields) == 3 {
   112  			n.InternalAddress = fields[2]
   113  		}
   114  		o.Nodes[i] = n
   115  	}
   116  	return nil
   117  }
   118  
   119  func (o *clusterOptions) Validate() error {
   120  	if o.User.Name == "" {
   121  		return cfgcore.MakeError("user name is empty")
   122  	}
   123  	if o.clusterName == "" {
   124  		return cfgcore.MakeError("kubernetes name is empty")
   125  	}
   126  	if err := validateUser(o); err != nil {
   127  		return err
   128  	}
   129  	if !o.RoleGroup.IsValidate() {
   130  		return cfgcore.MakeError("etcd, master or worker is empty")
   131  	}
   132  	if err := o.checkReplicaNode(o.RoleGroup.ETCD); err != nil {
   133  		return err
   134  	}
   135  	if err := o.checkReplicaNode(o.RoleGroup.Master); err != nil {
   136  		return err
   137  	}
   138  	if err := o.checkReplicaNode(o.RoleGroup.Worker); err != nil {
   139  		return err
   140  	}
   141  	return nil
   142  }
   143  
   144  func (o *clusterOptions) checkReplicaNode(nodes []string) error {
   145  	sets := cfgutil.NewSet()
   146  	for _, node := range nodes {
   147  		if !o.hasNode(node) {
   148  			return cfgcore.MakeError("node %s is not exist!", node)
   149  		}
   150  		if sets.InArray(node) {
   151  			return cfgcore.MakeError("node %s is repeat!", node)
   152  		}
   153  		sets.Add(node)
   154  	}
   155  	return nil
   156  }
   157  
   158  func (o *clusterOptions) hasNode(n string) bool {
   159  	for _, node := range o.Nodes {
   160  		if node.Name == n {
   161  			return true
   162  		}
   163  	}
   164  	return false
   165  }
   166  
   167  func (o *clusterOptions) confirm(promptStr string) (bool, error) {
   168  	const yesStr = "yes"
   169  	const noStr = "no"
   170  
   171  	confirmStr := []string{yesStr, noStr}
   172  	printer.Warning(o.IOStreams.Out, promptStr)
   173  	input, err := prompt.NewPrompt("Please type [yes/No] to confirm:",
   174  		func(input string) error {
   175  			if !slices.Contains(confirmStr, strings.ToLower(input)) {
   176  				return fmt.Errorf("typed \"%s\" does not match \"%s\"", input, confirmStr)
   177  			}
   178  			return nil
   179  		}, o.IOStreams.In).Run()
   180  	if err != nil {
   181  		return false, err
   182  	}
   183  	return strings.ToLower(input) == yesStr, nil
   184  }
   185  
   186  func (o *clusterOptions) fillClusterConfig(configFile string) error {
   187  	_, err := os.Stat(configFile)
   188  	if err != nil {
   189  		return err
   190  	}
   191  	b, err := os.ReadFile(configFile)
   192  	if err != nil {
   193  		return err
   194  	}
   195  
   196  	c, err := builder.BuildResourceFromYaml(o.Cluster, string(b))
   197  	if err != nil {
   198  		return err
   199  	}
   200  	o.Cluster = *c
   201  	o.clusterName = c.Name
   202  	return nil
   203  }
   204  
   205  func checkAndUpdateHomeDir(user *types.ClusterUser) error {
   206  	if !strings.HasPrefix(user.PrivateKeyPath, "~/") {
   207  		return nil
   208  	}
   209  	home, err := os.UserHomeDir()
   210  	if err != nil {
   211  		return err
   212  	}
   213  	user.PrivateKeyPath = filepath.Join(home, user.PrivateKeyPath[2:])
   214  	return nil
   215  }
   216  
   217  func validateUser(o *clusterOptions) error {
   218  	if o.User.Password != "" || o.User.PrivateKey != "" {
   219  		return nil
   220  	}
   221  	if o.User.PrivateKey == "" && o.User.PrivateKeyPath != "" {
   222  		if err := checkAndUpdateHomeDir(&o.User); err != nil {
   223  			return err
   224  		}
   225  		if _, err := os.Stat(o.User.PrivateKeyPath); err != nil {
   226  			return err
   227  		}
   228  		b, err := os.ReadFile(o.User.PrivateKeyPath)
   229  		if err != nil {
   230  			return err
   231  		}
   232  		o.User.PrivateKey = string(b)
   233  	}
   234  	return nil
   235  }
   236  
   237  func syncClusterNodeRole(cluster *kubekeyapiv1alpha2.ClusterSpec, runtime *common.KubeRuntime) {
   238  	hostSet := set.NewLinkedHashSetString()
   239  	for _, role := range cluster.GroupHosts() {
   240  		for _, host := range role {
   241  			if host.IsRole(common.Master) || host.IsRole(common.Worker) {
   242  				host.SetRole(common.K8s)
   243  			}
   244  			if !hostSet.InArray(host.GetName()) {
   245  				hostSet.Add(host.GetName())
   246  				runtime.BaseRuntime.AppendHost(host)
   247  				runtime.BaseRuntime.AppendRoleMap(host)
   248  			}
   249  		}
   250  	}
   251  }