diff --git a/cmd/cli.go b/cmd/cli.go index fe99c0c7df4d..1740343410c1 100644 --- a/cmd/cli.go +++ b/cmd/cli.go @@ -36,6 +36,8 @@ import ( utilcomp "k8s.io/kubectl/pkg/util/completion" "k8s.io/kubectl/pkg/util/templates" + infras "github.com/apecloud/kubeblocks/internal/cli/cmd/infrastructure" + "github.com/apecloud/kubeblocks/internal/cli/cmd/addon" "github.com/apecloud/kubeblocks/internal/cli/cmd/alert" "github.com/apecloud/kubeblocks/internal/cli/cmd/bench" @@ -183,6 +185,7 @@ A Command Line Interface for KubeBlocks`, fault.NewFaultCmd(f, ioStreams), builder.NewBuilderCmd(f, ioStreams), report.NewReportCmd(f, ioStreams), + infras.NewInfraCmd(ioStreams), ) filters := []string{"options"} diff --git a/cmd/infrastructure/builder/builder.go b/cmd/infrastructure/builder/builder.go new file mode 100644 index 000000000000..203c2a5ebc81 --- /dev/null +++ b/cmd/infrastructure/builder/builder.go @@ -0,0 +1,83 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package builder + +import ( + "bufio" + "embed" + "encoding/json" + "strings" + + "github.com/leaanthony/debme" + "k8s.io/apimachinery/pkg/util/yaml" + + cfgcore "github.com/apecloud/kubeblocks/internal/configuration" + "github.com/apecloud/kubeblocks/internal/gotemplate" +) + +var ( + //go:embed template/* + cueTemplate embed.FS +) + +func newBuildTemplate(templateName string) (string, error) { + tmplFs, _ := debme.FS(cueTemplate, "template") + if tmlBytes, err := tmplFs.ReadFile(templateName); err != nil { + return "", err + } else { + return string(tmlBytes), nil + } +} + +func BuildFromTemplate(values *gotemplate.TplValues, templateName string) (string, error) { + tpl, err := newBuildTemplate(templateName) + if err != nil { + return "", err + } + + engine := gotemplate.NewTplEngine(values, nil, templateName, nil, nil) + rendered, err := engine.Render(tpl) + if err != nil { + return "", err + } + return rendered, nil +} + +func BuildResourceFromYaml[T any](obj T, bYaml string) (*T, error) { + var ret map[string]interface{} + + content, err := yaml.NewYAMLReader(bufio.NewReader(strings.NewReader(bYaml))).Read() + if err != nil { + return nil, cfgcore.WrapError(err, "failed to read the cluster yaml") + } + err = yaml.Unmarshal(content, &ret) + if err != nil { + return nil, cfgcore.WrapError(err, "failed to unmarshal the cluster yaml") + } + + contentToJSON, err := yaml.ToJSON(content) + if err != nil { + return nil, cfgcore.WrapError(err, "Unable to convert configuration to json") + } + if err := json.Unmarshal(contentToJSON, &obj); err != nil { + return nil, cfgcore.WrapError(err, "failed to unmarshal the cluster") + } + return &obj, nil +} diff --git a/cmd/infrastructure/builder/template/containerd.config.toml.tpl b/cmd/infrastructure/builder/template/containerd.config.toml.tpl new file mode 100644 index 000000000000..b4af2c0bf370 --- /dev/null +++ b/cmd/infrastructure/builder/template/containerd.config.toml.tpl @@ -0,0 +1,48 @@ +version = 2 +{{- if .DataRoot }} +root = {{ .DataRoot }} +{{ else }} +root = "/var/lib/containerd" +{{- end }} +state = "/run/containerd" + +[grpc] + address = "/run/containerd/containerd.sock" + uid = 0 + gid = 0 + max_recv_message_size = 16777216 + max_send_message_size = 16777216 + +[debug] + level = "info" + +[metrics] + address = "" + grpc_histogram = false + +[timeouts] + "io.containerd.timeout.shim.cleanup" = "5s" + "io.containerd.timeout.shim.load" = "5s" + "io.containerd.timeout.shim.shutdown" = "3s" + "io.containerd.timeout.task.state" = "2s" + +[plugins."io.containerd.grpc.v1.cri".containerd] +default_runtime_name = "runc" + +[plugins."io.containerd.grpc.v1.cri"] +stream_server_address = "127.0.0.1" +max_container_log_line_size = 262144 +sandbox_image = "{{ .SandBoxImage }}" + +[plugins."io.containerd.grpc.v1.cri".registry] +config_path = "/etc/containerd/certs.d:/etc/docker/certs.d" + +[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc] +runtime_type = "io.containerd.runc.v2" + +[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options] +SystemdCgroup = true + +[plugins."io.containerd.grpc.v1.cri".cni] +bin_dir = "/opt/cni/bin" +conf_dir = "/etc/cni/net.d" diff --git a/cmd/infrastructure/builder/template/containerd.service.tpl b/cmd/infrastructure/builder/template/containerd.service.tpl new file mode 100644 index 000000000000..d5af236df2f3 --- /dev/null +++ b/cmd/infrastructure/builder/template/containerd.service.tpl @@ -0,0 +1,26 @@ +[Unit] +Description=containerd container runtime +Documentation=https://containerd.io +After=network.target local-fs.target + +[Service] +ExecStartPre=-/sbin/modprobe overlay +ExecStart=/usr/bin/containerd + +Type=notify +Delegate=yes +KillMode=process +Restart=always +RestartSec=5 +# Having non-zero Limit*s causes performance problems due to accounting overhead +# in the kernel. We recommend using cgroups to do container-local accounting. +LimitNPROC=infinity +LimitCORE=infinity +LimitNOFILE=infinity +# Comment TasksMax if your systemd version does not supports it. +# Only systemd 226 and above support this version. +TasksMax=infinity +OOMScoreAdjust=-999 + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/cmd/infrastructure/builder/template/crictl.yaml.tpl b/cmd/infrastructure/builder/template/crictl.yaml.tpl new file mode 100644 index 000000000000..236bb88f5fdf --- /dev/null +++ b/cmd/infrastructure/builder/template/crictl.yaml.tpl @@ -0,0 +1,5 @@ +runtime-endpoint: {{ .Endpoint }} +image-endpoint: {{ .Endpoint }} +timeout: 5 +debug: false +pull-image-on-create: false \ No newline at end of file diff --git a/cmd/infrastructure/builder/template/init_os.sh.tpl b/cmd/infrastructure/builder/template/init_os.sh.tpl new file mode 100644 index 000000000000..3a016ae43a85 --- /dev/null +++ b/cmd/infrastructure/builder/template/init_os.sh.tpl @@ -0,0 +1,167 @@ +#!/usr/bin/env bash + +function swap_off() { + echo "swap off..." + swapoff -a + sed -i /^[^#]*swap*/s/^/\#/g /etc/fstab + + # clean cache + echo 3 > /proc/sys/vm/drop_caches + echo +} + +function selinux_off() { + echo "selinux off..." + setenforce 0 + echo "enforce: $(getenforce)" + + if [ -f /etc/selinux/config ]; then + sed -ri 's/SELINUX=enforcing/SELINUX=disabled/' /etc/selinux/config + fi + echo +} + +function firewalld_off() { + echo "firewall off..." + systemctl stop firewalld.service 1>/dev/null 2>&1 + systemctl disable firewalld.service 1>/dev/null 2>&1 + systemctl stop ufw 1>/dev/null 2>&1 + systemctl disable ufw 1>/dev/null 2>&1 + echo +} + +function replace_in_file() { + local filename="${1:?filename is required}" + local match_regex="${2:?match regex is required}" + local substitute_regex="${3:?substitute regex is required}" + local posix_regex=${4:-true} + + local result + local -r del=$'\001' + if [[ $posix_regex = true ]]; then + result="$(sed -E "s${del}${match_regex}${del}${substitute_regex}${del}g" "$filename")" + else + result="$(sed "s${del}${match_regex}${del}${substitute_regex}${del}g" "$filename")" + fi + echo "$result" > "$filename" +} + +function sysctl_set_keyvalue() { + local -r key="${1:?missing key}" + local -r value="${2:?missing value}" + local -r conf_file="${3:-"/etc/sysctl.conf"}" + if grep -qE "^#*\s*${key}" "$conf_file" >/dev/null; then + replace_in_file "$conf_file" "^#*\s*${key}\s*=.*" "${key} = ${value}" + else + echo "${key} = ${value}" >>"$conf_file" + fi +} + +function set_network() { + echo "set network..." + + sysctl_set_keyvalue "net.ipv4.tcp_tw_recycle" "0" + sysctl_set_keyvalue "net.ipv4.ip_forward" "1" + sysctl_set_keyvalue "net.bridge.bridge-nf-call-arptables" "1" + sysctl_set_keyvalue "net.bridge.bridge-nf-call-ip6tables" "1" + sysctl_set_keyvalue "net.bridge.bridge-nf-call-iptables" "1" + # for node port + sysctl_set_keyvalue "net.ipv4.ip_local_reserved_ports" "30000-32767" + + echo +} + +function install_hugepage() { + local -r hugepage="${1:?missing key}" + + echo "install hugepage..." + hpg_sz=$(grep Hugepagesize /proc/meminfo | awk '{print $2}') + ## convert to KB + re_hpg_num=$(echo $hugepage | awk '{sub("GB", "*1024*1024*1024", $1) || sub("MB", "*1024*1024", $1) || sub("KB", "*1024", $1); printf $1 "+"} END {print 0}' | bc) + hpg_num=$(echo "$re_hpg_num / ( $hpg_sz * 1024 )" | bc) + + sysctl_set_keyvalue "vm.nr_hugepages" "$hpg_num" + echo +} + +function common_os_setting() { + swap_off + selinux_off + firewalld_off + set_network +} + +function install_hosts() { + sed -i ':a;$!{N;ba};s@# kubeblocks hosts BEGIN.*# kubeblocks hosts END@@' /etc/hosts + sed -i '/^$/N;/\n$/N;//D' /etc/hosts + + cat >>/etc/hosts< /dev/null 2>&1 + if [ $? -eq 0 ]; then + modprobe br_netfilter + mkdir -p /etc/modules-load.d + echo 'br_netfilter' > /etc/modules-load.d/kubekey-br_netfilter.conf + fi + + modinfo overlay > /dev/null 2>&1 + if [ $? -eq 0 ]; then + modprobe overlay + echo 'overlay' >> /etc/modules-load.d/kubekey-br_netfilter.conf + fi +} + +function install_ipvs() { + modprobe ip_vs + modprobe ip_vs_rr + modprobe ip_vs_wrr + modprobe ip_vs_sh + + cat > /etc/modules-load.d/kube_proxy-ipvs.conf << EOF +ip_vs +ip_vs_rr +ip_vs_wrr +ip_vs_sh +EOF + + modprobe nf_conntrack_ipv4 1>/dev/null 2>/dev/null + if [ $? -eq 0 ]; then + echo 'nf_conntrack_ipv4' > /etc/modules-load.d/kube_proxy-ipvs.conf + else + modprobe nf_conntrack + echo 'nf_conntrack' > /etc/modules-load.d/kube_proxy-ipvs.conf + fi +} + +{{- if $.Options.HugePageFeature }} +install_hugepage {{ $.Options.HugePageFeature.HugePageSize }} +{{- end }} + +install_netfilter +install_ipvs +install_hosts +common_os_setting + +# for es/opensearch +sysctl_set_keyvalue "vm.max_map_count" "262144" +# for kubeblocks +sysctl_set_keyvalue "fs.inotify.max_user_watches" "524288" +sysctl_set_keyvalue "fs.inotify.max_user_instances" "524288" +sysctl -p + +# Make sure the iptables utility doesn't use the nftables backend. +update-alternatives --set iptables /usr/sbin/iptables-legacy >/dev/null 2>&1 || true +update-alternatives --set ip6tables /usr/sbin/ip6tables-legacy >/dev/null 2>&1 || true +update-alternatives --set arptables /usr/sbin/arptables-legacy >/dev/null 2>&1 || true +update-alternatives --set ebtables /usr/sbin/ebtables-legacy >/dev/null 2>&1 || true + +ulimit -u 65535 +ulimit -n 65535 diff --git a/cmd/infrastructure/builder/template/kubekey_cluster.tpl b/cmd/infrastructure/builder/template/kubekey_cluster.tpl new file mode 100644 index 000000000000..193954395178 --- /dev/null +++ b/cmd/infrastructure/builder/template/kubekey_cluster.tpl @@ -0,0 +1,64 @@ +apiVersion: kubekey.kubesphere.io/v1alpha2 +kind: Cluster +metadata: + name: {{ $.Name }} +spec: + hosts: + {{- range $.Hosts }} + - name: {{ .Name }} + address: {{ .Address }} + internalAddress: {{ .InternalAddress }} + user: {{ $.User.Name }} + password: {{ $.User.Password }} + privateKey: {{ $.User.PrivateKey | quote }} + timeout: {{ $.Timeout }} + {{- end }} + roleGroups: + {{- $roles := keys $.RoleGroups }} + {{- range $roles }} + {{- $nodes := get $.RoleGroups . }} + {{ . }}: + {{- range $nodes }} + - {{ . }} + {{- end }} + {{- end }} + controlPlaneEndpoint: + domain: {{ $.Kubernetes.ControlPlaneEndpoint.Domain }} + {{- $address := ""}} + {{- if hasKey $.RoleGroups "master" }} + {{- $mName := index (get $.RoleGroups "master") 0 }} + {{- range $.Hosts }} + {{- if eq .Name $mName }} + {{- $address = .InternalAddress }} + {{- end }} + {{- end }} + {{- end }} + {{- if eq $address "" }} + {{- failed "require control address." }} + {{- end }} + address: {{ $address }} + port: {{ $.Kubernetes.ControlPlaneEndpoint.Port }} + kubernetes: + nodelocaldns: false + dnsDomain: {{ $.Kubernetes.Networking.DNSDomain }} + version: {{ $.Version }} + clusterName: {{ $.Name }} + {{- $criType := "containerd" }} + nodeCidrMaskSize: 24 + proxyMode: {{ $.Kubernetes.ProxyMode }} + containerRuntimeEndpoint: {{ $.Kubernetes.CRI.ContainerRuntimeEndpoint }} + containerManager: {{ $.Kubernetes.CRI.ContainerRuntimeType }} + etcd: + backupDir: /var/backups/kube_etcd + backupPeriod: 1800 + keepBackupNumber: 6 + backupScript: /usr/local/bin/kube-scripts + {{- if hasKey $.RoleGroups "etcd" }} + type: kubekey + {{- else }} + type: kubeadm + {{- end }} + network: + plugin: {{ $.Kubernetes.Networking.Plugin }} + kubePodsCIDR: {{ $.Kubernetes.Networking.PodSubnet }} + kubeServiceCIDR: {{ $.Kubernetes.Networking.ServiceSubnet }} \ No newline at end of file diff --git a/cmd/infrastructure/builder/template_task.go b/cmd/infrastructure/builder/template_task.go new file mode 100644 index 000000000000..eaf7d5d1cc94 --- /dev/null +++ b/cmd/infrastructure/builder/template_task.go @@ -0,0 +1,55 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package builder + +import ( + "path/filepath" + + "github.com/kubesphere/kubekey/v3/cmd/kk/pkg/core/action" + "github.com/kubesphere/kubekey/v3/cmd/kk/pkg/core/connector" + "github.com/kubesphere/kubekey/v3/cmd/kk/pkg/core/util" + + cfgcore "github.com/apecloud/kubeblocks/internal/configuration" + "github.com/apecloud/kubeblocks/internal/gotemplate" +) + +type Template struct { + action.BaseAction + Template string + Dst string + Values gotemplate.TplValues +} + +func (t *Template) Execute(runtime connector.Runtime) error { + templateStr, err := BuildFromTemplate(&t.Values, t.Template) + if err != nil { + return cfgcore.WrapError(err, "failed to render template %s", t.Template) + } + + fileName := filepath.Join(runtime.GetHostWorkDir(), t.Template) + if err := util.WriteFile(fileName, []byte(templateStr)); err != nil { + return cfgcore.WrapError(err, "failed to write file %s", fileName) + } + + if err := runtime.GetRunner().SudoScp(fileName, t.Dst); err != nil { + return cfgcore.WrapError(err, "failed to scp file %s to remote %s", fileName, t.Dst) + } + return nil +} diff --git a/cmd/infrastructure/cluster.go b/cmd/infrastructure/cluster.go new file mode 100644 index 000000000000..90c77690da7e --- /dev/null +++ b/cmd/infrastructure/cluster.go @@ -0,0 +1,251 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package infrastructure + +import ( + "fmt" + "os" + "os/user" + "path/filepath" + "strings" + + "github.com/StudioSol/set" + kubekeyapiv1alpha2 "github.com/kubesphere/kubekey/v3/cmd/kk/apis/kubekey/v1alpha2" + "github.com/kubesphere/kubekey/v3/cmd/kk/pkg/common" + "github.com/spf13/cobra" + "golang.org/x/exp/slices" + "k8s.io/apimachinery/pkg/util/rand" + "k8s.io/cli-runtime/pkg/genericclioptions" + + "github.com/apecloud/kubeblocks/internal/cli/cmd/infrastructure/builder" + "github.com/apecloud/kubeblocks/internal/cli/cmd/infrastructure/types" + "github.com/apecloud/kubeblocks/internal/cli/printer" + "github.com/apecloud/kubeblocks/internal/cli/util/prompt" + cfgcore "github.com/apecloud/kubeblocks/internal/configuration" + cfgutil "github.com/apecloud/kubeblocks/internal/configuration/util" +) + +type clusterOptions struct { + types.Cluster + IOStreams genericclioptions.IOStreams + + clusterConfig string + clusterName string + timeout int64 + nodes []string +} + +func buildCommonFlags(cmd *cobra.Command, o *clusterOptions) { + cmd.Flags().StringVarP(&o.clusterConfig, "config", "c", "", "Specify infra cluster config file. [option]") + cmd.Flags().StringVarP(&o.clusterName, "name", "", "", "Specify kubernetes cluster name") + cmd.Flags().StringSliceVarP(&o.nodes, "nodes", "", nil, "List of machines on which kubernetes is installed. [require]") + + // for user + cmd.Flags().StringVarP(&o.User.Name, "user", "u", "", "Specify the account to access the remote server. [require]") + cmd.Flags().Int64VarP(&o.timeout, "timeout", "t", 30, "Specify the ssh timeout.[option]") + cmd.Flags().StringVarP(&o.User.Password, "password", "p", "", "Specify the password for the account to execute sudo. [option]") + cmd.Flags().StringVarP(&o.User.PrivateKey, "private-key", "", "", "The PrimaryKey for ssh to the remote machine. [option]") + cmd.Flags().StringVarP(&o.User.PrivateKeyPath, "private-key-path", "", "", "Specify the file PrimaryKeyPath of ssh to the remote machine. default ~/.ssh/id_rsa.") + + cmd.Flags().StringSliceVarP(&o.RoleGroup.ETCD, "etcd", "", nil, "Specify etcd nodes") + cmd.Flags().StringSliceVarP(&o.RoleGroup.Master, "master", "", nil, "Specify master nodes") + cmd.Flags().StringSliceVarP(&o.RoleGroup.Worker, "worker", "", nil, "Specify worker nodes") +} + +func (o *clusterOptions) Complete() error { + if o.clusterName == "" && o.clusterConfig == "" { + o.clusterName = "kubeblocks-" + rand.String(6) + fmt.Printf("The cluster name is not set, auto generate cluster name: %s\n", o.clusterName) + } + + if o.clusterConfig != "" { + return o.fillClusterConfig(o.clusterConfig) + } + + if o.User.Name == "" { + currentUser, err := user.Current() + if err != nil { + return err + } + o.User.Name = currentUser.Username + fmt.Printf("The user is not set, use current user %s\n", o.User.Name) + } + if o.User.Password == "" && o.User.PrivateKey == "" && o.User.PrivateKeyPath == "" { + home, err := os.UserHomeDir() + if err != nil { + return err + } + o.User.PrivateKeyPath = filepath.Join(home, ".ssh", "id_rsa") + } + if len(o.nodes) == 0 { + return cfgcore.MakeError("The list of machines where kubernetes is installed must be specified.") + } + o.Nodes = make([]types.ClusterNode, len(o.nodes)) + for i, node := range o.nodes { + fields := strings.SplitN(node, ":", 3) + if len(fields) < 2 { + return cfgcore.MakeError("The node format is incorrect, require: [name:address:internalAddress].") + } + n := types.ClusterNode{ + Name: fields[0], + Address: fields[1], + InternalAddress: fields[1], + } + if len(fields) == 3 { + n.InternalAddress = fields[2] + } + o.Nodes[i] = n + } + return nil +} + +func (o *clusterOptions) Validate() error { + if o.User.Name == "" { + return cfgcore.MakeError("user name is empty") + } + if o.clusterName == "" { + return cfgcore.MakeError("kubernetes name is empty") + } + if err := validateUser(o); err != nil { + return err + } + if !o.RoleGroup.IsValidate() { + return cfgcore.MakeError("etcd, master or worker is empty") + } + if err := o.checkReplicaNode(o.RoleGroup.ETCD); err != nil { + return err + } + if err := o.checkReplicaNode(o.RoleGroup.Master); err != nil { + return err + } + if err := o.checkReplicaNode(o.RoleGroup.Worker); err != nil { + return err + } + return nil +} + +func (o *clusterOptions) checkReplicaNode(nodes []string) error { + sets := cfgutil.NewSet() + for _, node := range nodes { + if !o.hasNode(node) { + return cfgcore.MakeError("node %s is not exist!", node) + } + if sets.InArray(node) { + return cfgcore.MakeError("node %s is repeat!", node) + } + sets.Add(node) + } + return nil +} + +func (o *clusterOptions) hasNode(n string) bool { + for _, node := range o.Nodes { + if node.Name == n { + return true + } + } + return false +} + +func (o *clusterOptions) confirm(promptStr string) (bool, error) { + const yesStr = "yes" + const noStr = "no" + + confirmStr := []string{yesStr, noStr} + printer.Warning(o.IOStreams.Out, promptStr) + input, err := prompt.NewPrompt("Please type [yes/No] to confirm:", + func(input string) error { + if !slices.Contains(confirmStr, strings.ToLower(input)) { + return fmt.Errorf("typed \"%s\" does not match \"%s\"", input, confirmStr) + } + return nil + }, o.IOStreams.In).Run() + if err != nil { + return false, err + } + return strings.ToLower(input) == yesStr, nil +} + +func (o *clusterOptions) fillClusterConfig(configFile string) error { + _, err := os.Stat(configFile) + if err != nil { + return err + } + b, err := os.ReadFile(configFile) + if err != nil { + return err + } + + c, err := builder.BuildResourceFromYaml(o.Cluster, string(b)) + if err != nil { + return err + } + o.Cluster = *c + o.clusterName = c.Name + return nil +} + +func checkAndUpdateHomeDir(user *types.ClusterUser) error { + if !strings.HasPrefix(user.PrivateKeyPath, "~/") { + return nil + } + home, err := os.UserHomeDir() + if err != nil { + return err + } + user.PrivateKeyPath = filepath.Join(home, user.PrivateKeyPath[2:]) + return nil +} + +func validateUser(o *clusterOptions) error { + if o.User.Password != "" || o.User.PrivateKey != "" { + return nil + } + if o.User.PrivateKey == "" && o.User.PrivateKeyPath != "" { + if err := checkAndUpdateHomeDir(&o.User); err != nil { + return err + } + if _, err := os.Stat(o.User.PrivateKeyPath); err != nil { + return err + } + b, err := os.ReadFile(o.User.PrivateKeyPath) + if err != nil { + return err + } + o.User.PrivateKey = string(b) + } + return nil +} + +func syncClusterNodeRole(cluster *kubekeyapiv1alpha2.ClusterSpec, runtime *common.KubeRuntime) { + hostSet := set.NewLinkedHashSetString() + for _, role := range cluster.GroupHosts() { + for _, host := range role { + if host.IsRole(common.Master) || host.IsRole(common.Worker) { + host.SetRole(common.K8s) + } + if !hostSet.InArray(host.GetName()) { + hostSet.Add(host.GetName()) + runtime.BaseRuntime.AppendHost(host) + runtime.BaseRuntime.AppendRoleMap(host) + } + } + } +} diff --git a/cmd/infrastructure/constant/const.go b/cmd/infrastructure/constant/const.go new file mode 100644 index 000000000000..18b3e44bbb5c --- /dev/null +++ b/cmd/infrastructure/constant/const.go @@ -0,0 +1,50 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package constant + +const ( + ContainerdService = "containerd.service.tpl" + ContainerdConfig = "containerd.config.toml.tpl" + CRICtlConfig = "crictl.yaml.tpl" + ConfigureOSScripts = "init_os.sh.tpl" + + ContainerdServiceInstallPath = "/etc/systemd/system/containerd.service" + ContainerdConfigInstallPath = "/etc/containerd/config.toml" + CRICtlConfigInstallPath = "/etc/crictl.yaml" + + DefaultSandBoxImage = "k8s.gcr.io/pause:3.8" + + DefaultK8sVersion = "v1.26.5" // https://github.com/kubernetes/kubernetes/releases/tag/v1.26.5 + DefaultEtcdVersion = "v3.4.26" // https://github.com/etcd-io/etcd/releases/tag/v3.4.26 + DefaultCRICtlVersion = "v1.26.0" // https://github.com/kubernetes-sigs/cri-tools/releases/tag/v1.26.0 + DefaultHelmVersion = "v3.12.0" // https://github.com/helm/helm/releases + DefaultRuncVersion = "v1.1.7" // https://github.com/opencontainers/runc/releases + DefaultCniVersion = "v1.3.0" // https://github.com/containernetworking/plugins/releases + DefaultContainerdVersion = "1.7.2" // https://github.com/containerd/containerd/releases +) + +const ( + DefaultK8sDNSDomain = "cluster.local" + DefaultAPIDNSDomain = "lb.kubeblocks.local" + DefaultK8sProxyMode = "ipvs" + DefaultAPIServerPort = 6443 + + DefaultNetworkPlugin = "cilium" +) diff --git a/cmd/infrastructure/create.go b/cmd/infrastructure/create.go new file mode 100644 index 000000000000..2d432a9148f2 --- /dev/null +++ b/cmd/infrastructure/create.go @@ -0,0 +1,224 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package infrastructure + +import ( + "fmt" + "os" + + "github.com/kubesphere/kubekey/v3/cmd/kk/pkg/common" + "github.com/kubesphere/kubekey/v3/cmd/kk/pkg/core/connector" + "github.com/spf13/cobra" + versionutil "k8s.io/apimachinery/pkg/util/version" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/kubectl/pkg/util/templates" + + "github.com/apecloud/kubeblocks/internal/cli/cmd/infrastructure/constant" + "github.com/apecloud/kubeblocks/internal/cli/cmd/infrastructure/tasks" + "github.com/apecloud/kubeblocks/internal/cli/cmd/infrastructure/types" + "github.com/apecloud/kubeblocks/internal/cli/util" + cfgcore "github.com/apecloud/kubeblocks/internal/configuration" + "github.com/apecloud/kubeblocks/internal/configuration/container" +) + +type createOptions struct { + clusterOptions + version types.InfraVersionInfo + + criType string + debug bool + sandBoxImage string + + securityEnhancement bool + outputKubeconfig string +} + +var createExamples = templates.Examples(` + # Create kubernetes cluster with specified config yaml + kbcli infra create -c cluster.yaml + + # example cluster.yaml + cat cluster.yaml +metadata: + name: kb-k8s-test-cluster +user: + name: user1 + privateKeyPath: ~/.ssh/test.pem +nodes: + - name: kb-infra-node-0 + address: 1.1.1.1 + internalAddress: 10.128.0.19 + - name: kb-infra-node-1 + address: 1.1.1.2 + internalAddress: 10.128.0.20 + - name: kb-infra-node-2 + address: 1.1.1.3 + internalAddress: 10.128.0.21 + options: + hugePageFeature: + hugePageSize: 10GB +roleGroup: + etcd: + - kb-infra-node-0 + - kb-infra-node-1 + - kb-infra-node-2 + master: + - kb-infra-node-0 + worker: + - kb-infra-node-1 + - kb-infra-node-2 + +kubernetes: + containerManager: containerd + # apis/kubeadm/types.Networking + networking: + plugin: cilium + dnsDomain: cluster.local + podSubnet: 10.233.64.0/18 + serviceSubnet: 10.233.0.0/18 + controlPlaneEndpoint: + domain: lb.kubeblocks.local + port: 6443 + cri: + containerRuntimeType: "containerd" + containerRuntimeEndpoint: "unix:///run/containerd/containerd.sock" + sandBoxImage: "k8s.gcr.io/pause:3.8" +addons: + - name: openebs + namespace: kube-blocks + sources: + chart: + name: openebs + version: 3.7.0 + repo: https://openebs.github.io/charts + options: + values: + - "localprovisioner.basePath=/mnt/disks" + - "localprovisioner.hostpathClass.isDefaultClass=true" +`) + +func (o *createOptions) Run() error { + const minKubernetesVersion = "v1.24.0" + + v, err := versionutil.ParseSemantic(o.version.KubernetesVersion) + if err != nil { + return err + } + c, err := v.Compare(minKubernetesVersion) + if err != nil { + return err + } + if c < 0 { + return cfgcore.MakeError("kubernetes version must be greater than %s", minKubernetesVersion) + } + + o.Cluster.Kubernetes.AutoDefaultFill() + o.version = o.Version + o.checkAndSetDefaultVersion() + cluster, err := createClusterWithOptions(buildTemplateParams(o)) + if err != nil { + return err + } + + yes, err := o.confirm(fmt.Sprintf("install kubernetes using version: %v", o.version.KubernetesVersion)) + if err != nil { + return err + } + if !yes { + return nil + } + + runtime := &common.KubeRuntime{ + BaseRuntime: connector.NewBaseRuntime(o.clusterName, connector.NewDialer(), o.debug, false), + Cluster: cluster, + ClusterName: o.clusterName, + } + + syncClusterNodeRole(cluster, runtime) + checkAndUpdateZone() + pipelineRunner := tasks.NewPipelineRunner("CreateCluster", NewCreatePipeline(o), runtime) + if err := pipelineRunner.Do(o.IOStreams.Out); err != nil { + return err + } + fmt.Fprintf(o.IOStreams.Out, "Kubernetes Installation is complete.\n\n") + return nil +} + +func checkAndUpdateZone() { + const ZoneName = "KKZONE" + if location, _ := util.GetIPLocation(); location == "CN" { + os.Setenv(ZoneName, "cn") + } + fmt.Printf("current zone: %s\n", os.Getenv(ZoneName)) +} + +func NewCreateKubernetesCmd(streams genericclioptions.IOStreams) *cobra.Command { + o := &createOptions{ + clusterOptions: clusterOptions{ + IOStreams: streams, + }} + o.checkAndSetDefaultVersion() + cmd := &cobra.Command{ + Use: "create", + Short: "create kubernetes cluster.", + Example: createExamples, + Run: func(cmd *cobra.Command, args []string) { + util.CheckErr(o.Complete()) + util.CheckErr(o.Validate()) + util.CheckErr(o.Run()) + }, + } + o.buildCreateInfraFlags(cmd) + o.Version = o.version + return cmd +} + +func (o *createOptions) buildCreateInfraFlags(cmd *cobra.Command) { + buildCommonFlags(cmd, &o.clusterOptions) + cmd.Flags().StringVarP(&o.version.KubernetesVersion, "version", "", o.version.KubernetesVersion, fmt.Sprintf("Specify install kubernetes version. default version is %s", o.version.KubernetesVersion)) + cmd.Flags().StringVarP(&o.sandBoxImage, "sandbox-image", "", constant.DefaultSandBoxImage, "Specified sandbox-image will not be used by the cri. [option]") + cmd.Flags().StringVarP(&o.criType, "container-runtime", "", string(container.ContainerdType), "Specify kubernetes container runtime. default is containerd") + cmd.Flags().BoolVarP(&o.debug, "debug", "", false, "set debug mode") + cmd.Flags().StringVarP(&o.outputKubeconfig, "output-kubeconfig", "", tasks.GetDefaultConfig(), "Specified output kubeconfig. [option]") +} + +func (o *createOptions) checkAndSetDefaultVersion() { + if o.version.KubernetesVersion == "" { + o.version.KubernetesVersion = constant.DefaultK8sVersion + } + if o.version.EtcdVersion == "" { + o.version.EtcdVersion = constant.DefaultEtcdVersion + } + if o.version.ContainerVersion == "" { + o.version.ContainerVersion = constant.DefaultContainerdVersion + } + if o.version.HelmVersion == "" { + o.version.HelmVersion = constant.DefaultHelmVersion + } + if o.version.CRICtlVersion == "" { + o.version.CRICtlVersion = constant.DefaultCRICtlVersion + } + if o.version.CniVersion == "" { + o.version.CniVersion = constant.DefaultCniVersion + } + if o.version.RuncVersion == "" { + o.version.RuncVersion = constant.DefaultRuncVersion + } +} diff --git a/cmd/infrastructure/create_test.go b/cmd/infrastructure/create_test.go new file mode 100644 index 000000000000..85593f929bba --- /dev/null +++ b/cmd/infrastructure/create_test.go @@ -0,0 +1,100 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package infrastructure + +import ( + "os" + "path/filepath" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "k8s.io/cli-runtime/pkg/genericclioptions" + cmdtesting "k8s.io/kubectl/pkg/cmd/testing" + + "github.com/apecloud/kubeblocks/internal/cli/testing" + "github.com/apecloud/kubeblocks/test/testdata" +) + +var _ = Describe("infra create test", func() { + + var ( + tf *cmdtesting.TestFactory + streams genericclioptions.IOStreams + ) + + BeforeEach(func() { + streams, _, _, _ = genericclioptions.NewTestIOStreams() + tf = cmdtesting.NewTestFactory().WithNamespace(testing.Namespace) + }) + + AfterEach(func() { + tf.Cleanup() + }) + + mockPrivateKeyFile := func(tmpDir string) string { + privateKeyFile := filepath.Join(tmpDir, "id_rsa.pem") + Expect(os.WriteFile(privateKeyFile, []byte("private key"), os.ModePerm)).Should(Succeed()) + return privateKeyFile + } + + It("test create k8s cluster with config file", func() { + tmpDir, _ := os.MkdirTemp(os.TempDir(), "test-") + defer os.RemoveAll(tmpDir) + + By("Create cluster with config file") + o := &createOptions{ + clusterOptions: clusterOptions{ + IOStreams: streams, + }} + o.checkAndSetDefaultVersion() + o.clusterConfig = testdata.SubTestDataPath("infrastructure/infra-cluster.yaml") + Expect(o.Complete()).To(Succeed()) + o.Cluster.User.PrivateKeyPath = mockPrivateKeyFile(tmpDir) + Expect(o.Validate()).To(Succeed()) + }) + + It("test create k8s cluster with params", func() { + tmpDir, _ := os.MkdirTemp(os.TempDir(), "test-") + defer os.RemoveAll(tmpDir) + + By("Create cluster with config file") + o := &createOptions{ + clusterOptions: clusterOptions{ + IOStreams: streams, + }} + o.checkAndSetDefaultVersion() + + o.nodes = []string{ + "node0:1.1.1.1:10.128.0.1", + "node1:1.1.1.2:10.128.0.2", + "node2:1.1.1.3:10.128.0.3", + } + o.Cluster.User.PrivateKeyPath = mockPrivateKeyFile(tmpDir) + Expect(o.Complete()).Should(Succeed()) + Expect(o.Validate()).ShouldNot(Succeed()) + + o.Cluster.RoleGroup.Master = []string{"node0"} + o.Cluster.RoleGroup.ETCD = []string{"node0"} + o.Cluster.RoleGroup.Worker = []string{"node1", "node2"} + Expect(o.Validate()).Should(Succeed()) + }) + +}) diff --git a/cmd/infrastructure/delete.go b/cmd/infrastructure/delete.go new file mode 100644 index 000000000000..2c0f27c6cd0b --- /dev/null +++ b/cmd/infrastructure/delete.go @@ -0,0 +1,117 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package infrastructure + +import ( + "fmt" + + "github.com/apecloud/kubeblocks/internal/gotemplate" + "github.com/kubesphere/kubekey/v3/cmd/kk/pkg/common" + "github.com/kubesphere/kubekey/v3/cmd/kk/pkg/core/connector" + "github.com/kubesphere/kubekey/v3/cmd/kk/pkg/core/pipeline" + "github.com/spf13/cobra" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/kubectl/pkg/util/templates" + + "github.com/apecloud/kubeblocks/internal/cli/util" +) + +type deleteOptions struct { + clusterOptions + + deleteCRI bool + debug bool +} + +var deleteExamples = templates.Examples(` + # delete kubernetes cluster with specified config yaml + kbcli infra delete -c cluster.yaml +`) + +func (o *deleteOptions) Run() error { + o.Cluster.Kubernetes.AutoDefaultFill() + cluster, err := createClusterWithOptions(&gotemplate.TplValues{ + builtinClusterNameObject: o.clusterName, + builtinClusterVersionObject: "0.0.0", + builtinUserObject: o.User, + builtinHostsObject: o.Nodes, + builtinTimeoutObject: o.timeout, + builtinKubernetesObject: o.Cluster.Kubernetes, + builtinRoleGroupsObject: gotemplate.TplValues{ + common.ETCD: o.RoleGroup.ETCD, + common.Master: o.RoleGroup.Master, + common.Worker: o.RoleGroup.Worker, + }, + }) + if err != nil { + return err + } + + yes, err := o.confirm(fmt.Sprintf("delete kubernetes: %s", o.clusterName)) + if err != nil { + return err + } + if !yes { + return nil + } + + runtime := &common.KubeRuntime{ + BaseRuntime: connector.NewBaseRuntime(o.clusterName, connector.NewDialer(), o.debug, false), + Cluster: cluster, + ClusterName: o.clusterName, + } + syncClusterNodeRole(cluster, runtime) + + pipeline := pipeline.Pipeline{ + Name: "DeleteCluster", + Modules: NewDeletePipeline(o), + Runtime: runtime, + } + if err := pipeline.Start(); err != nil { + return err + } + fmt.Fprintf(o.IOStreams.Out, "Kubernetes deletion is complete.\n\n") + return nil +} + +func (o *deleteOptions) buildDeleteInfraFlags(cmd *cobra.Command) { + buildCommonFlags(cmd, &o.clusterOptions) + cmd.Flags().BoolVarP(&o.debug, "debug", "", false, "set debug mode") + cmd.Flags().BoolVarP(&o.deleteCRI, "delete-cri", "", false, "delete cri") +} + +func NewDeleteKubernetesCmd(streams genericclioptions.IOStreams) *cobra.Command { + o := &deleteOptions{ + clusterOptions: clusterOptions{ + IOStreams: streams, + }} + cmd := &cobra.Command{ + Use: "delete", + Short: "delete kubernetes cluster.", + Example: deleteExamples, + Run: func(cmd *cobra.Command, args []string) { + util.CheckErr(o.Complete()) + util.CheckErr(o.Validate()) + util.CheckErr(o.Run()) + }, + } + o.buildDeleteInfraFlags(cmd) + return cmd +} diff --git a/cmd/infrastructure/infras.go b/cmd/infrastructure/infras.go new file mode 100644 index 000000000000..f47465f4ee37 --- /dev/null +++ b/cmd/infrastructure/infras.go @@ -0,0 +1,36 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package infrastructure + +import ( + "github.com/spf13/cobra" + "k8s.io/cli-runtime/pkg/genericclioptions" +) + +// NewInfraCmd for builder functions +func NewInfraCmd(streams genericclioptions.IOStreams) *cobra.Command { + cmd := &cobra.Command{ + Use: "infra", + Short: "infra command", + } + cmd.AddCommand(NewCreateKubernetesCmd(streams)) + cmd.AddCommand(NewDeleteKubernetesCmd(streams)) + return cmd +} diff --git a/cmd/infrastructure/infras_test.go b/cmd/infrastructure/infras_test.go new file mode 100644 index 000000000000..e7307b7e189a --- /dev/null +++ b/cmd/infrastructure/infras_test.go @@ -0,0 +1,43 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package infrastructure + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "k8s.io/cli-runtime/pkg/genericclioptions" +) + +var _ = Describe("kubeblock infra", func() { + var streams genericclioptions.IOStreams + + BeforeEach(func() { + streams, _, _, _ = genericclioptions.NewTestIOStreams() + }) + + AfterEach(func() { + }) + + It("command should succeed", func() { + cmd := NewInfraCmd(streams) + Expect(cmd).ShouldNot(BeNil()) + }) +}) diff --git a/cmd/infrastructure/loader_adapter.go b/cmd/infrastructure/loader_adapter.go new file mode 100644 index 000000000000..fe2149106b00 --- /dev/null +++ b/cmd/infrastructure/loader_adapter.go @@ -0,0 +1,72 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package infrastructure + +import ( + kubekeyapiv1alpha2 "github.com/kubesphere/kubekey/v3/cmd/kk/apis/kubekey/v1alpha2" + "github.com/kubesphere/kubekey/v3/cmd/kk/pkg/common" + + "github.com/apecloud/kubeblocks/internal/cli/cmd/infrastructure/builder" + "github.com/apecloud/kubeblocks/internal/gotemplate" +) + +var ReplicaSetSignature = func(_ kubekeyapiv1alpha2.Cluster, _ any) {} + +func createClusterWithOptions(values *gotemplate.TplValues) (*kubekeyapiv1alpha2.ClusterSpec, error) { + const tplFile = "kubekey_cluster.tpl" + rendered, err := builder.BuildFromTemplate(values, tplFile) + if err != nil { + return nil, err + } + + cluster, err := builder.BuildResourceFromYaml(kubekeyapiv1alpha2.Cluster{}, rendered) + if err != nil { + return nil, err + } + return &cluster.Spec, nil +} + +const ( + builtinClusterNameObject = "Name" + builtinTimeoutObject = "Timeout" + builtinClusterVersionObject = "Version" + builtinCRITypeObject = "CRIType" + builtinUserObject = "User" + builtinHostsObject = "Hosts" + builtinRoleGroupsObject = "RoleGroups" + builtinKubernetesObject = "Kubernetes" +) + +func buildTemplateParams(o *createOptions) *gotemplate.TplValues { + return &gotemplate.TplValues{ + builtinClusterNameObject: o.clusterName, + builtinClusterVersionObject: o.version.KubernetesVersion, + builtinCRITypeObject: o.criType, + builtinUserObject: o.User, + builtinHostsObject: o.Nodes, + builtinTimeoutObject: o.timeout, + builtinKubernetesObject: o.Cluster.Kubernetes, + builtinRoleGroupsObject: gotemplate.TplValues{ + common.ETCD: o.RoleGroup.ETCD, + common.Master: o.RoleGroup.Master, + common.Worker: o.RoleGroup.Worker, + }, + } +} diff --git a/cmd/infrastructure/loader_adapter_test.go b/cmd/infrastructure/loader_adapter_test.go new file mode 100644 index 000000000000..144d12659502 --- /dev/null +++ b/cmd/infrastructure/loader_adapter_test.go @@ -0,0 +1,82 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package infrastructure + +import ( + "testing" + + "github.com/apecloud/kubeblocks/internal/configuration/container" + kubekeyapiv1alpha2 "github.com/kubesphere/kubekey/v3/cmd/kk/apis/kubekey/v1alpha2" + + "github.com/apecloud/kubeblocks/internal/cli/cmd/infrastructure/types" +) + +func TestCreateClusterWithOptions(t *testing.T) { + tests := []struct { + name string + args *createOptions + want *kubekeyapiv1alpha2.ClusterSpec + wantErr bool + }{{ + name: "generateClusterTest", + args: &createOptions{ + version: types.InfraVersionInfo{}, + criType: string(container.ContainerdType), + clusterOptions: clusterOptions{ + clusterName: "for_test", + Cluster: types.Cluster{ + Nodes: []types.ClusterNode{ + { + Name: "node1", + Address: "127.0.0.1", + InternalAddress: "127.0.0.1", + }, { + Name: "node2", + Address: "127.0.0.2", + InternalAddress: "127.0.0.2", + }, + }, + User: types.ClusterUser{ + Name: "test", + Password: "test", + }, + RoleGroup: types.RoleGroup{ + ETCD: []string{"node1"}, + Master: []string{"node1"}, + Worker: []string{"node2"}, + }, + }, + }, + }, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.args.Cluster.Kubernetes.AutoDefaultFill() + got, err := createClusterWithOptions(buildTemplateParams(tt.args)) + if (err != nil) != tt.wantErr { + t.Errorf("createClusterWithOptions() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got == nil { + t.Errorf("createClusterWithOptions() got = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/cmd/infrastructure/pipeline.go b/cmd/infrastructure/pipeline.go new file mode 100644 index 000000000000..810391e5e6bf --- /dev/null +++ b/cmd/infrastructure/pipeline.go @@ -0,0 +1,74 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package infrastructure + +import ( + "github.com/kubesphere/kubekey/v3/cmd/kk/pkg/bootstrap/os" + "github.com/kubesphere/kubekey/v3/cmd/kk/pkg/bootstrap/precheck" + "github.com/kubesphere/kubekey/v3/cmd/kk/pkg/certs" + "github.com/kubesphere/kubekey/v3/cmd/kk/pkg/container" + "github.com/kubesphere/kubekey/v3/cmd/kk/pkg/core/module" + "github.com/kubesphere/kubekey/v3/cmd/kk/pkg/etcd" + "github.com/kubesphere/kubekey/v3/cmd/kk/pkg/filesystem" + "github.com/kubesphere/kubekey/v3/cmd/kk/pkg/kubernetes" + "github.com/kubesphere/kubekey/v3/cmd/kk/pkg/plugins/dns" + "github.com/kubesphere/kubekey/v3/cmd/kk/pkg/plugins/network" + + "github.com/apecloud/kubeblocks/internal/cli/cmd/infrastructure/tasks" +) + +func NewCreatePipeline(o *createOptions) []module.Module { + return []module.Module{ + &precheck.GreetingsModule{}, + &tasks.CheckNodeArchitectureModule{}, + &precheck.NodePreCheckModule{}, + &tasks.InstallDependenciesModule{}, + &tasks.PrepareK8sBinariesModule{BinaryVersion: o.version}, + &tasks.ConfigureNodeOSModule{Nodes: o.Nodes}, + &kubernetes.StatusModule{}, + &tasks.InstallCRIModule{SandBoxImage: o.Cluster.Kubernetes.CRI.SandBoxImage}, + &etcd.PreCheckModule{}, + &etcd.CertsModule{}, + &etcd.InstallETCDBinaryModule{}, + &etcd.ConfigureModule{}, + &etcd.BackupModule{}, + &kubernetes.InstallKubeBinariesModule{}, + &kubernetes.InitKubernetesModule{}, + &dns.ClusterDNSModule{}, + &kubernetes.StatusModule{}, + &tasks.SaveKubeConfigModule{OutputKubeconfig: o.outputKubeconfig}, + &kubernetes.JoinNodesModule{}, + &network.DeployNetworkPluginModule{}, + &kubernetes.ConfigureKubernetesModule{}, + &filesystem.ChownModule{}, + &kubernetes.SecurityEnhancementModule{Skip: !o.securityEnhancement}, + &tasks.AddonsInstaller{Addons: o.Addons, Kubeconfig: o.outputKubeconfig}, + } +} + +func NewDeletePipeline(o *deleteOptions) []module.Module { + return []module.Module{ + &precheck.GreetingsModule{}, + &kubernetes.ResetClusterModule{}, + &container.UninstallContainerModule{Skip: !o.deleteCRI}, + &os.ClearOSEnvironmentModule{}, + &certs.UninstallAutoRenewCertsModule{}, + } +} diff --git a/cmd/infrastructure/suit_test.go b/cmd/infrastructure/suit_test.go new file mode 100644 index 000000000000..10f1ace770bd --- /dev/null +++ b/cmd/infrastructure/suit_test.go @@ -0,0 +1,32 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package infrastructure + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestAppp(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Infra Cmd Test Suite") +} diff --git a/cmd/infrastructure/tasks/addon.go b/cmd/infrastructure/tasks/addon.go new file mode 100644 index 000000000000..6b7c1bcbbb0a --- /dev/null +++ b/cmd/infrastructure/tasks/addon.go @@ -0,0 +1,73 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package tasks + +import ( + cfgcore "github.com/apecloud/kubeblocks/internal/configuration" + "github.com/kubesphere/kubekey/v3/cmd/kk/pkg/common" + "github.com/kubesphere/kubekey/v3/cmd/kk/pkg/core/connector" + "github.com/kubesphere/kubekey/v3/cmd/kk/pkg/core/task" + + "github.com/apecloud/kubeblocks/internal/cli/cmd/infrastructure/types" + "github.com/apecloud/kubeblocks/internal/cli/cmd/infrastructure/utils" +) + +type AddonsInstaller struct { + common.KubeModule + + Addons []types.PluginMeta + Kubeconfig string +} + +type KBAddonsInstall struct { + common.KubeAction + + Addons []types.PluginMeta + Kubeconfig string +} + +func (a *AddonsInstaller) Init() { + a.Name = "AddonsInstaller" + a.Desc = "Install helm addons" + a.Tasks = []task.Interface{ + &task.LocalTask{ + Name: "AddonsInstaller", + Desc: "Install helm addons", + Action: &KBAddonsInstall{Addons: a.Addons, Kubeconfig: a.Kubeconfig}, + }} +} + +func (i *KBAddonsInstall) Execute(runtime connector.Runtime) error { + var installer utils.Installer + for _, addon := range i.Addons { + switch { + case addon.Sources.Chart != nil: + installer = utils.NewHelmInstaller(*addon.Sources.Chart, i.Kubeconfig) + case addon.Sources.Yaml != nil: + installer = utils.NewYamlInstaller(*addon.Sources.Yaml, i.Kubeconfig) + default: + return cfgcore.MakeError("addon source not supported: addon: %v", addon) + } + if err := installer.Install(addon.Name, addon.Namespace); err != nil { + return err + } + } + return nil +} diff --git a/cmd/infrastructure/tasks/dependent.go b/cmd/infrastructure/tasks/dependent.go new file mode 100644 index 000000000000..30204fa604d0 --- /dev/null +++ b/cmd/infrastructure/tasks/dependent.go @@ -0,0 +1,217 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package tasks + +import ( + "github.com/kubesphere/kubekey/v3/cmd/kk/pkg/bootstrap/confirm" + "github.com/kubesphere/kubekey/v3/cmd/kk/pkg/bootstrap/os" + "github.com/kubesphere/kubekey/v3/cmd/kk/pkg/common" + "github.com/kubesphere/kubekey/v3/cmd/kk/pkg/container" + "github.com/kubesphere/kubekey/v3/cmd/kk/pkg/container/templates" + "github.com/kubesphere/kubekey/v3/cmd/kk/pkg/core/connector" + "github.com/kubesphere/kubekey/v3/cmd/kk/pkg/core/logger" + "github.com/kubesphere/kubekey/v3/cmd/kk/pkg/core/prepare" + "github.com/kubesphere/kubekey/v3/cmd/kk/pkg/core/task" + "github.com/kubesphere/kubekey/v3/cmd/kk/pkg/kubernetes" + "github.com/mitchellh/mapstructure" + + "github.com/apecloud/kubeblocks/internal/cli/cmd/infrastructure/builder" + "github.com/apecloud/kubeblocks/internal/cli/cmd/infrastructure/constant" + "github.com/apecloud/kubeblocks/internal/gotemplate" +) + +type InstallDependenciesModule struct { + common.KubeModule +} + +func (i *InstallDependenciesModule) isNotReadyHosts() []connector.Host { + hosts := make([]connector.Host, 0) + for _, host := range i.Runtime.GetAllHosts() { + var result confirm.PreCheckResults + if v, ok := host.GetCache().Get(common.NodePreCheck); ok { + _ = mapstructure.Decode(v, &result) + if result.Socat == "" || + result.Conntrack == "" || + result.Curl == "" || + result.Ipset == "" || + result.Chronyd == "" || + result.Ipvsadm == "" || + result.Ebtables == "" { + hosts = append(hosts, host) + } + } + } + return hosts +} + +// Init install dependencies module +func (i *InstallDependenciesModule) Init() { + i.Name = "InstallDependenciesModule" + i.Desc = "install dependencies" + + hosts := i.isNotReadyHosts() + if len(hosts) == 0 { + logger.Log.Info("All hosts are ready, skip install dependencies") + return + } + + i.Tasks = []task.Interface{ + &task.RemoteTask{ + Name: "GetOSData", + Desc: "Get OS release", + Hosts: i.Runtime.GetAllHosts(), + Action: new(os.GetOSData), + Parallel: true, + }, + &task.RemoteTask{ + Name: "InstallDependenciesModule", + Desc: "check and install dependencies", + Hosts: hosts, + Action: new(InstallDependenciesTask), + Parallel: true, + }, + } +} + +type CheckNodeArchitectureModule struct { + common.KubeModule +} + +// Init install dependencies module +func (i *CheckNodeArchitectureModule) Init() { + i.Name = "CheckNodeArch" + i.Desc = "check and update host arch" + i.Tasks = []task.Interface{ + &task.RemoteTask{ + Name: "CheckNodeArch", + Desc: "check and update node arch", + Hosts: i.Runtime.GetAllHosts(), + Action: new(UpdateNodeTask), + Parallel: true, + }, + } +} + +type InstallCRIModule struct { + common.KubeModule + SandBoxImage string +} + +func (i *InstallCRIModule) Init() { + i.Name = "InstallContainerModule" + i.Desc = "Install container manager" + + syncContainerd := &task.RemoteTask{ + Name: "SyncContainerd", + Desc: "Sync containerd binaries", + Hosts: i.Runtime.GetHostsByRole(common.K8s), + Prepare: &prepare.PrepareCollection{ + &kubernetes.NodeInCluster{Not: true}, + &container.ContainerdExist{Not: true}, + }, + Action: new(container.SyncContainerd), + Parallel: true, + Retry: 2, + } + + syncCrictlBinaries := &task.RemoteTask{ + Name: "SyncCrictlBinaries", + Desc: "Sync crictl binaries", + Hosts: i.Runtime.GetHostsByRole(common.K8s), + Prepare: &prepare.PrepareCollection{ + &kubernetes.NodeInCluster{Not: true}, + &container.CrictlExist{Not: true}, + }, + Action: new(container.SyncCrictlBinaries), + Parallel: true, + Retry: 2, + } + + generateContainerdService := &task.RemoteTask{ + Name: "GenerateContainerdService", + Desc: "Generate containerd service", + Hosts: i.Runtime.GetHostsByRole(common.K8s), + Prepare: &prepare.PrepareCollection{ + &kubernetes.NodeInCluster{Not: true}, + &container.ContainerdExist{Not: true}, + }, + Action: &builder.Template{ + Template: constant.ContainerdService, + Dst: constant.ContainerdServiceInstallPath, + }, + Parallel: true, + } + + generateContainerdConfig := &task.RemoteTask{ + Name: "GenerateContainerdConfig", + Desc: "Generate containerd config", + Hosts: i.Runtime.GetHostsByRole(common.K8s), + Prepare: &prepare.PrepareCollection{ + &kubernetes.NodeInCluster{Not: true}, + &container.ContainerdExist{Not: true}, + }, + Action: &builder.Template{ + Template: constant.ContainerdConfig, + Dst: constant.ContainerdConfigInstallPath, + Values: gotemplate.TplValues{ + "SandBoxImage": i.SandBoxImage, + "DataRoot": templates.DataRoot(i.KubeConf), + }}, + Parallel: true, + } + + generateCrictlConfig := &task.RemoteTask{ + Name: "GenerateCrictlConfig", + Desc: "Generate crictl config", + Hosts: i.Runtime.GetHostsByRole(common.K8s), + Prepare: &prepare.PrepareCollection{ + &kubernetes.NodeInCluster{Not: true}, + &container.ContainerdExist{Not: true}, + }, + Action: &builder.Template{ + Template: constant.CRICtlConfig, + Dst: constant.CRICtlConfigInstallPath, + Values: gotemplate.TplValues{ + "Endpoint": i.KubeConf.Cluster.Kubernetes.ContainerRuntimeEndpoint, + }}, + Parallel: true, + } + + enableContainerd := &task.RemoteTask{ + Name: "EnableContainerd", + Desc: "Enable containerd", + Hosts: i.Runtime.GetHostsByRole(common.K8s), + Prepare: &prepare.PrepareCollection{ + &kubernetes.NodeInCluster{Not: true}, + &container.ContainerdExist{Not: true}, + }, + Action: new(container.EnableContainerd), + Parallel: true, + } + + i.Tasks = []task.Interface{ + syncContainerd, + syncCrictlBinaries, + generateContainerdService, + generateContainerdConfig, + generateCrictlConfig, + enableContainerd, + } +} diff --git a/cmd/infrastructure/tasks/download.go b/cmd/infrastructure/tasks/download.go new file mode 100644 index 000000000000..83b6cc8aada6 --- /dev/null +++ b/cmd/infrastructure/tasks/download.go @@ -0,0 +1,98 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package tasks + +import ( + "fmt" + "os" + "os/exec" + + "github.com/kubesphere/kubekey/v3/cmd/kk/pkg/common" + "github.com/kubesphere/kubekey/v3/cmd/kk/pkg/core/logger" + "github.com/kubesphere/kubekey/v3/cmd/kk/pkg/core/util" + "github.com/kubesphere/kubekey/v3/cmd/kk/pkg/files" + + "github.com/apecloud/kubeblocks/internal/cli/cmd/infrastructure/types" + kbutils "github.com/apecloud/kubeblocks/internal/cli/cmd/infrastructure/utils" + cfgcore "github.com/apecloud/kubeblocks/internal/configuration" +) + +const ( + CurlDownloadURLFormat = "curl -L -o %s %s" + WgetDownloadURLFormat = "wget -O %s %s" + + defaultDownloadURL = CurlDownloadURLFormat +) + +func downloadKubernetesBinaryWithArch(downloadPath string, arch string, binaryVersion types.InfraVersionInfo) (map[string]*files.KubeBinary, error) { + downloadCommand := func(path, url string) string { + return fmt.Sprintf(defaultDownloadURL, path, url) + } + + binaries := []*files.KubeBinary{ + files.NewKubeBinary("etcd", arch, binaryVersion.EtcdVersion, downloadPath, downloadCommand), + files.NewKubeBinary("kubeadm", arch, binaryVersion.KubernetesVersion, downloadPath, downloadCommand), + files.NewKubeBinary("kubelet", arch, binaryVersion.KubernetesVersion, downloadPath, downloadCommand), + files.NewKubeBinary("kubectl", arch, binaryVersion.KubernetesVersion, downloadPath, downloadCommand), + files.NewKubeBinary("kubecni", arch, binaryVersion.CniVersion, downloadPath, downloadCommand), + files.NewKubeBinary("helm", arch, binaryVersion.HelmVersion, downloadPath, downloadCommand), + // for containerd + files.NewKubeBinary("crictl", arch, binaryVersion.CRICtlVersion, downloadPath, downloadCommand), + files.NewKubeBinary("containerd", arch, binaryVersion.ContainerVersion, downloadPath, downloadCommand), + files.NewKubeBinary("runc", arch, binaryVersion.RuncVersion, downloadPath, downloadCommand), + } + + binariesMap := make(map[string]*files.KubeBinary) + for _, binary := range binaries { + if err := binary.CreateBaseDir(); err != nil { + return nil, cfgcore.WrapError(err, "failed to create file %s base dir.", binary.FileName) + } + logger.Log.Messagef(common.LocalHost, "downloading %s %s %s ...", arch, binary.ID, binary.Version) + binariesMap[binary.ID] = binary + if checkDownloadBinary(binary) { + continue + } + if err := download(binary); err != nil { + return nil, cfgcore.WrapError(err, "failed to download %s binary: %s", binary.ID, binary.GetCmd()) + } + } + return binariesMap, nil +} + +func checkDownloadBinary(binary *files.KubeBinary) bool { + if !util.IsExist(binary.Path()) { + return false + } + err := kbutils.CheckSha256sum(binary) + if err != nil { + logger.Log.Messagef(common.LocalHost, "failed to check %s sha256, error: %v", binary.ID, err) + _ = os.Remove(binary.Path()) + return false + } + logger.Log.Messagef(common.LocalHost, "%s is existed", binary.ID) + return true +} + +func download(binary *files.KubeBinary) error { + if err := kbutils.RunCommand(exec.Command("/bin/sh", "-c", binary.GetCmd())); err != nil { + return err + } + return kbutils.WriteSha256sum(binary) +} diff --git a/cmd/infrastructure/tasks/kubeconfig.go b/cmd/infrastructure/tasks/kubeconfig.go new file mode 100644 index 000000000000..2776c6bd0527 --- /dev/null +++ b/cmd/infrastructure/tasks/kubeconfig.go @@ -0,0 +1,89 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package tasks + +import ( + "fmt" + "os" + "time" + + "github.com/apecloud/kubeblocks/internal/cli/util" + cfgcore "github.com/apecloud/kubeblocks/internal/configuration" + "k8s.io/client-go/tools/clientcmd" + clientcmdapi "k8s.io/client-go/tools/clientcmd/api" +) + +func kubeconfigMerge(newKubeConfig *clientcmdapi.Config, existingKubeConfig *clientcmdapi.Config, outPath string) error { + // merge clusters + for k, v := range newKubeConfig.Clusters { + if _, ok := existingKubeConfig.Clusters[k]; ok { + return cfgcore.MakeError("Cluster '%s' already exists in target KubeConfig", k) + } + existingKubeConfig.Clusters[k] = v + } + + // merge auth + for k, v := range newKubeConfig.AuthInfos { + if _, ok := existingKubeConfig.AuthInfos[k]; ok { + return cfgcore.MakeError("AuthInfo '%s' already exists in target KubeConfig", k) + } + existingKubeConfig.AuthInfos[k] = v + } + + // merge contexts + for k, v := range newKubeConfig.Contexts { + if _, ok := existingKubeConfig.Contexts[k]; ok { + return cfgcore.MakeError("Context '%s' already exists in target KubeConfig", k) + } + existingKubeConfig.Contexts[k] = v + } + + existingKubeConfig.CurrentContext = newKubeConfig.CurrentContext + return kubeconfigWrite(existingKubeConfig, outPath) +} + +func kubeconfigWrite(config *clientcmdapi.Config, path string) error { + tempPath := fmt.Sprintf("%s.kb_%s", path, time.Now().Format("20060102_150405.000000")) + if err := clientcmd.WriteToFile(*config, tempPath); err != nil { + return cfgcore.WrapError(err, "failed to write merged kubeconfig to temporary file '%s'", tempPath) + } + + // Move temporary file over existing KubeConfig + if err := os.Rename(tempPath, path); err != nil { + return cfgcore.WrapError(err, "failed to overwrite existing KubeConfig '%s' with new kubeconfig '%s'", path, tempPath) + } + return nil +} + +func GetDefaultConfig() string { + defaultKubeConfigLoadingRules := clientcmd.NewDefaultClientConfigLoadingRules() + kcFile := defaultKubeConfigLoadingRules.GetDefaultFilename() + if kcFile == "" { + kcFile = util.GetKubeconfigDir() + } + return kcFile +} + +func kubeconfigLoad(kcFile string) (*clientcmdapi.Config, error) { + if _, err := os.Stat(kcFile); err == nil { + return clientcmd.LoadFromFile(kcFile) + } + return nil, nil +} diff --git a/cmd/infrastructure/tasks/kubernetes.go b/cmd/infrastructure/tasks/kubernetes.go new file mode 100644 index 000000000000..8c14780308a3 --- /dev/null +++ b/cmd/infrastructure/tasks/kubernetes.go @@ -0,0 +1,217 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package tasks + +import ( + "fmt" + "path/filepath" + "strings" + + "github.com/StudioSol/set" + kubekeyapiv1alpha2 "github.com/kubesphere/kubekey/v3/cmd/kk/apis/kubekey/v1alpha2" + "github.com/kubesphere/kubekey/v3/cmd/kk/pkg/bootstrap/os" + "github.com/kubesphere/kubekey/v3/cmd/kk/pkg/bootstrap/os/templates" + "github.com/kubesphere/kubekey/v3/cmd/kk/pkg/common" + "github.com/kubesphere/kubekey/v3/cmd/kk/pkg/core/connector" + "github.com/kubesphere/kubekey/v3/cmd/kk/pkg/core/task" + "github.com/kubesphere/kubekey/v3/cmd/kk/pkg/kubernetes" + "k8s.io/client-go/tools/clientcmd" + clientcmdapi "k8s.io/client-go/tools/clientcmd/api" + + "github.com/apecloud/kubeblocks/internal/cli/cmd/infrastructure/builder" + "github.com/apecloud/kubeblocks/internal/cli/cmd/infrastructure/constant" + "github.com/apecloud/kubeblocks/internal/cli/cmd/infrastructure/types" + cfgcore "github.com/apecloud/kubeblocks/internal/configuration" + "github.com/apecloud/kubeblocks/internal/gotemplate" +) + +type PrepareK8sBinariesModule struct { + common.KubeModule + + // kubernetes version + BinaryVersion types.InfraVersionInfo +} + +type ConfigureNodeOSModule struct { + common.KubeModule + Nodes []types.ClusterNode +} + +type SaveKubeConfigModule struct { + common.KubeModule + + OutputKubeconfig string +} + +func (p *PrepareK8sBinariesModule) Init() { + p.Name = "PrepareK8sBinariesModule" + p.Desc = "Download installation binaries for kubernetes" + + p.Tasks = []task.Interface{ + &task.LocalTask{ + Name: "PrepareK8sBinaries", + Desc: "Download installation binaries", + Action: &DownloadKubernetesBinary{BinaryVersion: p.BinaryVersion}, + }} +} + +func (c *ConfigureNodeOSModule) Init() { + c.Name = "ConfigureNodeOSModule" + c.Desc = "Init os dependencies" + c.Tasks = []task.Interface{ + &task.RemoteTask{ + Name: "GetOSData", + Desc: "Get OS release", + Hosts: c.Runtime.GetAllHosts(), + Action: new(os.GetOSData), + Parallel: true, + }, + &task.RemoteTask{ + Name: "SetHostName", + Desc: "Prepare to init OS", + Hosts: c.Runtime.GetAllHosts(), + Action: new(os.NodeConfigureOS), + Parallel: true, + }, + &task.RemoteTask{ + Name: "GenerateScript", + Desc: "Generate init os script", + Hosts: c.Runtime.GetAllHosts(), + Action: &NodeScriptGenerator{ + Hosts: templates.GenerateHosts(c.Runtime, c.KubeConf), + Nodes: c.Nodes, + }, + Parallel: true, + }, + &task.RemoteTask{ + Name: "ExecScript", + Desc: "Exec init os script", + Hosts: c.Runtime.GetAllHosts(), + Action: new(os.NodeExecScript), + Parallel: true, + }} +} + +func (p *SaveKubeConfigModule) Init() { + p.Name = "SaveKubeConfigModule" + p.Desc = "Save kube config to local file" + + p.Tasks = []task.Interface{ + &task.LocalTask{ + Name: "SaveKubeConfig", + Desc: "Save kube config to local file", + Action: &SaveKubeConfig{outputKubeconfig: p.OutputKubeconfig}, + }} +} + +type DownloadKubernetesBinary struct { + common.KubeAction + BinaryVersion types.InfraVersionInfo +} + +func (d *DownloadKubernetesBinary) Execute(runtime connector.Runtime) error { + archSet := set.NewLinkedHashSetString() + for _, host := range runtime.GetAllHosts() { + archSet.Add(host.GetArch()) + } + + for _, arch := range archSet.AsSlice() { + binariesMap, err := downloadKubernetesBinaryWithArch(runtime.GetWorkDir(), arch, d.BinaryVersion) + if err != nil { + return err + } + d.PipelineCache.Set(common.KubeBinaries+"-"+arch, binariesMap) + } + return nil +} + +type SaveKubeConfig struct { + common.KubeAction + + outputKubeconfig string +} + +func (c *SaveKubeConfig) Execute(runtime connector.Runtime) error { + master := runtime.GetHostsByRole(common.Master)[0] + + status, ok := c.PipelineCache.Get(common.ClusterStatus) + if !ok { + return cfgcore.MakeError("failed to get kubernetes status.") + } + cluster := status.(*kubernetes.KubernetesStatus) + kubeConfigStr := cluster.KubeConfig + kc, err := clientcmd.Load([]byte(kubeConfigStr)) + if err != nil { + return err + } + updateClusterAPIServer(kc, master, c.KubeConf.Cluster.ControlPlaneEndpoint) + kcFile := GetDefaultConfig() + existingKC, err := kubeconfigLoad(kcFile) + if err != nil { + return err + } + if c.outputKubeconfig == "" { + c.outputKubeconfig = kcFile + } + if existingKC != nil { + return kubeconfigMerge(kc, existingKC, c.outputKubeconfig) + } + return kubeconfigWrite(kc, c.outputKubeconfig) +} + +type NodeScriptGenerator struct { + common.KubeAction + + Nodes []types.ClusterNode + Hosts []string +} + +func (c *NodeScriptGenerator) Execute(runtime connector.Runtime) error { + foundHostOptions := func(nodes []types.ClusterNode, host connector.Host) types.NodeOptions { + for _, node := range nodes { + switch { + default: + return types.NodeOptions{} + case node.Name != host.GetName(): + case node.NodeOptions != nil: + return *node.NodeOptions + } + } + return types.NodeOptions{} + } + + scriptsTemplate := builder.Template{ + Template: constant.ConfigureOSScripts, + Dst: filepath.Join(common.KubeScriptDir, "initOS.sh"), + Values: gotemplate.TplValues{ + "Hosts": c.Hosts, + "Options": foundHostOptions(c.Nodes, runtime.RemoteHost()), + }} + return scriptsTemplate.Execute(runtime) +} + +func updateClusterAPIServer(kc *clientcmdapi.Config, master connector.Host, endpoint kubekeyapiv1alpha2.ControlPlaneEndpoint) { + cpePrefix := fmt.Sprintf("https://%s:", endpoint.Domain) + for _, cluster := range kc.Clusters { + if strings.HasPrefix(cluster.Server, cpePrefix) { + cluster.Server = fmt.Sprintf("https://%s:%d", master.GetAddress(), endpoint.Port) + } + } +} diff --git a/cmd/infrastructure/tasks/kubernetes_test.go b/cmd/infrastructure/tasks/kubernetes_test.go new file mode 100644 index 000000000000..c26042f49b6f --- /dev/null +++ b/cmd/infrastructure/tasks/kubernetes_test.go @@ -0,0 +1,20 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package tasks diff --git a/cmd/infrastructure/tasks/pipeline_runner.go b/cmd/infrastructure/tasks/pipeline_runner.go new file mode 100644 index 000000000000..d4200c571cda --- /dev/null +++ b/cmd/infrastructure/tasks/pipeline_runner.go @@ -0,0 +1,103 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package tasks + +import ( + "fmt" + "io" + + "github.com/kubesphere/kubekey/v3/cmd/kk/pkg/core/cache" + "github.com/kubesphere/kubekey/v3/cmd/kk/pkg/core/connector" + "github.com/kubesphere/kubekey/v3/cmd/kk/pkg/core/ending" + "github.com/kubesphere/kubekey/v3/cmd/kk/pkg/core/module" + "github.com/kubesphere/kubekey/v3/cmd/kk/pkg/core/pipeline" + + cfgcore "github.com/apecloud/kubeblocks/internal/configuration" +) + +type PipelineWrapper struct { + pipeline.Pipeline +} + +func NewPipelineRunner(name string, modules []module.Module, runtime connector.Runtime) *PipelineWrapper { + return &PipelineWrapper{ + Pipeline: pipeline.Pipeline{ + Name: name, + Modules: modules, + Runtime: runtime, + PipelineCache: cache.NewCache(), + SpecHosts: len(runtime.GetAllHosts()), + }, + } +} + +func (w *PipelineWrapper) Do(output io.Writer) error { + defer func() { + w.PipelineCache.Clean() + w.releaseHostsConnector() + }() + + for i := range w.Modules { + m := w.Modules[i] + if m.IsSkip() { + continue + } + if res := w.safeRunModule(m); res.IsFailed() { + return cfgcore.WrapError(res.CombineResult, "failed to execute module: %s", getModuleName(m)) + } + } + fmt.Fprintf(output, "succeed to execute all modules in the pipeline[%s]", w.Name) + return nil +} + +func (w *PipelineWrapper) safeRunModule(m module.Module) *ending.ModuleResult { + newCache := func() *cache.Cache { + if moduleCache, ok := w.ModuleCachePool.Get().(*cache.Cache); ok { + return moduleCache + } + return cache.NewCache() + } + releaseCache := func(cache *cache.Cache) { + cache.Clean() + w.ModuleCachePool.Put(cache) + } + + moduleCache := newCache() + defer releaseCache(moduleCache) + m.Default(w.Runtime, w.PipelineCache, moduleCache) + m.AutoAssert() + m.Init() + return w.RunModule(m) +} + +func (w *PipelineWrapper) releaseHostsConnector() { + for _, host := range w.Runtime.GetAllHosts() { + if connector := w.Runtime.GetConnector(); connector != nil { + connector.Close(host) + } + } +} + +func getModuleName(m module.Module) string { + if b, ok := m.(*module.BaseModule); ok { + return b.Name + } + return "" +} diff --git a/cmd/infrastructure/tasks/tasks.go b/cmd/infrastructure/tasks/tasks.go new file mode 100644 index 000000000000..4cc6a41ef9ff --- /dev/null +++ b/cmd/infrastructure/tasks/tasks.go @@ -0,0 +1,132 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package tasks + +import ( + "fmt" + "strings" + + kubekeyapiv1alpha2 "github.com/kubesphere/kubekey/v3/cmd/kk/apis/kubekey/v1alpha2" + "github.com/kubesphere/kubekey/v3/cmd/kk/pkg/bootstrap/os" + "github.com/kubesphere/kubekey/v3/cmd/kk/pkg/common" + "github.com/kubesphere/kubekey/v3/cmd/kk/pkg/core/connector" + "github.com/kubesphere/kubekey/v3/cmd/kk/pkg/core/logger" + "github.com/kubesphere/kubekey/v3/util/osrelease" + + cfgcore "github.com/apecloud/kubeblocks/internal/configuration" +) + +type InstallDependenciesTask struct { + common.KubeAction + pkg []string +} + +var dependenciesPkg = []string{"socat", "conntrack", "ipset", "ebtables", "chrony", "iptables", "curl", "ipvsadm"} + +func (i *InstallDependenciesTask) Execute(runtime connector.Runtime) (err error) { + host := runtime.RemoteHost() + release, ok := host.GetCache().Get(os.Release) + if !ok { + return cfgcore.MakeError("failed to get os release.") + } + + r := release.(*osrelease.Data) + installCommand, err := checkRepositoryInstallerCommand(r, runtime) + if err != nil { + return err + } + depPkg := strings.Join(append(i.pkg, dependenciesPkg...), " ") + stdout, err := runtime.GetRunner().SudoCmd(fmt.Sprintf("%s %s", installCommand, depPkg), true) + logger.Log.Info(stdout) + return err +} + +type UpdateNodeTask struct { + common.KubeAction +} + +func (i *UpdateNodeTask) Execute(runtime connector.Runtime) (err error) { + host := runtime.RemoteHost() + if host.GetArch() != "" { + return nil + } + + stdout, err := runtime.GetRunner().Cmd("uname -m", false) + if err != nil { + return err + } + host.SetArch(parseNodeArchitecture(stdout)) + updateClusterSpecHost(i.KubeConf.Cluster, host) + return nil +} + +func updateClusterSpecHost(clusterSpec *kubekeyapiv1alpha2.ClusterSpec, host connector.Host) { + for i := range clusterSpec.Hosts { + h := &clusterSpec.Hosts[i] + if h.Name == host.GetName() { + h.Arch = host.GetArch() + } + } +} + +func parseNodeArchitecture(stdout string) string { + switch strings.TrimSpace(stdout) { + default: + return "amd64" + case "x86_64": + return "amd64" + case "arm64", "arm": + return "arm64" + case "aarch64", "aarch32": + return "arm" + } +} + +func checkRepositoryInstallerCommand(osData *osrelease.Data, runtime connector.Runtime) (string, error) { + const ( + debianCommand = "apt install -y" + rhelCommand = "yum install -y" + ) + + isDebianCore := func() bool { + checkDeb, err := runtime.GetRunner().SudoCmd("which apt", false) + return err == nil && strings.Contains(checkDeb, "bin") + } + isRhelCore := func() bool { + checkDeb, err := runtime.GetRunner().SudoCmd("which yum", false) + return err == nil && strings.Contains(checkDeb, "bin") + } + + switch strings.ToLower(osData.ID) { + case "ubuntu", "debian": + return debianCommand, nil + case "centos", "rhel": + return rhelCommand, nil + } + + switch { + default: + return "", cfgcore.MakeError("failed to check apt or yum.") + case isRhelCore(): + return rhelCommand, nil + case isDebianCore(): + return debianCommand, nil + } +} diff --git a/cmd/infrastructure/types/features.go b/cmd/infrastructure/types/features.go new file mode 100644 index 000000000000..e79d61063f02 --- /dev/null +++ b/cmd/infrastructure/types/features.go @@ -0,0 +1,27 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package types + +type HugePageFeature struct { + HugePageSize string `json:"hugePageSize"` + + // HugeSizeWithUnit is the huge page size with unit, such as 2Mi, 1Gi. + HugeSizeWithUnit string `json:"hugeSizeWithUnit"` +} diff --git a/cmd/infrastructure/types/type.go b/cmd/infrastructure/types/type.go new file mode 100644 index 000000000000..e73ebd1aaeb0 --- /dev/null +++ b/cmd/infrastructure/types/type.go @@ -0,0 +1,215 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package types + +import ( + "github.com/apecloud/kubeblocks/internal/cli/cmd/infrastructure/constant" + kubekeyapiv1alpha2 "github.com/kubesphere/kubekey/v3/cmd/kk/apis/kubekey/v1alpha2" + "helm.sh/helm/v3/pkg/cli/values" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type InfraVersionInfo struct { + KubernetesVersion string `json:"kubernetesVersion"` + EtcdVersion string `json:"etcdVersion"` + ContainerVersion string `json:"containerVersion"` + CRICtlVersion string `json:"crictlVersion"` + RuncVersion string `json:"runcVersion"` + CniVersion string `json:"cniVersion"` + HelmVersion string `json:"helmVersion"` +} + +type PluginMeta struct { + Name string `json:"name,omitempty"` + Namespace string `json:"namespace,omitempty"` + Sources PluginSources `json:"sources,omitempty"` +} + +type PluginSources struct { + Chart *HelmChart `json:"chart"` + Yaml *Yaml `json:"yaml"` +} + +type HelmChart struct { + Name string `json:"name"` + Version string `json:"version"` + Repo string `json:"repo"` + Path string `json:"path"` + ValueOptions values.Options `json:"options"` +} + +type Yaml struct { + Path []string `json:"path,omitempty"` +} + +type Cluster struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + User ClusterUser `json:"user"` + Nodes []ClusterNode `json:"nodes"` + + RoleGroup RoleGroup `json:"roleGroup"` + Addons []PluginMeta `json:"addons"` + + // for kubeadm configuration + Kubernetes Kubernetes `json:"kubernetes"` + + Version InfraVersionInfo `json:"version"` +} + +type RoleGroup struct { + ETCD []string `json:"etcd"` + Master []string `json:"master"` + Worker []string `json:"worker"` +} + +type ClusterNode struct { + Name string `json:"name"` + Address string `json:"address"` + InternalAddress string `json:"internalAddress"` + NodeOptions *NodeOptions `json:"options"` +} + +type NodeOptions struct { + HugePageFeature *HugePageFeature `json:"hugePageFeature"` +} + +type Kubernetes struct { + // ClusterName string `json:"clusterName"` + // DNSDomain string `json:"dnsDomain"` + ProxyMode string `json:"proxyMode"` + + Networking Networking `json:"networking"` + CRI ContainerRuntime `json:"cri"` + + ControlPlaneEndpoint ControlPlaneEndpoint `json:"controlPlaneEndpoint"` + APIServer APIServer `json:"apiServer"` + Scheduler Scheduler `json:"scheduler"` +} + +type Networking struct { + // using network plugin, default is calico + Plugin string `json:"plugin"` + + // apis/kubeadm/types.Networking + ServiceSubnet string `json:"serviceSubnet"` + PodSubnet string `json:"podSubnet"` + DNSDomain string `json:"dnsDomain"` +} + +type ContainerRuntime struct { + ContainerRuntimeType string `json:"containerRuntimeType"` + ContainerRuntimeEndpoint string `json:"containerRuntimeEndpoint"` + SandBoxImage string `json:"sandBoxImage"` +} + +type ControlPlaneComponent struct { + // apiserver extra args + ExtraArgs map[string]string `json:"extraArgs"` +} + +type APIServer struct { + ControlPlaneComponent `json:",inline"` +} + +type Scheduler struct { + ControlPlaneComponent `json:",inline"` +} + +type ControlPlaneEndpoint struct { + Domain string `json:"domain"` + Port int `json:"port"` + + // TODO support apiserver loadbalancer + LoadBalancer string `json:"loadBalancer"` +} + +type ClusterUser struct { + // user name + Name string `json:"name"` + // sudo password + Password string `json:"password"` + // ssh privateKey + PrivateKey string `json:"privateKey"` + PrivateKeyPath string `json:"privateKeyPath"` +} + +func (g *RoleGroup) IsValidate() bool { + return len(g.ETCD) > 0 && len(g.Master) > 0 && len(g.Worker) > 0 +} + +func (k *Kubernetes) AutoDefaultFill() { + // if k.ClusterName == "" { + // k.ClusterName = constant.DefaultK8sClusterName + // } + // if k.DNSDomain == "" { + // k.DNSDomain = constant.DefaultK8sDNSDomain + // } + if k.ProxyMode == "" { + k.ProxyMode = constant.DefaultK8sProxyMode + } + + fillNetworkField(&k.Networking) + fillContainerRuntimeField(&k.CRI) + fillAPIServerField(&k.APIServer) + fillSchedulerField(&k.Scheduler) + fillControlPlaneField(&k.ControlPlaneEndpoint) +} + +func fillContainerRuntimeField(c *ContainerRuntime) { + if c.ContainerRuntimeType == "" { + c.ContainerRuntimeType = kubekeyapiv1alpha2.Conatinerd + c.ContainerRuntimeEndpoint = kubekeyapiv1alpha2.DefaultContainerdEndpoint + } + if c.SandBoxImage == "" { + c.SandBoxImage = constant.DefaultSandBoxImage + } +} + +func fillControlPlaneField(c *ControlPlaneEndpoint) { + if c.Port == 0 { + c.Port = constant.DefaultAPIServerPort + } + if c.Domain == "" { + c.Domain = constant.DefaultAPIDNSDomain + } +} + +func fillSchedulerField(s *Scheduler) { +} + +func fillAPIServerField(a *APIServer) { +} + +func fillNetworkField(n *Networking) { + if n.Plugin == "" { + n.Plugin = constant.DefaultNetworkPlugin + } + if n.DNSDomain == "" { + n.DNSDomain = constant.DefaultK8sDNSDomain + } + if n.ServiceSubnet == "" { + n.ServiceSubnet = kubekeyapiv1alpha2.DefaultServiceCIDR + } + if n.PodSubnet == "" { + n.PodSubnet = kubekeyapiv1alpha2.DefaultPodsCIDR + } +} diff --git a/cmd/infrastructure/utils/checksum.go b/cmd/infrastructure/utils/checksum.go new file mode 100644 index 000000000000..3305293198e6 --- /dev/null +++ b/cmd/infrastructure/utils/checksum.go @@ -0,0 +1,81 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package utils + +import ( + "crypto/sha256" + "fmt" + "io" + "os" + "strings" + + "github.com/kubesphere/kubekey/v3/cmd/kk/pkg/core/util" + "github.com/kubesphere/kubekey/v3/cmd/kk/pkg/files" + + cfgcore "github.com/apecloud/kubeblocks/internal/configuration" +) + +func getSha256sumFile(binary *files.KubeBinary) string { + return fmt.Sprintf("%s.sum.%s", binary.Path(), "sha256") +} + +func CheckSha256sum(binary *files.KubeBinary) error { + checksumFile := getSha256sumFile(binary) + if !util.IsExist(checksumFile) { + return cfgcore.MakeError("checksum file %s is not exist", checksumFile) + } + + checksum, err := calSha256sum(binary.Path()) + if err != nil { + return err + } + + data, err := os.ReadFile(checksumFile) + if err != nil { + return err + } + if strings.TrimSpace(string(data)) == checksum { + return nil + } + return cfgcore.MakeError("checksum of %s is not match, [%s] vs [%s]", binary.ID, string(data), checksum) +} + +func calSha256sum(path string) (string, error) { + file, err := os.Open(path) + if err != nil { + return "", err + } + defer file.Close() + + data, err := io.ReadAll(file) + if err != nil { + return "", err + } + return fmt.Sprintf("%x", sha256.Sum256(data)), nil +} + +func WriteSha256sum(binary *files.KubeBinary) error { + checksumFile := getSha256sumFile(binary) + sum, err := calSha256sum(binary.Path()) + if err != nil { + return err + } + return util.WriteFile(checksumFile, []byte(sum)) +} diff --git a/cmd/infrastructure/utils/command_wrapper.go b/cmd/infrastructure/utils/command_wrapper.go new file mode 100644 index 000000000000..a732a47621c1 --- /dev/null +++ b/cmd/infrastructure/utils/command_wrapper.go @@ -0,0 +1,56 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package utils + +import ( + "errors" + "fmt" + "io" + "os/exec" + + "github.com/kubesphere/kubekey/v3/cmd/kk/pkg/core/common" + "github.com/kubesphere/kubekey/v3/cmd/kk/pkg/core/logger" +) + +func RunCommand(cmd *exec.Cmd) error { + logger.Log.Messagef(common.LocalHost, "Running: %s", cmd.String()) + stdout, err := cmd.StdoutPipe() + if err != nil { + return err + } + cmd.Stderr = cmd.Stdout + if err = cmd.Start(); err != nil { + return err + } + + // read from stdout + for { + tmp := make([]byte, 1024) + _, err := stdout.Read(tmp) + fmt.Print(string(tmp)) + if errors.Is(err, io.EOF) { + break + } else if err != nil { + logger.Log.Errorln(err) + break + } + } + return cmd.Wait() +} diff --git a/cmd/infrastructure/utils/helm_wrapper.go b/cmd/infrastructure/utils/helm_wrapper.go new file mode 100644 index 000000000000..0bbaa213fd08 --- /dev/null +++ b/cmd/infrastructure/utils/helm_wrapper.go @@ -0,0 +1,92 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package utils + +import ( + "fmt" + "path/filepath" + + "helm.sh/helm/v3/pkg/repo" + "k8s.io/klog/v2" + + "github.com/apecloud/kubeblocks/internal/cli/cmd/infrastructure/types" + "github.com/apecloud/kubeblocks/internal/cli/util/helm" +) + +type HelmInstallHelper struct { + types.HelmChart + kubeconfig string +} + +func (h *HelmInstallHelper) Install(name, ns string) error { + if err := h.addRepo(); err != nil { + return err + } + helmConfig := helm.NewConfig(ns, h.kubeconfig, "", klog.V(1).Enabled()) + installOpts := h.buildChart(name, ns) + output, err := installOpts.Install(helmConfig) + if err != nil && helm.ReleaseNotFound(err) { + fmt.Println(output) + return nil + } + return err +} + +func NewHelmInstaller(chart types.HelmChart, kubeconfig string) Installer { + installer := HelmInstallHelper{ + HelmChart: chart, + kubeconfig: kubeconfig} + return &installer +} + +func (h *HelmInstallHelper) buildChart(name, ns string) *helm.InstallOpts { + return &helm.InstallOpts{ + Name: name, + Chart: h.getChart(name), + Wait: true, + Version: h.Version, + Namespace: ns, + ValueOpts: &h.ValueOptions, + TryTimes: 3, + CreateNamespace: true, + Atomic: true, + } +} + +func (h *HelmInstallHelper) getChart(name string) string { + if h.Name == "" { + return "" + } + // install helm package form local path + if h.Repo != "" { + return filepath.Join(h.Name, name) + } + if h.Path != "" { + return filepath.Join(h.Path, name) + } + return "" +} + +func (h *HelmInstallHelper) addRepo() error { + if h.Repo == "" { + return nil + } + return helm.AddRepo(&repo.Entry{Name: h.Name, URL: h.Repo}) +} diff --git a/cmd/infrastructure/utils/install.go b/cmd/infrastructure/utils/install.go new file mode 100644 index 000000000000..6da5dce97191 --- /dev/null +++ b/cmd/infrastructure/utils/install.go @@ -0,0 +1,24 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package utils + +type Installer interface { + Install(name, ns string) error +} diff --git a/cmd/infrastructure/utils/yaml_wrapper.go b/cmd/infrastructure/utils/yaml_wrapper.go new file mode 100644 index 000000000000..aaf400c4d75d --- /dev/null +++ b/cmd/infrastructure/utils/yaml_wrapper.go @@ -0,0 +1,34 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package utils + +import "github.com/apecloud/kubeblocks/internal/cli/cmd/infrastructure/types" + +type YamlInstallHelper struct { +} + +func (y *YamlInstallHelper) Install(name, ns string) error { + // TODO implement me + panic("implement me") +} + +func NewYamlInstaller(yaml types.Yaml, kubeconfig string) Installer { + return &YamlInstallHelper{} +} diff --git a/util/helm/errors.go b/util/helm/errors.go index 687872499b67..9f0b7d3e9eab 100644 --- a/util/helm/errors.go +++ b/util/helm/errors.go @@ -34,7 +34,7 @@ import ( var ErrReleaseNotDeployed = fmt.Errorf("release: not in deployed status") -func releaseNotFound(err error) bool { +func ReleaseNotFound(err error) bool { if err == nil { return false } diff --git a/util/helm/helm.go b/util/helm/helm.go index df995d489732..474c770bf839 100644 --- a/util/helm/helm.go +++ b/util/helm/helm.go @@ -198,7 +198,7 @@ func (i *InstallOpts) tryInstall(cfg *action.Configuration) (*release.Release, e if released != nil { return released, nil } - if err != nil && !releaseNotFound(err) { + if err != nil && !ReleaseNotFound(err) { return nil, err } } diff --git a/util/helm/helm_test.go b/util/helm/helm_test.go index 56addc920e15..62a1364f7941 100644 --- a/util/helm/helm_test.go +++ b/util/helm/helm_test.go @@ -119,7 +119,7 @@ var _ = Describe("helm util", func() { }) It("should fail when release is not found", func() { - Expect(releaseNotFound(o.Upgrade(cfg))).Should(BeTrue()) + Expect(ReleaseNotFound(o.Upgrade(cfg))).Should(BeTrue()) Expect(o.Uninstall(cfg)).Should(HaveOccurred()) // release not found }) diff --git a/util/util.go b/util/util.go index d6692151d225..8039c140e8f3 100644 --- a/util/util.go +++ b/util/util.go @@ -559,7 +559,7 @@ func IsSupportReconfigureParams(tpl appsv1alpha1.ComponentConfigSpec, values map return true, nil } -func getIPLocation() (string, error) { +func GetIPLocation() (string, error) { client := &http.Client{Timeout: 10 * time.Second} req, err := http.NewRequest("GET", "https://ifconfig.io/country_code", nil) if err != nil { @@ -595,7 +595,7 @@ func GetHelmChartRepoURL() string { // if helm repo url is not specified, choose one from GitHub and GitLab based on the IP location // if location is CN, or we can not get location, use GitLab helm chart repo repo := types.KubeBlocksChartURL - location, _ := getIPLocation() + location, _ := GetIPLocation() if location == "CN" || location == "" { repo = types.GitLabHelmChartRepo } diff --git a/util/util_test.go b/util/util_test.go index 5c5708853be4..0531d5c6761d 100644 --- a/util/util_test.go +++ b/util/util_test.go @@ -252,7 +252,7 @@ var _ = Describe("util", func() { }) It("get IP location", func() { - _, _ = getIPLocation() + _, _ = GetIPLocation() }) It("get helm chart repo url", func() {