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 }