diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 426e38cd3..fd42b93a5 100755 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -54,7 +54,10 @@ jobs: ./ossutil64 -c .ossutilconfig cp -f dist/sealos_linux_arm64/sealos oss://${BUCKETNAME:-sealyun}/${{ steps.prepare.outputs.tag_name }}/sealos-arm64 ./ossutil64 -c .ossutilconfig cp -f dist/sealos_linux_amd64/sealos oss://${BUCKETNAME:-sealyun}/latest/sealos ./ossutil64 -c .ossutilconfig cp -f dist/sealos_linux_arm64/sealos oss://${BUCKETNAME:-sealyun}/latest/sealos-arm64 - + ./ossutil64 -c .ossutilconfig cp -f dist/sealctl_linux_amd64/sealctl oss://${BUCKETNAME:-sealyun}/${{ steps.prepare.outputs.tag_name }}/sealctl + ./ossutil64 -c .ossutilconfig cp -f dist/sealctl_linux_arm64/sealctl oss://${BUCKETNAME:-sealyun}/${{ steps.prepare.outputs.tag_name }}/sealctl-arm64 + ./ossutil64 -c .ossutilconfig cp -f dist/sealctl_linux_amd64/sealctl oss://${BUCKETNAME:-sealyun}/latest/sealctl + ./ossutil64 -c .ossutilconfig cp -f dist/sealctl_linux_arm64/sealctl oss://${BUCKETNAME:-sealyun}/latest/sealctl-arm64 - name: set up buildx id: buildx uses: crazy-max/ghaction-docker-buildx@v1 diff --git a/.goreleaser.yml b/.goreleaser.yml index d0f4969b5..dc48885eb 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -7,6 +7,9 @@ before: builds: - env: - CGO_ENABLED=0 + main: ./cmd/sealos + id: sealos + binary: sealos goos: - linux - darwin @@ -18,6 +21,23 @@ builds: - -X github.com/fanux/sealos/pkg/version.gitCommit={{.ShortCommit}} - -X github.com/fanux/sealos/pkg/version.buildDate={{.Date}} - -s -w + - env: + - CGO_ENABLED=0 + main: ./cmd/sealctl + id: sealctl + binary: sealctl + goos: + - linux + - darwin + goarch: + - amd64 + - arm64 + ldflags: + - -X github.com/fanux/sealos/pkg/version.gitVersion={{.Version}} + - -X github.com/fanux/sealos/pkg/version.gitCommit={{.ShortCommit}} + - -X github.com/fanux/sealos/pkg/version.buildDate={{.Date}} + - -s -w + #archives: # - replacements: # darwin: Darwin diff --git a/Makefile b/Makefile index 760df3f01..cdcd8b595 100644 --- a/Makefile +++ b/Makefile @@ -70,6 +70,8 @@ install-ossutil: ## check license if not exist install go-lint tools push-oss:install-ossutil build $(OSSUTIL_BIN) cp -f dist/sealos_linux_amd64/sealos oss://sealyun-temp/sealos/${COMMIT_ID}/sealos $(OSSUTIL_BIN) cp -f dist/sealos_linux_arm64/sealos oss://sealyun-temp/sealos/${COMMIT_ID}/sealos-arm64 + $(OSSUTIL_BIN) cp -f dist/sealctl_linux_amd64/sealctl oss://sealyun-temp/sealos/${COMMIT_ID}/sealctl + $(OSSUTIL_BIN) cp -f dist/sealctl_linux_arm64/sealctl oss://sealyun-temp/sealos/${COMMIT_ID}/sealctl-arm64 generator-contributors: git log --format='%aN <%aE>' | sort -uf > CONTRIBUTORS diff --git a/cmd/completion.go b/cmd/completion.go deleted file mode 100644 index 0125970fc..000000000 --- a/cmd/completion.go +++ /dev/null @@ -1,230 +0,0 @@ -package cmd - -import ( - "bytes" - "fmt" - "io" - "os" - - "github.com/fanux/sealos/pkg/utils/logger" - - "github.com/spf13/cobra" -) - -const defaultCopyRight = ` -# Copyright © 2020 sealos. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -` - -var ( - completionShells = map[string]func(out io.Writer, boilerPlate string, cmd *cobra.Command) error{ - "bash": runCompletionBash, - "zsh": runCompletionZsh, - } - completionExample = ` - # Installing bash completion on macOS using homebrew - ## If running Bash 3.2 included with macOS - brew install bash-completion - ## or, if running Bash 4.1+ - brew install bash-completion@2 - ## If you've installed via other means, you may need add the completion to your completion directory - sealos completion bash > $(brew --prefix)/etc/bash_completion.d/sealos - - # Load the sealos completion code for zsh[1] into the current shell - source <(sealos completion zsh) - # Set the sealos completion code for zsh[1] to autoload on startup - sealos completion zsh > "${fpath[1]}/_sealos" - - # Installing bash completion on Linux - ## If bash-completion is not installed on Linux, please install the 'bash-completion' package - ## via your distribution's package manager. - ## Load the sealos completion code for bash into the current shell - source <(sealos completion bash) - ## Write bash completion code to a file and source if from .bash_profile - sealos completion bash > ~/.sealos/completion.bash.inc - printf " - # sealos shell completion - source '$HOME/.sealos/completion.bash.inc' - " >> $HOME/.bash_profile - source $HOME/.bash_profile -` -) - -func init() { - rootCmd.AddCommand(NewCmdCompletion(os.Stdout, "")) -} - -func NewCmdCompletion(out io.Writer, boilerPlate string) *cobra.Command { - var shells []string - for s := range completionShells { - shells = append(shells, s) - } - - cmd := &cobra.Command{ - Use: "completion bash", - DisableFlagsInUseLine: true, - Short: "Output shell completion code for the specified shell (bash or zsh)", - Example: completionExample, - Run: func(cmd *cobra.Command, args []string) { - err := RunCompletion(out, boilerPlate, cmd, args) - if err != nil { - logger.Error(err) - return - } - }, - ValidArgs: shells, - } - return cmd -} - -func RunCompletion(out io.Writer, copyRight string, cmd *cobra.Command, args []string) error { - if len(args) == 0 { - return fmt.Errorf("shell not specified") - } - if len(args) > 1 { - return fmt.Errorf("too many arguments, expected only the shell type") - } - run, found := completionShells[args[0]] - if !found { - return fmt.Errorf("unsupported shell type %q", args[0]) - } - - return run(out, copyRight, cmd.Parent()) -} - -func runCompletionBash(out io.Writer, copyRight string, sealos *cobra.Command) error { - if len(copyRight) == 0 { - copyRight = defaultCopyRight - } - if _, err := out.Write([]byte(copyRight)); err != nil { - return err - } - - return sealos.GenBashCompletion(out) -} - -func runCompletionZsh(out io.Writer, copyRight string, sealos *cobra.Command) error { - zshHead := "#compdef sealos\n" - - _, _ = out.Write([]byte(zshHead)) - - if len(copyRight) == 0 { - copyRight = defaultCopyRight - } - if _, err := out.Write([]byte(copyRight)); err != nil { - return err - } - - zshInitialization := ` -__sealos_bash_source() { - alias shopt=':' - emulate -L sh - setopt kshglob noshglob braceexpand - source "$@" -} -__sealos_type() { - # -t is not supported by zsh - if [ "$1" == "-t" ]; then - shift - # fake Bash 4 to disable "complete -o nospace". Instead - # "compopt +-o nospace" is used in the code to toggle trailing - # spaces. We don't support that, but leave trailing spaces on - # all the time - if [ "$1" = "__sealos_compopt" ]; then - echo builtin - return 0 - fi - fi - type "$@" -} -__sealos_compgen() { - local completions w - completions=( $(compgen "$@") ) || return $? - # filter by given word as prefix - while [[ "$1" = -* && "$1" != -- ]]; do - shift - shift - done - if [[ "$1" == -- ]]; then - shift - fi - for w in "${completions[@]}"; do - if [[ "${w}" = "$1"* ]]; then - echo "${w}" - fi - done -} -__sealos_compopt() { - true # don't do anything. Not supported by bashcompinit in zsh -} -__sealos_ltrim_colon_completions() -{ - if [[ "$1" == *:* && "$COMP_WORDBREAKS" == *:* ]]; then - # Remove colon-word prefix from COMPREPLY items - local colon_word=${1%${1##*:}} - local i=${#COMPREPLY[*]} - while [[ $((--i)) -ge 0 ]]; do - COMPREPLY[$i]=${COMPREPLY[$i]#"$colon_word"} - done - fi -} -__sealos_get_comp_words_by_ref() { - cur="${COMP_WORDS[COMP_CWORD]}" - prev="${COMP_WORDS[${COMP_CWORD}-1]}" - words=("${COMP_WORDS[@]}") - cword=("${COMP_CWORD[@]}") -} -__sealos_filedir() { - # Don't need to do anything here. - # Otherwise we will get trailing space without "compopt -o nospace" - true -} -autoload -U +X bashcompinit && bashcompinit -# use word boundary patterns for BSD or GNU sed -LWORD='[[:<:]]' -RWORD='[[:>:]]' -if sed --help 2>&1 | grep -q 'GNU\|BusyBox'; then - LWORD='\<' - RWORD='\>' -fi -__sealos_convert_bash_to_zsh() { - sed \ - -e 's/declare -F/whence -w/' \ - -e 's/_get_comp_words_by_ref "\$@"/_get_comp_words_by_ref "\$*"/' \ - -e 's/local \([a-zA-Z0-9_]*\)=/local \1; \1=/' \ - -e 's/flags+=("\(--.*\)=")/flags+=("\1"); two_word_flags+=("\1")/' \ - -e 's/must_have_one_flag+=("\(--.*\)=")/must_have_one_flag+=("\1")/' \ - -e "s/${LWORD}_filedir${RWORD}/__sealos_filedir/g" \ - -e "s/${LWORD}_get_comp_words_by_ref${RWORD}/__sealos_get_comp_words_by_ref/g" \ - -e "s/${LWORD}__ltrim_colon_completions${RWORD}/__sealos_ltrim_colon_completions/g" \ - -e "s/${LWORD}compgen${RWORD}/__sealos_compgen/g" \ - -e "s/${LWORD}compopt${RWORD}/__sealos_compopt/g" \ - -e "s/${LWORD}declare${RWORD}/builtin declare/g" \ - -e "s/\\\$(type${RWORD}/\$(__sealos_type/g" \ - <<'BASH_COMPLETION_EOF' -` - _, _ = out.Write([]byte(zshInitialization)) - - buf := new(bytes.Buffer) - _ = sealos.GenBashCompletion(buf) - _, _ = out.Write(buf.Bytes()) - - zshTail := ` -BASH_COMPLETION_EOF -} -__sealos_bash_source <(__sealos_convert_bash_to_zsh) -` - _, _ = out.Write([]byte(zshTail)) - return nil -} diff --git a/cmd/sealctl/boot/boot.go b/cmd/sealctl/boot/boot.go new file mode 100644 index 000000000..33d2c7a9e --- /dev/null +++ b/cmd/sealctl/boot/boot.go @@ -0,0 +1,50 @@ +/* +Copyright 2022 cuisongliu@qq.com. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package boot + +import ( + "fmt" + "github.com/fanux/sealos/pkg/utils/logger" + "os" +) + +var Debug bool +var ConfigDir string + +// initConfig reads in config file and ENV variables if set. +func initConfig() { + logger.Cfg(Debug, ConfigDir, "sealctl") +} + +func InitRootDirectory(rootDirs []string) error { + for _, dir := range rootDirs { + err := os.MkdirAll(dir, 0755) + if err != nil { + return fmt.Errorf("failed to mkdir %s, err: %s", dir, err) + } + } + return nil +} + +func OnBootOnDie() { + + if err := InitRootDirectory([]string{ConfigDir}); err != nil { + logger.Error("onBoot is error: %v", err) + os.Exit(1) + } + initConfig() +} diff --git a/cmd/cert.go b/cmd/sealctl/cmd/cert.go similarity index 75% rename from cmd/cert.go rename to cmd/sealctl/cmd/cert.go index fad7e8451..521da6d5e 100755 --- a/cmd/cert.go +++ b/cmd/sealctl/cmd/cert.go @@ -15,8 +15,10 @@ package cmd import ( - "github.com/fanux/sealos/pkg/install" + "github.com/fanux/sealos/pkg/utils/kubernetes/cert" + "github.com/fanux/sealos/pkg/utils/logger" "github.com/spf13/cobra" + "os" ) type Flag struct { @@ -37,7 +39,11 @@ var certCmd = &cobra.Command{ Short: "generate certs", Long: `you can specify expire time`, Run: func(cmd *cobra.Command, args []string) { - install.GenerateCert(config.CertPath, config.CertEtcdPath, config.AltNames, config.NodeIP, config.NodeName, config.ServiceCIDR, config.DNSDomain) + err := cert.GenerateCert(config.CertPath, config.CertEtcdPath, config.AltNames, config.NodeIP, config.NodeName, config.ServiceCIDR, config.DNSDomain) + if err != nil { + logger.Error(err) + os.Exit(-1) + } }, } @@ -52,14 +58,4 @@ func init() { certCmd.Flags().StringVar(&config.DNSDomain, "dns-domain", "cluster.local", "cluster dns domain") certCmd.Flags().StringVar(&config.CertPath, "cert-path", "/etc/kubernetes/pki", "kubernetes cert file path") certCmd.Flags().StringVar(&config.CertEtcdPath, "cert-etcd-path", "/etc/kubernetes/pki/etcd", "kubernetes etcd cert file path") - - // Here you will define your flags and configuration settings. - - // Cobra supports Persistent Flags which will work for this command - // and all subcommands, e.g.: - // certCmd.PersistentFlags().String("foo", "", "A help for foo") - - // Cobra supports local flags which will only run when this command - // is called directly, e.g.: - // certCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") } diff --git a/cmd/sealctl/cmd/hostname.go b/cmd/sealctl/cmd/hostname.go new file mode 100755 index 000000000..e730d7be1 --- /dev/null +++ b/cmd/sealctl/cmd/hostname.go @@ -0,0 +1,48 @@ +// Copyright © 2021 sealos. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "github.com/spf13/cobra" + "os" +) + +// hostnameCmd represents the cert command +var hostnameCmd = &cobra.Command{ + Use: "hostname", + Short: "get os.hostname", + RunE: func(cmd *cobra.Command, args []string) error { + hostname, err := os.Hostname() + if err != nil { + return err + } + print(hostname) + return nil + }, +} + +func init() { + rootCmd.AddCommand(hostnameCmd) + + // Here you will define your flags and configuration settings. + + // Cobra supports Persistent Flags which will work for this command + // and all subcommands, e.g.: + // hostnameCmd.PersistentFlags().String("foo", "", "A help for foo") + + // Cobra supports local flags which will only run when this command + // is called directly, e.g.: + // hostnameCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") +} diff --git a/cmd/sealctl/cmd/hosts.go b/cmd/sealctl/cmd/hosts.go new file mode 100644 index 000000000..aa900d120 --- /dev/null +++ b/cmd/sealctl/cmd/hosts.go @@ -0,0 +1,112 @@ +/* +Copyright 2022 cuisongliu@qq.com. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cmd + +import ( + "fmt" + "github.com/fanux/sealos/pkg/hosts" + "github.com/spf13/cobra" +) + +var hostsPath string + +func NewHostsCmd() *cobra.Command { + var cmd = &cobra.Command{ + Use: "hosts", + Short: "hosts manager", + //Run: func(cmd *cobra.Command, args []string) { + // + //}, + } + // check route for host + cmd.AddCommand(NewHostsListCmd()) + cmd.AddCommand(NewHostsAddCmd()) + cmd.AddCommand(NewHostsDeleteCmd()) + cmd.PersistentFlags().StringVar(&hostsPath, "path", "/etc/hosts", "default hosts path") + return cmd +} + +func NewHostsListCmd() *cobra.Command { + var cmd = &cobra.Command{ + Use: "list", + Short: "hosts manager list", + Run: func(cmd *cobra.Command, args []string) { + hf := &hosts.HostFile{Path: hostsPath} + hf.ListCurrentHosts() + }, + } + return cmd +} + +func NewHostsAddCmd() *cobra.Command { + var ip, domain string + var cmd = &cobra.Command{ + Use: "add", + Short: "hosts manager add", + PreRunE: func(cmd *cobra.Command, args []string) error { + if ip == "" { + return fmt.Errorf("ip not empty") + } + if domain == "" { + return fmt.Errorf("domain not empty") + } + return nil + }, + Run: func(cmd *cobra.Command, args []string) { + hf := &hosts.HostFile{Path: hostsPath} + hf.AppendHost(domain, ip) + }, + } + cmd.Flags().StringVar(&ip, "ip", "", "ip address") + cmd.Flags().StringVar(&domain, "domain", "", "domain address") + + return cmd +} +func NewHostsDeleteCmd() *cobra.Command { + var domain string + var cmd = &cobra.Command{ + Use: "delete", + Short: "hosts manager delete", + PreRunE: func(cmd *cobra.Command, args []string) error { + if domain == "" { + return fmt.Errorf("domain not empty") + } + return nil + }, + Run: func(cmd *cobra.Command, args []string) { + hf := &hosts.HostFile{Path: hostsPath} + hf.DeleteDomain(domain) + }, + } + cmd.Flags().StringVar(&domain, "domain", "", "domain address") + + return cmd +} + +func init() { + rootCmd.AddCommand(NewHostsCmd()) + + // Here you will define your flags and configuration settings. + + // Cobra supports Persistent Flags which will work for this command + // and all subcommands, e.g.: + // hostnameCmd.PersistentFlags().String("foo", "", "A help for foo") + + // Cobra supports local flags which will only run when this command + // is called directly, e.g.: + // hostnameCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") +} diff --git a/cmd/ipvs.go b/cmd/sealctl/cmd/ipvs.go similarity index 61% rename from cmd/ipvs.go rename to cmd/sealctl/cmd/ipvs.go index 3aa752929..ef37051db 100644 --- a/cmd/ipvs.go +++ b/cmd/sealctl/cmd/ipvs.go @@ -15,16 +15,18 @@ package cmd import ( - v1 "github.com/fanux/sealos/pkg/types/v1alpha1" + "github.com/sealyun/lvscare/care" "github.com/spf13/cobra" ) +var Ipvs care.LvsCare + // ipvsCmd represents the ipvs command var ipvsCmd = &cobra.Command{ Use: "ipvs", Short: "sealos create or care local ipvs lb", Run: func(cmd *cobra.Command, args []string) { - v1.Ipvs.VsAndRsCare() + Ipvs.VsAndRsCare() }, } @@ -32,14 +34,14 @@ func init() { rootCmd.AddCommand(ipvsCmd) // Here you will define your flags and configuration settings. - ipvsCmd.Flags().BoolVar(&v1.Ipvs.RunOnce, "run-once", false, "is run once mode") - ipvsCmd.Flags().BoolVarP(&v1.Ipvs.Clean, "clean", "c", true, " clean Vip ipvs rule before join node, if Vip has no ipvs rule do nothing.") - ipvsCmd.Flags().StringVar(&v1.Ipvs.VirtualServer, "vs", "", "virturl server like 10.54.0.2:6443") - ipvsCmd.Flags().StringSliceVar(&v1.Ipvs.RealServer, "rs", []string{}, "virturl server like 192.168.0.2:6443") + ipvsCmd.Flags().BoolVar(&Ipvs.RunOnce, "run-once", false, "is run once mode") + ipvsCmd.Flags().BoolVarP(&Ipvs.Clean, "clean", "c", true, " clean Vip ipvs rule before join node, if Vip has no ipvs rule do nothing.") + ipvsCmd.Flags().StringVar(&Ipvs.VirtualServer, "vs", "", "virturl server like 10.54.0.2:6443") + ipvsCmd.Flags().StringSliceVar(&Ipvs.RealServer, "rs", []string{}, "virturl server like 192.168.0.2:6443") - ipvsCmd.Flags().StringVar(&v1.Ipvs.HealthPath, "health-path", "/healthz", "health check path") - ipvsCmd.Flags().StringVar(&v1.Ipvs.HealthSchem, "health-schem", "https", "health check schem") - ipvsCmd.Flags().Int32Var(&v1.Ipvs.Interval, "interval", 5, "health check interval, unit is sec.") + ipvsCmd.Flags().StringVar(&Ipvs.HealthPath, "health-path", "/healthz", "health check path") + ipvsCmd.Flags().StringVar(&Ipvs.HealthSchem, "health-schem", "https", "health check schem") + ipvsCmd.Flags().Int32Var(&Ipvs.Interval, "interval", 5, "health check interval, unit is sec.") // Cobra supports Persistent Flags which will work for this command // and all subcommands, e.g.: // ipvsCmd.PersistentFlags().String("foo", "", "A help for foo") diff --git a/cmd/sealctl/cmd/kube.go b/cmd/sealctl/cmd/kube.go new file mode 100755 index 000000000..a08ceac34 --- /dev/null +++ b/cmd/sealctl/cmd/kube.go @@ -0,0 +1,82 @@ +// Copyright © 2021 sealos. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "fmt" + "github.com/fanux/sealos/pkg/utils/exec" + "github.com/spf13/cobra" +) + +func NewKubeCmd() *cobra.Command { + var fieldSelector, selector, kubeConfig, sources, namespace string + var allNamespace bool + var cmd = &cobra.Command{ + Use: "kube", + Short: "kube resource list json", + RunE: func(cmd *cobra.Command, args []string) error { + template := "kubectl get %s %s %s %s %s -o json" + ns := "" + c := "" + fs := "" + ls := "" + if allNamespace { + ns = "--all-namespaces" + } else { + if namespace != "" { + ns = "-n " + namespace + } + } + + if kubeConfig != "" { + c = "--kubeconfig=" + kubeConfig + } + if fieldSelector != "" { + c = "--field-selector=" + fieldSelector + } + if selector != "" { + c = "--selector=" + selector + } + data, err := exec.RunBashCmd(fmt.Sprintf(template, sources, ns, c, fs, ls)) + if err != nil { + return err + } + println(data) + return nil + }, + } + cmd.Flags().BoolVar(&allNamespace, "--all-namespaces", false, " If present, list the requested object(s) across all namespaces. Namespace in current\ncontext is ignored even if specified with --namespace.") + cmd.Flags().StringVar(&namespace, "namespace", "", "kubernetes namespace") + cmd.Flags().StringVar(&sources, "sources", "nodes", "e.g. pods,deployments") + cmd.Flags().StringVar(&fieldSelector, "field-selector", "", "Selector (field query) to filter on, supports '=', '==', and '!='.(e.g. --field-selector\nkey1=value1,key2=value2). The server only supports a limited number of field queries per type.") + cmd.Flags().StringVar(&selector, "selector", "", "Selector (label query) to filter on, supports '=', '==', and '!='.(e.g. -l key1=value1,key2=value2)") + cmd.Flags().StringVar(&kubeConfig, "kubeconfig", "", "Path to the kubeconfig file to use for CLI requests.") + + return cmd +} + +func init() { + rootCmd.AddCommand(NewKubeCmd()) + + // Here you will define your flags and configuration settings. + + // Cobra supports Persistent Flags which will work for this command + // and all subcommands, e.g.: + // hostnameCmd.PersistentFlags().String("foo", "", "A help for foo") + + // Cobra supports local flags which will only run when this command + // is called directly, e.g.: + // hostnameCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") +} diff --git a/cmd/sealctl/cmd/password.go b/cmd/sealctl/cmd/password.go new file mode 100755 index 000000000..f30fba5bb --- /dev/null +++ b/cmd/sealctl/cmd/password.go @@ -0,0 +1,103 @@ +// Copyright © 2021 sealos. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "github.com/fanux/sealos/cmd/sealctl/boot" + "github.com/fanux/sealos/pkg/passwd" + "github.com/fanux/sealos/pkg/utils/logger" + "github.com/spf13/cobra" + "io/ioutil" + "os" +) + +func NewPasswordCmd() *cobra.Command { + var cmd = &cobra.Command{ + Use: "password", + Short: "generator password", + //Run: func(cmd *cobra.Command, args []string) { + // + //}, + } + // check route for host + cmd.AddCommand(NewRegistryCmd()) + cmd.AddCommand(NewContainerdCmd()) + return cmd +} + +func NewRegistryCmd() *cobra.Command { + var pwdPath, username, password string + var printBool bool + var cmd = &cobra.Command{ + Use: "registry", + Short: "generator registry password and file", + Run: func(cmd *cobra.Command, args []string) { + pwd := passwd.Htpasswd(username, password) + if printBool { + println(pwd) + return + } + logger.Debug("password registry is %s", pwd) + if err := boot.InitRootDirectory([]string{pwdPath}); err != nil { + logger.Error("init dir is error: %v", err) + os.Exit(1) + } + err := ioutil.WriteFile(pwdPath, []byte(pwd), 0755) + if err != nil { + logger.Error(err) + os.Exit(1) + } + logger.Info("generator registry password is success") + }, + } + // manually to set host via gateway + cmd.Flags().StringVar(&pwdPath, "path", "/etc/registry/registry_htpasswd", "default password file") + cmd.Flags().StringVar(&username, "username", "admin", "username") + cmd.Flags().StringVar(&password, "password", "admin", "password") + + cmd.Flags().BoolVar(&printBool, "print", false, "is print") + + return cmd +} +func NewContainerdCmd() *cobra.Command { + var username, password string + var cmd = &cobra.Command{ + Use: "containerd", + Short: "generator containerd password", + Run: func(cmd *cobra.Command, args []string) { + pwd := passwd.LoginAuth(username, password) + println(pwd) + }, + } + // manually to set host via gateway + cmd.Flags().StringVar(&username, "username", "admin", "username") + cmd.Flags().StringVar(&password, "password", "admin", "password") + + return cmd +} + +func init() { + rootCmd.AddCommand(NewPasswordCmd()) + + // Here you will define your flags and configuration settings. + + // Cobra supports Persistent Flags which will work for this command + // and all subcommands, e.g.: + // hostnameCmd.PersistentFlags().String("foo", "", "A help for foo") + + // Cobra supports local flags which will only run when this command + // is called directly, e.g.: + // hostnameCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") +} diff --git a/cmd/sealctl/cmd/root.go b/cmd/sealctl/cmd/root.go new file mode 100644 index 000000000..0e47b91d9 --- /dev/null +++ b/cmd/sealctl/cmd/root.go @@ -0,0 +1,48 @@ +// Copyright © 2021 sealos. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "fmt" + "github.com/fanux/sealos/cmd/sealctl/boot" + "github.com/fanux/sealos/pkg/types/contants" + "os" + + "github.com/spf13/cobra" +) + +// rootCmd represents the base command when called without any subcommands +var rootCmd = &cobra.Command{ + Use: "sealctl", + Short: "tools for sealos.", + // Uncomment the following line if your bare application + // has an action associated with it: + // Run: func(cmd *cobra.Command, args []string) { }, +} + +// Execute adds all child commands to the root command and sets flags appropriately. +// This is called by main.main(). It only needs to happen once to the rootCmd. +func Execute() { + if err := rootCmd.Execute(); err != nil { + fmt.Println(err) + os.Exit(1) + } +} + +func init() { + cobra.OnInitialize(boot.OnBootOnDie) + rootCmd.PersistentFlags().StringVar(&boot.ConfigDir, "config", contants.DefaultConfigPath, "config dir (default is $HOME/.sealos)") + rootCmd.PersistentFlags().BoolVar(&boot.Debug, "debug", false, "enable debug logger") +} diff --git a/cmd/route.go b/cmd/sealctl/cmd/route.go similarity index 100% rename from cmd/route.go rename to cmd/sealctl/cmd/route.go diff --git a/cmd/sealctl/cmd/static_pod.go b/cmd/sealctl/cmd/static_pod.go new file mode 100755 index 000000000..5e688d3cf --- /dev/null +++ b/cmd/sealctl/cmd/static_pod.go @@ -0,0 +1,103 @@ +// Copyright © 2021 sealos. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "fmt" + "github.com/fanux/sealos/cmd/sealctl/boot" + "github.com/fanux/sealos/pkg/ipvs" + "github.com/fanux/sealos/pkg/types/contants" + "github.com/fanux/sealos/pkg/utils/logger" + "github.com/spf13/cobra" + "io/ioutil" + "os" + "path" +) + +var staticPodPath string + +func NewStaticPodCmd() *cobra.Command { + var cmd = &cobra.Command{ + Use: "static-pod", + Short: "generator static pod", + //Run: func(cmd *cobra.Command, args []string) { + // + //}, + } + // check route for host + cmd.AddCommand(NewLvscareCmd()) + cmd.PersistentFlags().StringVar(&staticPodPath, "path", "/etc/kubernetes/manifests", "default kubernetes static pod path") + return cmd +} + +func NewLvscareCmd() *cobra.Command { + var vip, image, name string + var masters []string + var printBool bool + var cmd = &cobra.Command{ + Use: "lvscare", + Short: "generator lvscare static pod file", + PreRunE: func(cmd *cobra.Command, args []string) error { + if len(masters) == 0 { + return fmt.Errorf("master not allow empty") + } + return nil + }, + Run: func(cmd *cobra.Command, args []string) { + fileName := fmt.Sprintf("%s.yaml", name) + yaml, err := ipvs.LvsStaticPodYaml(vip, masters, image, name) + if err != nil { + logger.Error(err) + os.Exit(1) + } + if printBool { + println(yaml) + return + } + logger.Debug("lvscare static pod yaml is %s", yaml) + if err = boot.InitRootDirectory([]string{staticPodPath}); err != nil { + logger.Error("init dir is error: %v", err) + os.Exit(1) + } + err = ioutil.WriteFile(path.Join(staticPodPath, fileName), []byte(yaml), 0755) + if err != nil { + logger.Error(err) + os.Exit(1) + } + logger.Info("generator lvscare static pod is success") + }, + } + // manually to set host via gateway + cmd.Flags().StringVar(&vip, "vip", "10.103.97.2", "default vip IP") + cmd.Flags().StringVar(&name, "name", contants.LvsCareStaticPodName, "generator lvscare static pod name") + cmd.Flags().StringVar(&image, "image", contants.DefaultLvsCareImage, "generator lvscare static pod image") + cmd.Flags().StringSliceVar(&masters, "masters", nil, "generator masters addrs") + cmd.Flags().BoolVar(&printBool, "print", false, "is print yaml") + return cmd +} + +func init() { + rootCmd.AddCommand(NewStaticPodCmd()) + + // Here you will define your flags and configuration settings. + + // Cobra supports Persistent Flags which will work for this command + // and all subcommands, e.g.: + // hostnameCmd.PersistentFlags().String("foo", "", "A help for foo") + + // Cobra supports local flags which will only run when this command + // is called directly, e.g.: + // hostnameCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") +} diff --git a/cmd/sealctl/cmd/token.go b/cmd/sealctl/cmd/token.go new file mode 100755 index 000000000..458f665a7 --- /dev/null +++ b/cmd/sealctl/cmd/token.go @@ -0,0 +1,56 @@ +// Copyright © 2021 sealos. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "github.com/fanux/sealos/pkg/utils/kubernetes/token" + "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/util/json" +) + +func NewTokenCmd() *cobra.Command { + var master bool + var cmd = &cobra.Command{ + Use: "token", + Short: "token generator", + Run: func(cmd *cobra.Command, args []string) { + var t *token.Token + if master { + t = token.Master() + } else { + t = token.Node() + } + data, _ := json.Marshal(t) + println(string(data)) + }, + } + cmd.Flags().BoolVar(&master, "master", false, "generator token type") + + return cmd +} + +func init() { + rootCmd.AddCommand(NewTokenCmd()) + + // Here you will define your flags and configuration settings. + + // Cobra supports Persistent Flags which will work for this command + // and all subcommands, e.g.: + // hostnameCmd.PersistentFlags().String("foo", "", "A help for foo") + + // Cobra supports local flags which will only run when this command + // is called directly, e.g.: + // hostnameCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") +} diff --git a/cmd/version.go b/cmd/sealctl/cmd/version.go similarity index 100% rename from cmd/version.go rename to cmd/sealctl/cmd/version.go diff --git a/pkg/utils/ssh/types.go b/cmd/sealctl/main.go similarity index 81% rename from pkg/utils/ssh/types.go rename to cmd/sealctl/main.go index 8272ea1db..0cdaa2996 100644 --- a/pkg/utils/ssh/types.go +++ b/cmd/sealctl/main.go @@ -12,16 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ssh +package main import ( - "time" + "github.com/fanux/sealos/cmd/sealctl/cmd" ) -type SSH struct { - User string - Password string - PkFile string - PkPassword string - Timeout *time.Duration +func main() { + cmd.Execute() } diff --git a/cmd/sealos/boot/boot.go b/cmd/sealos/boot/boot.go new file mode 100644 index 000000000..9844587c8 --- /dev/null +++ b/cmd/sealos/boot/boot.go @@ -0,0 +1,55 @@ +/* +Copyright 2022 cuisongliu@qq.com. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package boot + +import ( + "fmt" + "github.com/fanux/sealos/pkg/utils/logger" + "os" +) + +var Debug bool +var ConfigDir string +var ConfigFile string + +var ConfigFilePath string + +// initConfig reads in config file and ENV variables if set. +func initConfig() { + ConfigFilePath = ConfigDir + ConfigFile + logger.Cfg(Debug, ConfigDir, "sealos") +} + +func initRootDirectory() error { + var rootDirs = []string{ + ConfigDir, + } + for _, dir := range rootDirs { + err := os.MkdirAll(dir, 0755) + if err != nil { + return fmt.Errorf("failed to mkdir %s, err: %s", dir, err) + } + } + return nil +} + +func OnBootOnDie() { + if err := initRootDirectory(); err != nil { + panic(1) + } + initConfig() +} diff --git a/cmd/clean.go b/cmd/sealos/cmd/clean.go similarity index 96% rename from cmd/clean.go rename to cmd/sealos/cmd/clean.go index f1835ee9c..348ec92f5 100644 --- a/cmd/clean.go +++ b/cmd/sealos/cmd/clean.go @@ -15,6 +15,7 @@ package cmd import ( + "github.com/fanux/sealos/cmd/sealos/boot" "os" "github.com/fanux/sealos/pkg/utils/iputils" @@ -75,7 +76,7 @@ func CleanCmdFunc(cmd *cobra.Command, args []string) { deleteNodes := iputils.ParseIPs(v1.NodeIPs) deleteMasters := iputils.ParseIPs(v1.MasterIPs) c := &v1.SealConfig{} - if err := c.Load(cfgFile); err != nil { + if err := c.Load(boot.ConfigFilePath); err != nil { logger.Error(err) os.Exit(-1) } @@ -92,7 +93,7 @@ func CleanCmdFunc(cmd *cobra.Command, args []string) { } install.BuildClean(deleteNodes, deleteMasters) - c.Dump(cfgFile) + c.Dump(boot.ConfigFilePath) } // IsExistNodes diff --git a/cmd/config.go b/cmd/sealos/cmd/config.go similarity index 100% rename from cmd/config.go rename to cmd/sealos/cmd/config.go diff --git a/cmd/etcd.go b/cmd/sealos/cmd/etcd.go similarity index 96% rename from cmd/etcd.go rename to cmd/sealos/cmd/etcd.go index 60e70e3c9..508a5b590 100644 --- a/cmd/etcd.go +++ b/cmd/sealos/cmd/etcd.go @@ -16,6 +16,7 @@ package cmd import ( "fmt" + "github.com/fanux/sealos/cmd/sealos/boot" "os" "time" @@ -112,16 +113,16 @@ func NewEtcdHealthCommand() *cobra.Command { } func EtcdSaveCmdFunc(cmd *cobra.Command, args []string) { - e := install.GetEtcdBackFlags(cfgFile) + e := install.GetEtcdBackFlags(boot.ConfigFilePath) if err := e.Save(v1.IsK8sMaster); err == nil { - e.Dump(cfgFile) + e.Dump(boot.ConfigFilePath) } logger.Info("Finished saving/uploading snapshot [%s]", e.Name) e.HealthCheck() } func EtcdRestoreCmdFunc(cmd *cobra.Command, args []string) { - e := install.GetRestoreFlags(cfgFile) + e := install.GetRestoreFlags(boot.ConfigFilePath) // restore need interactive to confirm if !force { prompt := fmt.Sprintf("Are you sure to exec restore cmd will stop your kubernetes cluster immediately and restore etcd from your backup %s file (y/n)?", e.Name) @@ -159,6 +160,6 @@ func EtcdRestoreCmdFunc(cmd *cobra.Command, args []string) { } func EtcdHealthCmdFunc(cmd *cobra.Command, args []string) { - e := install.GetHealthFlag(cfgFile) + e := install.GetHealthFlag(boot.ConfigFilePath) e.HealthCheck() } diff --git a/cmd/exec.go b/cmd/sealos/cmd/exec.go similarity index 95% rename from cmd/exec.go rename to cmd/sealos/cmd/exec.go index 0d5b5ddab..c87dd0218 100644 --- a/cmd/exec.go +++ b/cmd/sealos/cmd/exec.go @@ -15,6 +15,7 @@ package cmd import ( + "github.com/fanux/sealos/cmd/sealos/boot" "github.com/fanux/sealos/pkg/install" "github.com/spf13/cobra" ) @@ -48,7 +49,7 @@ func init() { } func ExecCmdFunc(cmd *cobra.Command, args []string) { - e := install.GetExecFlag(cfgFile) + e := install.GetExecFlag(boot.ConfigFilePath) if e.IsUseCopy() { e.Copy() } diff --git a/cmd/init.go b/cmd/sealos/cmd/init.go similarity index 94% rename from cmd/init.go rename to cmd/sealos/cmd/init.go index 35c195e6b..43ec97e2b 100644 --- a/cmd/init.go +++ b/cmd/sealos/cmd/init.go @@ -15,6 +15,7 @@ package cmd import ( + "github.com/fanux/sealos/cmd/sealos/boot" "os" "github.com/fanux/sealos/pkg/utils/file" @@ -84,23 +85,23 @@ var initCmd = &cobra.Command{ c := &v1.SealConfig{} // 没有重大错误可以直接保存配置. 但是apiservercertsans为空. 但是不影响用户 clean // 如果用户指定了配置文件,并不使用--master, 这里就不dump, 需要使用load获取配置文件了. - if cfgFile != "" && len(v1.MasterIPs) == 0 { - err := c.Load(cfgFile) + if boot.ConfigFilePath != "" && len(v1.MasterIPs) == 0 { + err := c.Load(boot.ConfigFilePath) if err != nil { - logger.Error("load cfgFile %s err: %q", cfgFile, err) + logger.Error("load boot.ConfigFilePath %s err: %q", boot.ConfigFilePath, err) os.Exit(1) } } else { - c.Dump(cfgFile) + c.Dump(boot.ConfigFilePath) } install.BuildInit() // 安装完成后生成完整版 - c.Dump(cfgFile) + c.Dump(boot.ConfigFilePath) logger.Info(contact) }, PreRun: func(cmd *cobra.Command, args []string) { - // 使用了cfgFile 就不进行preRun了 - if cfgFile == "" && install.ExitInitCase() { + // 使用了boot.ConfigFilePath 就不进行preRun了 + if boot.ConfigFilePath == "" && install.ExitInitCase() { _ = cmd.Help() os.Exit(install.ErrorExitOSCase) } diff --git a/cmd/join.go b/cmd/sealos/cmd/join.go similarity index 94% rename from cmd/join.go rename to cmd/sealos/cmd/join.go index d178bed44..c39636af5 100644 --- a/cmd/join.go +++ b/cmd/sealos/cmd/join.go @@ -15,6 +15,7 @@ package cmd import ( + "github.com/fanux/sealos/cmd/sealos/boot" "os" "github.com/fanux/sealos/pkg/utils/iputils" @@ -53,7 +54,7 @@ func JoinCmdFunc(cmd *cobra.Command, args []string) { beforeMasters := iputils.ParseIPs(v1.MasterIPs) c := &v1.SealConfig{} - if err := c.Load(cfgFile); err != nil { + if err := c.Load(boot.ConfigFilePath); err != nil { logger.Error(err) c.ShowDefaultConfig() os.Exit(0) @@ -68,5 +69,5 @@ func JoinCmdFunc(cmd *cobra.Command, args []string) { } install.BuildJoin(beforeMasters, beforeNodes) - c.Dump(cfgFile) + c.Dump(boot.ConfigFilePath) } diff --git a/cmd/root.go b/cmd/sealos/cmd/root.go similarity index 55% rename from cmd/root.go rename to cmd/sealos/cmd/root.go index 889565fd2..7ca65ca29 100644 --- a/cmd/root.go +++ b/cmd/sealos/cmd/root.go @@ -16,22 +16,13 @@ package cmd import ( "fmt" + "github.com/fanux/sealos/cmd/sealos/boot" + "github.com/fanux/sealos/pkg/types/contants" "os" - "github.com/fanux/sealos/pkg/utils/file" - - "github.com/fanux/sealos/pkg/utils/logger" - - v1 "github.com/fanux/sealos/pkg/types/v1alpha1" "github.com/spf13/cobra" ) -var ( - cfgFile string - Info bool - feature bool -) - // rootCmd represents the base command when called without any subcommands var rootCmd = &cobra.Command{ Use: "sealos", @@ -51,22 +42,9 @@ func Execute() { } func init() { - cobra.OnInitialize(initConfig) - rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.sealos/config.yaml)") - rootCmd.PersistentFlags().BoolVar(&feature, "feature", false, "need use new feature. ex app feature") - rootCmd.PersistentFlags().BoolVar(&Info, "info", false, "logger ture for Info, false for Debug") -} + cobra.OnInitialize(boot.OnBootOnDie) + rootCmd.PersistentFlags().StringVar(&boot.ConfigDir, "config-dir", contants.DefaultConfigPath, "config dir (default is $HOME/.sealos)") + rootCmd.PersistentFlags().StringVar(&boot.ConfigFile, "config-file", contants.DefaultConfigFile, "config file (default is /config.yaml )") -// initConfig reads in config file and ENV variables if set. -func initConfig() { - // Find home directory. - configPath := v1.DefaultConfigPath - logFile := fmt.Sprintf("%s/sealos.log", configPath) - if !file.IsExist(configPath) { - err := os.MkdirAll(configPath, os.ModePerm) - if err != nil { - fmt.Println("create default sealos config dir failed, please create it by your self mkdir -p /root/.sealos && touch /root/.sealos/config.yaml") - } - } - logger.Cfg(!Info, logFile) + rootCmd.PersistentFlags().BoolVar(&boot.Debug, "debug", false, "enable debug logger") } diff --git a/cmd/upgrade.go b/cmd/sealos/cmd/upgrade.go similarity index 92% rename from cmd/upgrade.go rename to cmd/sealos/cmd/upgrade.go index 4071e1bf2..680aea293 100644 --- a/cmd/upgrade.go +++ b/cmd/sealos/cmd/upgrade.go @@ -15,6 +15,7 @@ package cmd import ( + "github.com/fanux/sealos/cmd/sealos/boot" "os" "github.com/fanux/sealos/pkg/utils/confirm" @@ -63,11 +64,11 @@ func UpgradeCmdFunc(cmd *cobra.Command, args []string) { } u := install.NewUpgrade(newVersion, newPkgURL) u.SetUP() - u.Dump(cfgFile) + u.Dump(boot.ConfigFilePath) } func PreRunUpgradeCmdFunc(cmd *cobra.Command, args []string) { - if err := install.ExitUpgradeCase(newVersion, newPkgURL, cfgFile); err != nil { + if err := install.ExitUpgradeCase(newVersion, newPkgURL, boot.ConfigFilePath); err != nil { logger.Error("PreRun error: ", err) os.Exit(1) } diff --git a/cmd/sealos/cmd/version.go b/cmd/sealos/cmd/version.go new file mode 100644 index 000000000..f38e2a7d8 --- /dev/null +++ b/cmd/sealos/cmd/version.go @@ -0,0 +1,50 @@ +// Copyright © 2021 sealos. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "encoding/json" + "fmt" + + "github.com/fanux/sealos/pkg/version" + "github.com/spf13/cobra" +) + +var shortPrint bool + +var versionCmd = &cobra.Command{ + Use: "version", + Short: "version", + Args: cobra.NoArgs, + Example: `sealos version`, + RunE: func(cmd *cobra.Command, args []string) error { + marshalled, err := json.Marshal(version.Get()) + if err != nil { + return err + } + if shortPrint { + fmt.Println(version.Get().String()) + } else { + fmt.Println(string(marshalled)) + } + return nil + + }, +} + +func init() { + rootCmd.AddCommand(versionCmd) + versionCmd.Flags().BoolVar(&shortPrint, "short", false, "if true, print just the version number.") +} diff --git a/main.go b/cmd/sealos/main.go similarity index 93% rename from main.go rename to cmd/sealos/main.go index 161f2c20f..c0b32c253 100644 --- a/main.go +++ b/cmd/sealos/main.go @@ -15,7 +15,7 @@ package main import ( - "github.com/fanux/sealos/cmd" + "github.com/fanux/sealos/cmd/sealos/cmd" ) func main() { diff --git a/go.mod b/go.mod index 0b9044c96..2de831492 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.15 require ( github.com/aliyun/alibaba-cloud-sdk-go v1.61.985 + github.com/emirpasic/gods v1.12.0 github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.0.72 github.com/mitchellh/go-homedir v1.1.0 github.com/onsi/ginkgo v1.11.0 diff --git a/go.sum b/go.sum index ffe19bed0..77c0ee217 100644 --- a/go.sum +++ b/go.sum @@ -113,6 +113,8 @@ github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4 github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg= +github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= diff --git a/pkg/client/clientset.go b/pkg/client/clientset.go new file mode 100644 index 000000000..00df700d4 --- /dev/null +++ b/pkg/client/clientset.go @@ -0,0 +1,60 @@ +/* +Copyright 2022 cuisongliu@qq.com. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package client + +import ( + "github.com/fanux/sealos/pkg/client/typed/apps/v1beta1" + "k8s.io/client-go/rest" +) + +var _ Interface = &Clientset{} + +type Interface interface { + AppsV1beta1() v1beta1.AppsV1Beta1Interface +} + +// Clientset contains the clients for groups. Each group has exactly one +// version included in a Clientset. +type Clientset struct { + appsv1beta1 *v1beta1.AppsV1Beta1Client +} + +// AppsV1beta1 retrieves the AppsV1Beta1Client +func (c *Clientset) AppsV1beta1() v1beta1.AppsV1Beta1Interface { + return c.appsv1beta1 +} + +// NewForConfig creates a new Clientset for the given config. +// If config's RateLimiter is not set and QPS and Burst are acceptable, +// NewForConfig will generate a rate-limiter in configShallowCopy. +func NewForConfig(c *rest.Config) (*Clientset, error) { + var cs Clientset + var err error + cs.appsv1beta1, err = v1beta1.NewForConfig() + if err != nil { + return nil, err + } + return &cs, nil +} + +// NewForConfigOrDie creates a new Clientset for the given config and +// panics if there is an error in the config. +func NewForConfigOrDie(c *rest.Config) *Clientset { + var cs Clientset + cs.appsv1beta1 = v1beta1.NewForConfigOrDie() + return &cs +} diff --git a/pkg/client/typed/apps/v1beta1/apps_client.go b/pkg/client/typed/apps/v1beta1/apps_client.go new file mode 100644 index 000000000..37be21fc7 --- /dev/null +++ b/pkg/client/typed/apps/v1beta1/apps_client.go @@ -0,0 +1,46 @@ +/* +Copyright 2022 cuisongliu@qq.com. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1beta1 + +var _ AppsV1Beta1Interface = &AppsV1Beta1Client{} + +type AppsV1Beta1Interface interface { + ConfigsGetter +} + +// AppsV1Beta1Client is used to interact with features provided by the apps.sealyun.com group. +type AppsV1Beta1Client struct { +} + +func (c *AppsV1Beta1Client) Configs() ConfigInterface { + return newConfigs(c) +} + +// NewForConfig creates a new AppsV1Beta1Client for the given config. +func NewForConfig() (*AppsV1Beta1Client, error) { + return &AppsV1Beta1Client{}, nil +} + +// NewForConfigOrDie creates a new AppsV1Beta1Client for the given config and +// panics if there is an error in the config. +func NewForConfigOrDie() *AppsV1Beta1Client { + client, err := NewForConfig() + if err != nil { + panic(err) + } + return client +} diff --git a/pkg/client/typed/apps/v1beta1/config.go b/pkg/client/typed/apps/v1beta1/config.go new file mode 100644 index 000000000..adb47838f --- /dev/null +++ b/pkg/client/typed/apps/v1beta1/config.go @@ -0,0 +1,58 @@ +/* +Copyright 2022 cuisongliu@qq.com. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1beta1 + +import ( + "context" + v1 "github.com/fanux/sealos/pkg/types/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +var _ ConfigInterface = &configs{} + + +// ConfigInterface has methods to work with Config resources. +type ConfigInterface interface { + Create(ctx context.Context, deployment *v1.Config, opts metav1.CreateOptions) (*v1.Config, error) + Update(ctx context.Context, deployment *v1.Config, opts metav1.UpdateOptions) (*v1.Config, error) + Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error + Get(ctx context.Context, name string, opts metav1.GetOptions) (*v1.Config, error) +} + +type ConfigsGetter interface { + Configs() ConfigInterface +} + +type configs struct { +} + +func newConfigs(c *AppsV1Beta1Client) *configs { + return &configs{} +} + +func (*configs) Create(ctx context.Context, config *v1.Config, opts metav1.CreateOptions) (*v1.Config, error) { + return nil, nil +} +func (*configs) Update(ctx context.Context, config *v1.Config, opts metav1.UpdateOptions) (*v1.Config, error) { + return nil, nil +} +func (*configs) Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error { + return nil +} +func (*configs) Get(ctx context.Context, name string, opts metav1.GetOptions) (*v1.Config, error) { + return nil, nil +} diff --git a/pkg/utils/kubernetes/join/certificate_key.go b/pkg/client/typed/apps/v1beta1/doc.go similarity index 97% rename from pkg/utils/kubernetes/join/certificate_key.go rename to pkg/client/typed/apps/v1beta1/doc.go index 45342e7c5..1806e0c0b 100644 --- a/pkg/utils/kubernetes/join/certificate_key.go +++ b/pkg/client/typed/apps/v1beta1/doc.go @@ -12,4 +12,4 @@ // See the License for the specific language governing permissions and // limitations under the License. -package join +package v1beta1 diff --git a/pkg/utils/cni/calico.go b/pkg/cni/calico.go similarity index 91% rename from pkg/utils/cni/calico.go rename to pkg/cni/calico.go index b95a2e4db..315203ee7 100644 --- a/pkg/utils/cni/calico.go +++ b/pkg/cni/calico.go @@ -28,9 +28,7 @@ func (c Calico) Manifests(template string) string { if c.metadata.CIDR == "" { c.metadata.CIDR = defaultCIDR } - if c.metadata.CniRepo == "" || c.metadata.CniRepo == defaultCNIRepo { - c.metadata.CniRepo = "calico" - } + c.metadata.CniRepo = "calico" if c.metadata.Version == "" { c.metadata.Version = "v3.8.2" } diff --git a/pkg/utils/cni/calico_yaml.go b/pkg/cni/calico_yaml.go similarity index 100% rename from pkg/utils/cni/calico_yaml.go rename to pkg/cni/calico_yaml.go diff --git a/pkg/utils/cni/net.go b/pkg/cni/net.go similarity index 97% rename from pkg/utils/cni/net.go rename to pkg/cni/net.go index 93705c523..7bc358250 100644 --- a/pkg/utils/cni/net.go +++ b/pkg/cni/net.go @@ -22,7 +22,6 @@ import ( const ( defaultInterface = "eth.*|en.*" defaultCIDR = "100.64.0.0/10" - defaultCNIRepo = "k8s.gcr.io" ) type MetaData struct { diff --git a/pkg/utils/cni/net_test.go b/pkg/cni/net_test.go similarity index 100% rename from pkg/utils/cni/net_test.go rename to pkg/cni/net_test.go diff --git a/pkg/utils/hosts/hosts.go b/pkg/hosts/hosts.go similarity index 59% rename from pkg/utils/hosts/hosts.go rename to pkg/hosts/hosts.go index 521782067..562b54413 100644 --- a/pkg/utils/hosts/hosts.go +++ b/pkg/hosts/hosts.go @@ -19,57 +19,35 @@ import ( "errors" "fmt" "io" + "net" "os" "strings" + "github.com/emirpasic/gods/maps/linkedhashmap" strings2 "github.com/fanux/sealos/pkg/utils/strings" "github.com/fanux/sealos/pkg/utils/file" - "github.com/fanux/sealos/pkg/utils/iputils" - "github.com/fanux/sealos/pkg/utils/logger" ) type HostFile struct { - Path string - Hosts map[string]*Hostname + Path string } -type Hostname struct { +type hostname struct { Comment string Domain string IP string } -func NewHostname(comment string, domain string, ip string) *Hostname { - return &Hostname{comment, domain, ip} -} -func (h *HostFile) Add(host *Hostname) { - if h.Hosts == nil { - h.Hosts = make(map[string]*Hostname) - } - h.Hosts[host.Domain] = host +func newHostname(comment string, domain string, ip string) *hostname { + return &hostname{comment, domain, ip} } -func (h *HostFile) Delete(host string) { - delete(h.Hosts, host) -} - -func getHostPath() string { - path := os.Getenv("GOHOST_FILE") - if path == "" { - path = "/etc/hosts" - } - return path -} - -func (h *Hostname) toString() string { - if len(h.Comment) > 0 { - h.Comment += "\n" - } +func (h *hostname) toString() string { return h.Comment + h.IP + " " + h.Domain + "\n" } -func appendToFile(filePath string, hostname *Hostname) { +func appendToFile(filePath string, hostname *hostname) { fp, err := os.OpenFile(filePath, os.O_WRONLY|os.O_APPEND, 0644) if err != nil { logger.Warn("failed opening file %s : %s\n", filePath, err) @@ -84,7 +62,7 @@ func appendToFile(filePath string, hostname *Hostname) { } } -func (h *HostFile) ParseHostFile(path string) (map[string]*Hostname, error) { +func (h *HostFile) ParseHostFile(path string) (*linkedhashmap.Map, error) { if !file.IsExist(path) { logger.Warn("path %s is not exists", path) return nil, errors.New("path %s is not exists") @@ -98,7 +76,7 @@ func (h *HostFile) ParseHostFile(path string) (map[string]*Hostname, error) { defer fp.Close() br := bufio.NewReader(fp) - hostnameMap := make(map[string]*Hostname) + lm := linkedhashmap.New() curComment := "" for { str, rErr := br.ReadString('\n') @@ -115,22 +93,22 @@ func (h *HostFile) ParseHostFile(path string) (map[string]*Hostname, error) { continue } tmpHostnameArr := strings.Fields(str) - curDomain := strings2.TrimSpaceWS(tmpHostnameArr[1]) - if !iputils.CheckDomain(curDomain) { - return hostnameMap, errors.New(" file contain error domain" + curDomain) - } + curDomain := strings.Join(tmpHostnameArr[1:]," ") + //if !iputils.CheckDomain(curDomain) { + // return lm, errors.New(" file contain error domain" + curDomain) + //} curIP := strings2.TrimSpaceWS(tmpHostnameArr[0]) - checkIP := iputils.CheckIP(curIP) - if !checkIP { - return hostnameMap, nil - } - tmpHostname := NewHostname(curComment, curDomain, curIP) - hostnameMap[tmpHostname.Domain] = tmpHostname + checkIP := net.ParseIP(curIP) + if checkIP == nil { + continue + } + tmpHostname := newHostname(curComment, curDomain, curIP) + lm.Put(tmpHostname.Domain, tmpHostname) curComment = "" } - return hostnameMap, nil + return lm, nil } func (h *HostFile) AppendHost(domain string, ip string) { @@ -138,11 +116,11 @@ func (h *HostFile) AppendHost(domain string, ip string) { return } - hostname := NewHostname("", domain, ip) - appendToFile(getHostPath(), hostname) + hostname := newHostname("", domain, ip) + appendToFile(h.Path, hostname) } -func (h *HostFile) writeToFile(hostnameMap map[string]*Hostname, path string) { +func (h *HostFile) writeToFile(hostnameMap *linkedhashmap.Map, path string) { if !file.IsExist(path) { logger.Warn("path %s is not exists", path) return @@ -155,13 +133,16 @@ func (h *HostFile) writeToFile(hostnameMap map[string]*Hostname, path string) { } defer fp.Close() - for _, mapVal := range hostnameMap { - _, writeErr := fp.WriteString(mapVal.toString()) - if writeErr != nil { - logger.Warn(writeErr) - return + hostnameMap.Each(func(key interface{}, value interface{}) { + if v, ok := value.(*hostname); ok { + _, writeErr := fp.WriteString(fmt.Sprintf("%s", v.toString())) + if writeErr != nil { + logger.Warn(writeErr) + return + } } - } + + }) } func (h *HostFile) DeleteDomain(domain string) { @@ -169,32 +150,33 @@ func (h *HostFile) DeleteDomain(domain string) { return } - currHostsMap, parseErr := h.ParseHostFile(getHostPath()) + currHostsMap, parseErr := h.ParseHostFile(h.Path) if parseErr != nil { logger.Warn("parse file failed" + parseErr.Error()) return } - - if len(currHostsMap) == 0 || currHostsMap[domain] == nil { + _, found := currHostsMap.Get(domain) + if currHostsMap == nil || !found { logger.Warn("domain %s not exist\n", domain) return } - - delete(currHostsMap, domain) - h.writeToFile(currHostsMap, getHostPath()) + currHostsMap.Remove(domain) + h.writeToFile(currHostsMap, h.Path) } func (h *HostFile) ListCurrentHosts() { - currHostsMap, parseErr := h.ParseHostFile(getHostPath()) + currHostsMap, parseErr := h.ParseHostFile(h.Path) if parseErr != nil { logger.Warn("parse file failed" + parseErr.Error()) return } - if len(currHostsMap) == 0 { + if currHostsMap == nil { return } + currHostsMap.Each(func(key interface{}, value interface{}) { + if v, ok := value.(*hostname); ok { + fmt.Print(fmt.Sprintf("%s", v.toString())) + } - for _, mapVal := range currHostsMap { - fmt.Println(mapVal.toString()) - } + }) } diff --git a/pkg/infra/aliyun/ali_provider.go b/pkg/infra/aliyun/ali_provider.go index 276e3ba41..f1b297d7f 100644 --- a/pkg/infra/aliyun/ali_provider.go +++ b/pkg/infra/aliyun/ali_provider.go @@ -248,7 +248,7 @@ func (a *AliProvider) Reconcile() error { } func (a *AliProvider) Apply() error { - if err := v1beta1.Default(a.Infra, DefaultInfra); err != nil { + if err := v1beta1.DefaultCluster(a.Infra, DefaultInfra); err != nil { return err } if err := validation.ValidateInfra(a.Infra, DefaultValidate); len(err) != 0 { diff --git a/pkg/infra/infra.go b/pkg/infra/infra.go index dda10426f..e56cb135b 100644 --- a/pkg/infra/infra.go +++ b/pkg/infra/infra.go @@ -57,7 +57,7 @@ func newAliProvider(infra *v2.Infra) (Interface, error) { func newHwProvider(infra *v2.Infra) (Interface, error) { hwProvider := new(huawei.HwProvider) hwProvider.Infra = infra - if err := v2.Default(hwProvider.Infra, huawei.DefaultInfra); err != nil { + if err := v2.DefaultCluster(hwProvider.Infra, huawei.DefaultInfra); err != nil { return nil, err } if err := validation.ValidateInfra(hwProvider.Infra, huawei.DefaultValidate); len(err) != 0 { diff --git a/pkg/install/check.go b/pkg/install/check.go index 744da149e..9acc9ce03 100644 --- a/pkg/install/check.go +++ b/pkg/install/check.go @@ -55,7 +55,7 @@ func (s *SealosInstaller) CheckValid() { dict := make(map[string]bool) var errList []string for _, h := range s.Hosts { - hostname := v1.SSHConfig.CmdToString(h, "hostname", "") //获取主机名 + hostname,_ := v1.SSHConfig.CmdToString(h, "hostname", "") //获取主机名 if hostname == "" { logger.Error("[%s] ------------ check error", h) os.Exit(1) @@ -84,7 +84,7 @@ func (s *SealosInstaller) CheckValid() { if versionutil.For120(v1.Version) { // for containerd. if docker exist ; exit frist. - dockerExist := v1.SSHConfig.CmdToString(h, "command -v dockerd &> /dev/null && echo yes || :", "") + dockerExist,_ := v1.SSHConfig.CmdToString(h, "command -v dockerd &> /dev/null && echo yes || :", "") if dockerExist == "yes" { errList = append(errList, h) } diff --git a/pkg/install/clean.go b/pkg/install/clean.go index cf0a6a870..9478ef389 100644 --- a/pkg/install/clean.go +++ b/pkg/install/clean.go @@ -16,6 +16,7 @@ package install import ( "fmt" + "github.com/fanux/sealos/pkg/types/contants" "os" "strings" "sync" @@ -28,10 +29,8 @@ import ( "github.com/fanux/sealos/pkg/utils/logger" - v1 "github.com/fanux/sealos/pkg/types/v1alpha1" - "github.com/fanux/sealos/pkg/utils/ssh" - "github.com/fanux/sealos/pkg/ipvs" + v1 "github.com/fanux/sealos/pkg/types/v1alpha1" ) type SealosClean struct { @@ -110,7 +109,7 @@ end: i.Clean() if i.cleanAll { logger.Info("if clean all and clean sealos config") - cfgPath := v1.DefaultConfigPath + cfgPath := contants.DefaultConfigPath _, _ = exec.RunSimpleCmd("rm -rf " + cfgPath) } } @@ -155,9 +154,10 @@ func (s *SealosClean) cleanNode(node string) { if !s.cleanAll { logger.Debug("clean node in master") if len(v1.MasterIPs) > 0 { - hostname := ssh.HostName(v1.SSHConfig, v1.MasterIPs[0], node) + //TODO 判断master上是否已经存在这个host + //hostname := ssh.HostName(v1.SSHConfig, v1.MasterIPs[0], node) cmd := "kubectl delete node %s" - _ = v1.SSHConfig.CmdAsync(v1.MasterIPs[0], fmt.Sprintf(cmd, strings.TrimSpace(hostname))) + _ = v1.SSHConfig.CmdAsync(v1.MasterIPs[0], fmt.Sprintf(cmd, strings.TrimSpace("hostname"))) } } } @@ -169,12 +169,12 @@ func (s *SealosClean) cleanMaster(master string) { if !s.cleanAll { logger.Debug("clean node in master") if len(v1.MasterIPs) > 0 { - hostname := ssh.HostName(v1.SSHConfig, v1.MasterIPs[0], master) + //hostname := ssh.HostName(v1.SSHConfig, v1.MasterIPs[0], master) cmd := "kubectl delete node %s" - _ = v1.SSHConfig.CmdAsync(v1.MasterIPs[0], fmt.Sprintf(cmd, strings.TrimSpace(hostname))) + _ = v1.SSHConfig.CmdAsync(v1.MasterIPs[0], fmt.Sprintf(cmd, strings.TrimSpace("hostname"))) } //清空所有的nodes的数据 - yaml := ipvs.LvsStaticPodYaml(v1.VIP, v1.MasterIPs, v1.LvscareImage) + yaml,_ := ipvs.LvsStaticPodYaml(v1.VIP, v1.MasterIPs, v1.LvscareImage, contants.LvsCareStaticPodName) var wg sync.WaitGroup for _, node := range v1.NodeIPs { wg.Add(1) @@ -220,7 +220,7 @@ func clean(host string) { func cleanRoute(node string) { // clean route cmdRoute := fmt.Sprintf("sealos route --host %s", iputils.IPFormat(node)) - status := v1.SSHConfig.CmdToString(node, cmdRoute, "") + status, _ := v1.SSHConfig.CmdToString(node, cmdRoute, "") if status != "ok" { // 删除为 vip创建的路由。 delRouteCmd := fmt.Sprintf("sealos route del --host %s --gateway %s", v1.VIP, iputils.IPFormat(node)) diff --git a/pkg/install/etcd_restore.go b/pkg/install/etcd_restore.go index 8cb203b5a..e962e0c07 100644 --- a/pkg/install/etcd_restore.go +++ b/pkg/install/etcd_restore.go @@ -27,7 +27,6 @@ import ( "github.com/fanux/sealos/pkg/utils/logger" v1 "github.com/fanux/sealos/pkg/types/v1alpha1" - "github.com/fanux/sealos/pkg/utils/ssh" "go.etcd.io/etcd/etcdutl/v3/snapshot" "go.uber.org/zap" @@ -104,7 +103,7 @@ func (e *EtcdFlags) StopPod() (string, error) { // RestoreAll is restore all ETCD nodes func (e *EtcdFlags) RestoreAll() { for _, host := range e.EtcdHosts { - hostname := v1.SSHConfig.CmdToString(host, "hostname", "") + hostname, _ := v1.SSHConfig.CmdToString(host, "hostname", "") // remove first cmd := fmt.Sprintf("rm -rf %s-%s", e.RestoreDir, hostname) _ = CmdWork(host, cmd, TMPDIR) @@ -147,7 +146,7 @@ func (e *EtcdFlags) restore(hostname, host string) { func (e *EtcdFlags) AfterRestore() error { // first to mv every for _, host := range e.EtcdHosts { - hostname := v1.SSHConfig.CmdToString(host, "hostname", "") + hostname, _ := v1.SSHConfig.CmdToString(host, "hostname", "") // /opt/sealos/etcd-restore-dev-k8s-master location := fmt.Sprintf("%s-%s", e.RestoreDir, hostname) // @@ -162,7 +161,8 @@ func (e *EtcdFlags) AfterRestore() error { // 复制并解压到相应目录 // use quiet to tar AfterHook := fmt.Sprintf(`tar xf %s -C /var/lib/ && mv /var/lib/%s %s && rm -rf %s`, sdtTmpTar, filepath.Base(location), ETCDDATADIR, sdtTmpTar) - ssh.CopyFiles(v1.SSHConfig, tmpFile, []string{host}, "/var/lib", nil, &AfterHook) + v1.SSHConfig.Copy(host, tmpFile, "/var/lib") + v1.SSHConfig.Cmd(host, AfterHook) //logger.Info("send etcd.zip to hosts") } @@ -208,7 +208,7 @@ func (e *EtcdFlags) RecoveryKuBeCluster(dir string) { func GetEtcdInitialCluster(hosts []string) string { initialCluster := "" for i, host := range hosts { - hostname := v1.SSHConfig.CmdToString(host, "hostname", "") + hostname,_ := v1.SSHConfig.CmdToString(host, "hostname", "") ip := reFormatHostToIP(host) initialCluster += fmt.Sprintf("etcd-%s=https://%s:2380", hostname, ip) if i < (len(hosts) - 1) { diff --git a/pkg/install/etcd_save.go b/pkg/install/etcd_save.go index 92e9aa020..cfe889ad1 100644 --- a/pkg/install/etcd_save.go +++ b/pkg/install/etcd_save.go @@ -31,8 +31,6 @@ import ( "github.com/fanux/sealos/pkg/utils/logger" v1 "github.com/fanux/sealos/pkg/types/v1alpha1" - "github.com/fanux/sealos/pkg/utils/ssh" - clientv3 "go.etcd.io/etcd/client/v3" "go.etcd.io/etcd/client/v3/snapshot" "go.uber.org/zap" @@ -117,7 +115,10 @@ func (e *EtcdFlags) Save(isK8sMaster bool) error { // 如果不存在, 说明在docker容器或者sealos执行的时候, 不在master0上 if isK8sMaster { // 复制本机的snapshot 到 各master节点 上。 - ssh.CopyFiles(v1.SSHConfig, e.LongName, e.EtcdHosts, e.BackDir, nil, nil) + for _, p := range e.EtcdHosts { + v1.SSHConfig.Copy(p, e.LongName, e.BackDir) + } + } logger.Info("Finished saving snapshot [%s]", e.Name) return nil diff --git a/pkg/install/exec.go b/pkg/install/exec.go index b4cc6b28b..8f3f54a4d 100644 --- a/pkg/install/exec.go +++ b/pkg/install/exec.go @@ -137,7 +137,7 @@ func (e *ExecFlag) copyByNodeIP() { logger.Info("[%s] is exist on remote host [%s]. skip...", e.Dst, node) return } - v1.SSHConfig.CopyLocalToRemote(node, e.Src, e.Dst) + v1.SSHConfig.Copy(node, e.Src, e.Dst) }(n) } wg.Wait() diff --git a/pkg/install/init.go b/pkg/install/init.go index 5c76bb279..94fe055f1 100644 --- a/pkg/install/init.go +++ b/pkg/install/init.go @@ -18,6 +18,8 @@ import ( "bufio" "encoding/json" "fmt" + "github.com/fanux/sealos/pkg/cni" + "github.com/fanux/sealos/pkg/types/contants" "io" "io/ioutil" "os" @@ -32,10 +34,7 @@ import ( cert2 "github.com/fanux/sealos/pkg/utils/kubernetes/cert" "github.com/fanux/sealos/pkg/utils/logger" - "github.com/fanux/sealos/pkg/utils/cni" - v1 "github.com/fanux/sealos/pkg/types/v1alpha1" - "github.com/fanux/sealos/pkg/utils/ssh" ) //BuildInit is @@ -82,10 +81,10 @@ func (s *SealosInstaller) getCgroupDriverFromShell(h string) string { var output string if versionutil.For120(v1.Version) { cmd := ContainerdShell - output = v1.SSHConfig.CmdToString(h, cmd, " ") + output,_ = v1.SSHConfig.CmdToString(h, cmd, " ") } else { cmd := DockerShell - output = v1.SSHConfig.CmdToString(h, cmd, " ") + output,_ = v1.SSHConfig.CmdToString(h, cmd, " ") } output = strings.TrimSpace(output) logger.Info("cgroup driver is %s", output) @@ -163,8 +162,8 @@ func (s *SealosInstaller) appendAPIServer() error { func (s *SealosInstaller) GenerateCert() { //cert generator in sealos - hostname := ssh.RemoteHostName(v1.SSHConfig, s.Masters[0]) - GenerateCert(v1.CertPath, v1.CertEtcdPath, v1.APIServerCertSANs, iputils.IPFormat(s.Masters[0]), hostname, v1.SvcCIDR, v1.DNSDomain) + //hostname := ssh.RemoteHostName(v1.SSHConfig, s.Masters[0]) + GenerateCert(v1.CertPath, v1.CertEtcdPath, v1.APIServerCertSANs, iputils.IPFormat(s.Masters[0]), "hostname", v1.SvcCIDR, v1.DNSDomain) //copy all cert to master0 //CertSA(kye,pub) + CertCA(key,crt) //s.sendNewCertAndKey(s.Masters) @@ -172,7 +171,7 @@ func (s *SealosInstaller) GenerateCert() { } func (s *SealosInstaller) CreateKubeconfig() { - hostname := ssh.RemoteHostName(v1.SSHConfig, s.Masters[0]) + hostname,_ := v1.SSHConfig.CmdToString( s.Masters[0],"","") certConfig := cert2.Config{ Path: v1.CertPath, @@ -181,7 +180,7 @@ func (s *SealosInstaller) CreateKubeconfig() { controlPlaneEndpoint := fmt.Sprintf("https://%s:6443", v1.APIServer) - err := cert2.CreateJoinControlPlaneKubeConfigFiles(v1.DefaultConfigPath, + err := cert2.CreateJoinControlPlaneKubeConfigFiles(contants.DefaultConfigPath, certConfig, hostname, controlPlaneEndpoint, "kubernetes") if err != nil { logger.Error("generator kubeconfig failed %s", err) @@ -205,7 +204,7 @@ func (s *SealosInstaller) InstallMaster0() { cmd = s.Command(v1.Version, InitMaster) - output := v1.SSHConfig.Cmd(s.Masters[0], cmd) + output,_ := v1.SSHConfig.Cmd(s.Masters[0], cmd) if output == nil { logger.Error("[%s] install kubernetes failed. please clean and uninstall.", s.Masters[0]) os.Exit(1) @@ -230,7 +229,7 @@ func (s *SealosInstaller) InstallMaster0() { if v1.SSHConfig.IsFileExist(s.Masters[0], "/root/kube/Metadata") { var metajson string var tmpdata v1.Metadata - metajson = v1.SSHConfig.CmdToString(s.Masters[0], "cat /root/kube/Metadata", "") + metajson,_ = v1.SSHConfig.CmdToString(s.Masters[0], "cat /root/kube/Metadata", "") err := json.Unmarshal([]byte(metajson), &tmpdata) if err != nil { logger.Warn("get metadata version err: ", err) @@ -246,7 +245,7 @@ func (s *SealosInstaller) InstallMaster0() { CniRepo: v1.Repo, Version: cniVersion, }).Manifests("") - configYamlPath := filepath.Join(v1.DefaultConfigPath, "cni.yaml") + configYamlPath := filepath.Join(contants.DefaultConfigPath, "cni.yaml") logger.Debug("cni yaml path is : ", configYamlPath) _ = ioutil.WriteFile(configYamlPath, []byte(netyaml), 0755) v1.SSHConfig.Copy(s.Masters[0], configYamlPath, "/tmp/cni.yaml") diff --git a/pkg/install/join.go b/pkg/install/join.go index b97255266..47dc06647 100644 --- a/pkg/install/join.go +++ b/pkg/install/join.go @@ -22,13 +22,9 @@ import ( "github.com/fanux/sealos/pkg/utils/iputils" "github.com/fanux/sealos/pkg/config" - "github.com/fanux/sealos/pkg/utils/kubernetes/cert" "github.com/fanux/sealos/pkg/utils/logger" v1 "github.com/fanux/sealos/pkg/types/v1alpha1" - "github.com/fanux/sealos/pkg/utils/ssh" - - "github.com/fanux/sealos/pkg/ipvs" ) //BuildJoin is @@ -82,20 +78,20 @@ func joinNodesFunc(joinNodes []string) { //这里主要是为了获取CertificateKey func (s *SealosInstaller) GeneratorCerts() { cmd := `kubeadm init phase upload-certs --upload-certs` + v1.VLogString() - output := v1.SSHConfig.CmdToString(s.Masters[0], cmd, "\r\n") + output,_ := v1.SSHConfig.CmdToString(s.Masters[0], cmd, "\r\n") logger.Debug("[globals]decodeCertCmd: %s", output) slice := strings.Split(output, "Using certificate key:\r\n") slice1 := strings.Split(slice[1], "\r\n") v1.CertificateKey = slice1[0] cmd = "kubeadm token create --print-join-command" + v1.VLogString() - out := v1.SSHConfig.Cmd(s.Masters[0], cmd) + out,_ := v1.SSHConfig.Cmd(s.Masters[0], cmd) decodeOutput(out) } //GeneratorToken is func (s *SealosInstaller) GeneratorToken() { cmd := `kubeadm token create --print-join-command` + v1.VLogString() - output := v1.SSHConfig.Cmd(s.Masters[0], cmd) + output,_ := v1.SSHConfig.Cmd(s.Masters[0], cmd) decodeOutput(output) } @@ -135,8 +131,9 @@ func (s *SealosInstaller) JoinMasters(masters []string) { wg.Add(1) go func(master string) { defer wg.Done() - hostname := ssh.RemoteHostName(v1.SSHConfig, master) - certCMD := CMD(v1.APIServerCertSANs, iputils.IPFormat(master), hostname, v1.SvcCIDR, v1.DNSDomain) + //hostname := ssh.RemoteHostName(v1.SSHConfig, master) + //TODO + certCMD := CMD(v1.APIServerCertSANs, iputils.IPFormat(master), "hostname", v1.SvcCIDR, v1.DNSDomain) _ = v1.SSHConfig.CmdAsync(master, certCMD) cmdHosts := fmt.Sprintf("echo %s >> /etc/hosts", getApiserverHost(iputils.IPFormat(s.Masters[0]))) @@ -177,7 +174,7 @@ func (s *SealosInstaller) JoinNodes() { // 如果不是默认路由, 则添加 vip 到 master的路由。 cmdRoute := fmt.Sprintf("sealos route --host %s", iputils.IPFormat(node)) - status := v1.SSHConfig.CmdToString(node, cmdRoute, "") + status,_ := v1.SSHConfig.CmdToString(node, cmdRoute, "") if status != "ok" { // 以自己的ip作为路由网关 addRouteCmd := fmt.Sprintf("sealos route add --host %s --gateway %s", v1.VIP, iputils.IPFormat(node)) @@ -187,10 +184,11 @@ func (s *SealosInstaller) JoinNodes() { _ = v1.SSHConfig.CmdAsync(node, ipvsCmd) // create ipvs rules before we join node cmd := s.Command(v1.Version, JoinNode) //create lvscare static pod - yaml := ipvs.LvsStaticPodYaml(v1.VIP, v1.MasterIPs, v1.LvscareImage) + //TODO + //yaml := ipvs.LvsStaticPodYaml(v1.VIP, v1.MasterIPs, v1.LvscareImage) _ = v1.SSHConfig.CmdAsync(node, cmd) - _ = v1.SSHConfig.Cmd(node, "mkdir -p /etc/kubernetes/manifests") - v1.SSHConfig.CopyConfigFile(node, "/etc/kubernetes/manifests/kube-sealyun-lvscare.yaml", []byte(yaml)) + _,_ = v1.SSHConfig.Cmd(node, "mkdir -p /etc/kubernetes/manifests") + //v1.SSHConfig.CopyConfigFile(node, "/etc/kubernetes/manifests/kube-sealyun-lvscare.yaml", []byte(yaml)) cleaninstall := `rm -rf /root/kube` _ = v1.SSHConfig.CmdAsync(node, cleaninstall) @@ -206,9 +204,10 @@ func (s *SealosInstaller) lvscare() { wg.Add(1) go func(node string) { defer wg.Done() - yaml := ipvs.LvsStaticPodYaml(v1.VIP, v1.MasterIPs, v1.LvscareImage) - _ = v1.SSHConfig.Cmd(node, "rm -rf /etc/kubernetes/manifests/kube-sealyun-lvscare* || :") - v1.SSHConfig.CopyConfigFile(node, "/etc/kubernetes/manifests/kube-sealyun-lvscare.yaml", []byte(yaml)) + //TODO + //yaml := ipvs.LvsStaticPodYaml(v1.VIP, v1.MasterIPs, v1.LvscareImage) + //_ = v1.SSHConfig.Cmd(node, "rm -rf /etc/kubernetes/manifests/kube-sealyun-lvscare* || :") + //v1.SSHConfig.CopyConfigFile(node, "/etc/kubernetes/manifests/kube-sealyun-lvscare.yaml", []byte(yaml)) }(node) } @@ -221,21 +220,23 @@ func (s *SealosInstaller) sendNewCertAndKey(Hosts []string) { wg.Add(1) go func(node string) { defer wg.Done() - v1.SSHConfig.CopyLocalToRemote(node, v1.CertPath, cert.KubeDefaultCertPath) + //TODO + //v1.SSHConfig.CopyLocalToRemote(node, v1.CertPath, cert.KubeDefaultCertPath) }(node) } wg.Wait() } func (s *SealosInstaller) sendKubeConfigFile(hosts []string, kubeFile string) { - absKubeFile := cert.KubernetesDir + "/" + kubeFile - sealosKubeFile := v1.DefaultConfigPath + "/" + kubeFile + //absKubeFile := cert.KubernetesDir + "/" + kubeFile + //sealosKubeFile := v1.DefaultConfigPath + "/" + kubeFile var wg sync.WaitGroup for _, node := range hosts { wg.Add(1) go func(node string) { defer wg.Done() - v1.SSHConfig.CopyLocalToRemote(node, sealosKubeFile, absKubeFile) + //TODO + //v1.SSHConfig.CopyLocalToRemote(node, sealosKubeFile, absKubeFile) }(node) } wg.Wait() diff --git a/pkg/install/send.go b/pkg/install/send.go index e7e9d430b..c94528946 100755 --- a/pkg/install/send.go +++ b/pkg/install/send.go @@ -18,13 +18,13 @@ import ( "fmt" "path" - "github.com/fanux/sealos/pkg/utils/exec" "github.com/fanux/sealos/pkg/utils/versionutil" v1 "github.com/fanux/sealos/pkg/types/v1alpha1" - "github.com/fanux/sealos/pkg/utils/ssh" ) +//TODO + //SendPackage is func (s *SealosInstaller) SendPackage() { pkg := path.Base(v1.PkgURL) @@ -33,30 +33,30 @@ func (s *SealosInstaller) SendPackage() { deletekubectl := `sed -i '/kubectl/d;/sealos/d' /root/.bashrc ` completion := "echo 'command -v kubectl &>/dev/null && source <(kubectl completion bash)' >> /root/.bashrc && echo '[ -x /usr/bin/sealos ] && source <(sealos completion bash)' >> /root/.bashrc && source /root/.bashrc" kubeHook = kubeHook + " && " + deletekubectl + " && " + completion - v1.PkgURL = ssh.CopyFiles(v1.SSHConfig, v1.PkgURL, s.Hosts, "/root", nil, &kubeHook) + //v1.PkgURL = ssh.CopyFiles(v1.SSHConfig, v1.PkgURL, s.Hosts, "/root", nil, &kubeHook) } // SendSealos is send the exec sealos to /usr/bin/sealos func (s *SealosInstaller) SendSealos() { // send sealos first to avoid old version - sealos := exec.FetchSealosAbsPath() - beforeHook := "ps -ef |grep -v 'grep'|grep sealos >/dev/null || rm -rf /usr/bin/sealos" - afterHook := "chmod a+x /usr/bin/sealos" - ssh.CopyFiles(v1.SSHConfig, sealos, s.Hosts, "/usr/bin", &beforeHook, &afterHook) + //sealos := exec.FetchSealosAbsPath() + //beforeHook := "ps -ef |grep -v 'grep'|grep sealos >/dev/null || rm -rf /usr/bin/sealos" + //afterHook := "chmod a+x /usr/bin/sealos" + //ssh.CopyFiles(v1.SSHConfig, sealos, s.Hosts, "/usr/bin", &beforeHook, &afterHook) } // SendPackage is send new pkg to all nodes. func (u *SealosUpgrade) SendPackage() { - all := append(u.Masters, u.Nodes...) - pkg := path.Base(u.NewPkgURL) + //all := append(u.Masters, u.Nodes...) + //pkg := path.Base(u.NewPkgURL) // rm old sealos in package avoid old version problem. if sealos not exist in package then skip rm - var kubeHook string + //var kubeHook string if versionutil.For120(v1.Version) { // TODO update need load modprobe -- br_netfilter modprobe -- bridge. // https://github.com/fanux/cloud-kernel/issues/23 - kubeHook = fmt.Sprintf("cd /root && rm -rf kube && tar zxvf %s && cd /root/kube/shell && rm -f ../bin/sealos && (ctr -n=k8s.io image import ../images/images.tar || true) && cp -f ../bin/* /usr/bin/ ", pkg) + //kubeHook = fmt.Sprintf("cd /root && rm -rf kube && tar zxvf %s && cd /root/kube/shell && rm -f ../bin/sealos && (ctr -n=k8s.io image import ../images/images.tar || true) && cp -f ../bin/* /usr/bin/ ", pkg) } else { - kubeHook = fmt.Sprintf("cd /root && rm -rf kube && tar zxvf %s && cd /root/kube/shell && rm -f ../bin/sealos && (docker load -i ../images/images.tar || true) && cp -f ../bin/* /usr/bin/ ", pkg) + //kubeHook = fmt.Sprintf("cd /root && rm -rf kube && tar zxvf %s && cd /root/kube/shell && rm -f ../bin/sealos && (docker load -i ../images/images.tar || true) && cp -f ../bin/* /usr/bin/ ", pkg) } - v1.PkgURL = ssh.CopyFiles(v1.SSHConfig, pkg, all, "/root", nil, &kubeHook) + //v1.PkgURL = ssh.CopyFiles(v1.SSHConfig, pkg, all, "/root", nil, &kubeHook) } diff --git a/pkg/install/upgrade.go b/pkg/install/upgrade.go index 7cf54cb56..5932ee86c 100644 --- a/pkg/install/upgrade.go +++ b/pkg/install/upgrade.go @@ -182,8 +182,9 @@ func (u *SealosUpgrade) SetIPtoHostName() { all := append(u.Masters, u.Nodes...) u.IPtoHostName = make(map[string]string, len(all)) for _, node := range all { - hostname := v1.SSHConfig.CmdToString(node, "hostname", "") - u.IPtoHostName[node] = hostname + //TODO + //hostname := v1.SSHConfig.CmdToString(node, "hostname", "") + u.IPtoHostName[node] = "hostname" } } diff --git a/pkg/ipvs/lvscare.go b/pkg/ipvs/lvscare.go index 110a7e064..98275fa2c 100644 --- a/pkg/ipvs/lvscare.go +++ b/pkg/ipvs/lvscare.go @@ -15,10 +15,9 @@ package ipvs import ( + "fmt" "strings" - "github.com/fanux/sealos/pkg/utils/logger" - "github.com/fanux/sealos/pkg/types/contants" "github.com/pkg/errors" @@ -29,13 +28,12 @@ import ( ) const ( - LvsCareStaticPodName = "kube-sealyun-lvscare" - LvsCareCommand = "/usr/bin/lvscare" + LvsCareCommand = "/usr/bin/lvscare" ) -func LvsStaticPodYaml(vip string, masters []string, image string) string { +func LvsStaticPodYaml(vip string, masters []string, image, name string) (string, error) { if vip == "" || len(masters) == 0 { - return "" + return "", fmt.Errorf("vip and mster not allow empty") } if image == "" { image = contants.DefaultLvsCareImage @@ -50,7 +48,7 @@ func LvsStaticPodYaml(vip string, masters []string, image string) string { } flag := true pod := componentPod(v1.Container{ - Name: LvsCareStaticPodName, + Name: name, Image: image, Command: []string{LvsCareCommand}, Args: args, @@ -59,10 +57,9 @@ func LvsStaticPodYaml(vip string, masters []string, image string) string { }) yaml, err := podToYaml(pod) if err != nil { - logger.Error("decode lvs care static pod yaml failed %s", err) - return "" + return "", err } - return string(yaml) + return string(yaml), nil } func podToYaml(pod v1.Pod) ([]byte, error) { diff --git a/pkg/ipvs/lvscare_test.go b/pkg/ipvs/lvscare_test.go index cfd5ca37e..e88c01540 100644 --- a/pkg/ipvs/lvscare_test.go +++ b/pkg/ipvs/lvscare_test.go @@ -15,6 +15,7 @@ package ipvs import ( + "github.com/fanux/sealos/pkg/types/contants" "testing" ) @@ -90,7 +91,7 @@ func TestLvsStaticPodYaml(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := LvsStaticPodYaml(tt.args.vip, tt.args.masters, tt.args.image); got != tt.want { + if got,_ := LvsStaticPodYaml(tt.args.vip, tt.args.masters, tt.args.image, contants.LvsCareStaticPodName); got != tt.want { t.Errorf("LvsStaticPodYaml() = %v, want %v", got, tt.want) } }) diff --git a/pkg/passwd/passwd.go b/pkg/passwd/passwd.go new file mode 100644 index 000000000..0793043a8 --- /dev/null +++ b/pkg/passwd/passwd.go @@ -0,0 +1,35 @@ +/* +Copyright 2022 cuisongliu@qq.com. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package passwd + +import ( + "encoding/base64" + "golang.org/x/crypto/bcrypt" +) + +func Htpasswd(username, password string) string { + pwdHash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + return "" + } + return username + ":" + string(pwdHash) +} + +func LoginAuth(username, password string) string { + const encodeStd = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" + return base64.NewEncoding(encodeStd).EncodeToString([]byte(username + ":" + password)) +} diff --git a/pkg/types/contants/contants.go b/pkg/types/contants/contants.go index 246fb1748..e8826ab5e 100644 --- a/pkg/types/contants/contants.go +++ b/pkg/types/contants/contants.go @@ -16,7 +16,14 @@ limitations under the License. package contants +import "github.com/fanux/sealos/pkg/utils/file" + const ( - DefaultLvsCareImage = "sealyun.hub:5000/fanux/lvscare:latest" - DefaultTmpDir = "/var/lib/sealos/tmp" + DefaultLvsCareImage = "sealyun.hub:5000/sealyun/lvscare:latest" + DefaultConfigFile = "/config.yaml" + LvsCareStaticPodName = "kube-sealyun-lvscare" +) + +var ( + DefaultConfigPath = file.UserHomeDir() + "/.sealos" ) diff --git a/pkg/types/v1alpha1/types.go b/pkg/types/v1alpha1/types.go index 0e3f5bbeb..93bca6231 100644 --- a/pkg/types/v1alpha1/types.go +++ b/pkg/types/v1alpha1/types.go @@ -19,7 +19,6 @@ import ( "io/ioutil" "os" - "github.com/fanux/sealos/pkg/utils/file" "github.com/fanux/sealos/pkg/utils/iputils" "github.com/fanux/sealos/pkg/utils/logger" @@ -30,14 +29,9 @@ import ( ) const ( - DefaultConfigFile = "/config.yaml" DefaultAPIServerDomain = "apiserver.cluster.local" ) -var ( - DefaultConfigPath = file.UserHomeDir() + "/.sealos" -) - type Metadata struct { K8sVersion string `json:"k8sVersion"` CniVersion string `json:"cniVersion"` @@ -75,7 +69,7 @@ type SealConfig struct { //Dump is func (c *SealConfig) Dump(path string) { if path == "" { - path = DefaultConfigPath + DefaultConfigFile + path = contants.DefaultConfigPath + contants.DefaultConfigFile } MasterIPs = iputils.ParseIPs(MasterIPs) c.Masters = MasterIPs @@ -104,7 +98,7 @@ func (c *SealConfig) Dump(path string) { logger.Error("dump config file failed: %s", err) } - err = os.MkdirAll(DefaultConfigPath, os.ModePerm) + err = os.MkdirAll(contants.DefaultConfigPath, os.ModePerm) if err != nil { logger.Warn("create default sealos config dir failed, please create it by your self mkdir -p /root/.sealos && touch /root/.sealos/config.yaml") } @@ -120,7 +114,7 @@ func Dump(path string, content interface{}) error { logger.Error("dump config file failed: %s", err) return err } - err = os.MkdirAll(DefaultConfigPath, os.ModePerm) + err = os.MkdirAll(contants.DefaultConfigPath, os.ModePerm) if err != nil { logger.Error("create dump dir failed %s", err) return err @@ -133,7 +127,7 @@ func Dump(path string, content interface{}) error { //Load is func (c *SealConfig) Load(path string) (err error) { if path == "" { - path = DefaultConfigPath + DefaultConfigFile + path = contants.DefaultConfigPath + contants.DefaultConfigFile } y, err := ioutil.ReadFile(path) @@ -197,8 +191,8 @@ func (c *SealConfig) ShowDefaultConfig() { c.PodCIDR = "100.64.0.0/10" c.SvcCIDR = "10.96.0.0/12" c.APIServerCertSANs = []string{DefaultAPIServerDomain, "127.0.0.1"} - c.CertPath = DefaultConfigPath + "/pki" - c.CertEtcdPath = DefaultConfigPath + "/pki/etcd" + c.CertPath = contants.DefaultConfigPath + "/pki" + c.CertEtcdPath = contants.DefaultConfigPath + "/pki/etcd" c.LvscareName = contants.DefaultLvsCareImage y, err := yaml.Marshal(c) diff --git a/pkg/types/v1alpha1/vars.go b/pkg/types/v1alpha1/vars.go index 9dadf5db0..29147fd56 100644 --- a/pkg/types/v1alpha1/vars.go +++ b/pkg/types/v1alpha1/vars.go @@ -15,6 +15,7 @@ package v1alpha1 import ( + "github.com/fanux/sealos/pkg/types/contants" "strconv" "github.com/fanux/sealos/pkg/utils/ssh" @@ -30,11 +31,11 @@ var ( APIServerCertSANs []string SSHConfig ssh.SSH APIServer string - CertPath = DefaultConfigPath + "/pki" - CertEtcdPath = DefaultConfigPath + "/pki/etcd" - EtcdCacart = DefaultConfigPath + "/pki/etcd/ca.crt" - EtcdCert = DefaultConfigPath + "/pki/etcd/healthcheck-client.crt" - EtcdKey = DefaultConfigPath + "/pki/etcd/healthcheck-client.key" + CertPath = contants.DefaultConfigPath + "/pki" + CertEtcdPath = contants.DefaultConfigPath + "/pki/etcd" + EtcdCacart = contants.DefaultConfigPath + "/pki/etcd/ca.crt" + EtcdCert = contants.DefaultConfigPath + "/pki/etcd/healthcheck-client.crt" + EtcdKey = contants.DefaultConfigPath + "/pki/etcd/healthcheck-client.key" CriSocket string CgroupDriver string diff --git a/pkg/types/v1beta1/config.go b/pkg/types/v1beta1/config.go new file mode 100644 index 000000000..82713122b --- /dev/null +++ b/pkg/types/v1beta1/config.go @@ -0,0 +1,130 @@ +/* +Copyright 2022 cuisongliu@qq.com. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1beta1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type ConfigCNI struct { + Type string `json:"type"` + Version string `json:"version"` + Data map[string]string `json:"data"` +} + +type ConfigMetadata struct { + Version string `json:"version"` + Arch string `json:"arch"` +} + +type ConfigTemplate struct { + Init string + InitRegistry string + CleanRegistry string +} + +type ConfigSystem struct { + CNI ConfigCNI `json:"cni"` + LvscareImage string `json:"lvscareimage"` + Metadata ConfigMetadata `json:"metadata"` + Template ConfigTemplate `json:"template"` +} + +// +type ConfigRegistry struct { + Domain string `json:"domain"` + Port string `json:"port"` + Username string `json:"username"` + Password string `json:"password"` + ConfigDir string `json:"configDir"` // /etc/registry + DataDir string `json:"dataDir"` // /var/lib/registry + Host *string `json:"host,omitempty"` +} + +type ConfigCRI struct { + Registry ConfigRegistry `json:"registry"` + DataDir string `json:"dataDir"` +} + +type ConfigNetwork struct { + DNSDomain string `json:"dnsdomain"` + APIServerCertSANs []string `json:"apiservercertsans"` + VIP string `json:"vip"` + SvcCIDR string `json:"svccidr"` + PodCIDR string `json:"podcidr"` +} + +type ConfigFiles struct { + CertPath string `json:"cert"` + CertEtcdPath string `json:"etcd"` + KubeadmInitPath string `json:"init"` + KubeadmJoinPath string `json:"join"` +} + +type ConfigJoinNode struct { + Token string `json:"token"` + Expires *metav1.Time `json:"expires,omitempty"` +} + +type ConfigJoinMaster struct { + DiscoveryTokenCaCertHash string `json:"discovery-token-ca-cert-hash"` + CertificateKey string `json:"certificate-key"` +} + +type ConfigToken struct { + JoinNode ConfigJoinNode `json:"join_node"` + JoinMaster ConfigJoinMaster `json:"join_master"` +} + +type ConfigData struct { + CRI ConfigCRI `json:"cri"` + Network ConfigNetwork `json:"network"` + Files ConfigFiles `json:"files"` + Token ConfigToken `json:"token"` +} + +// +kubebuilder:object:root=true +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// Config is the Schema for the Config API +type Config struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec ConfigSpec `json:"spec,omitempty"` +} + +type ConfigSSH struct { + User string `json:"user,omitempty"` + Passwd string `json:"passwd,omitempty"` + Pk string `json:"pk,omitempty"` + PkPasswd string `json:"pkPasswd,omitempty"` + Port string `json:"port,omitempty"` +} + +type ConfigHost struct { + IPS []string `json:"ips,omitempty"` + Roles []string `json:"roles,omitempty"` +} + +// ConfigSpec defines the desired state of Config +type ConfigSpec struct { + SSH ConfigSSH `json:"ssh"` + Hosts []ConfigHost `json:"hosts,omitempty"` + System ConfigSystem `json:"system"` + Data ConfigData `json:"data"` +} diff --git a/pkg/types/v1beta1/defaults.go b/pkg/types/v1beta1/defaults.go index 7e77d3692..a98c103f5 100644 --- a/pkg/types/v1beta1/defaults.go +++ b/pkg/types/v1beta1/defaults.go @@ -16,154 +16,13 @@ limitations under the License. package v1beta1 -import ( - "math/rand" - "time" - - "github.com/fanux/sealos/pkg/utils/strings" - - "k8s.io/apimachinery/pkg/util/sets" -) - -const ( - SourceCidrIP = "0.0.0.0/0" - SSHPort = "22/22" - APIServerPort = "6443/6443" -) - -var ( - sshExportPort = ExportPort{ - Protocol: ProtocolTCP, - CidrIP: SourceCidrIP, - PortRange: SSHPort, - } - apiserverExportPort = ExportPort{ - Protocol: ProtocolTCP, - CidrIP: SourceCidrIP, - PortRange: APIServerPort, - } -) - -func Default(infra *Infra, fn func(infra *Infra) error) error { +func DefaultCluster(infra *Infra, fn func(infra *Infra) error) error { defaultCluster(infra) defaultHosts(infra) defaultToStatus(infra) return fn(infra) } -func defaultCluster(infra *Infra) { - infra.Spec.Cluster.AccessChannels.SSH.Port = 22 - if infra.Spec.Cluster.AccessChannels.SSH.Passwd == "" { - infra.Spec.Cluster.AccessChannels.SSH.Passwd = createPassword() - } - if infra.Spec.Cluster.Annotations == nil { - infra.Spec.Cluster.Annotations = make(map[string]string) - } - - if infra.Spec.Cluster.RegionIDs != nil { - infra.Spec.Cluster.RegionIDs = strings.RemoveSliceEmpty(infra.Spec.Cluster.RegionIDs) - } - if infra.Spec.Cluster.ZoneIDs != nil { - infra.Spec.Cluster.ZoneIDs = strings.RemoveSliceEmpty(infra.Spec.Cluster.ZoneIDs) - } - if infra.Spec.Cluster.Metadata.Network.Bandwidth == "" { - infra.Spec.Cluster.Metadata.Network.Bandwidth = "100" - } - if len(infra.Spec.Cluster.Metadata.Network.ExportPorts) == 0 { - infra.Spec.Cluster.Metadata.Network.ExportPorts = []ExportPort{ - sshExportPort, - apiserverExportPort, - } - } else { - ports := sets.NewString() - for _, port := range infra.Spec.Cluster.Metadata.Network.ExportPorts { - ports.Insert(port.PortRange) - } - if !ports.Has(SSHPort) { - infra.Spec.Cluster.Metadata.Network.ExportPorts = append(infra.Spec.Cluster.Metadata.Network.ExportPorts, sshExportPort) - } - if !ports.Has(APIServerPort) { - infra.Spec.Cluster.Metadata.Network.ExportPorts = append(infra.Spec.Cluster.Metadata.Network.ExportPorts, apiserverExportPort) - } - } - if infra.Spec.Cluster.Metadata.Network.PrivateCidrIP == "" { - infra.Spec.Cluster.Metadata.Network.PrivateCidrIP = "172.16.0.0/24" - } -} - -func defaultHosts(infra *Infra) { - for i, h := range infra.Spec.Hosts { - if string(h.Arch) == "" { - infra.Spec.Hosts[i].Arch = AMD64 - } - if h.Memory <= 0 { - infra.Spec.Hosts[i].Memory = 4 - } - if h.CPU <= 0 { - infra.Spec.Hosts[i].CPU = 2 - } - if len(h.Disks) == 0 { - disks := make([]Disk, 0) - disks = append(disks, Disk{ - Capacity: 50, - }) - infra.Spec.Hosts[i].Disks = disks - } - } -} - -func defaultToStatus(infra *Infra) { - if infra.Status.Cluster.Annotations == nil { - infra.Status.Cluster.Annotations = make(map[string]string) - } - status := infra.Status.Hosts - if status == nil { - status = make([]HostStatus, 0) - } - for _, h := range infra.Spec.Hosts { - index := infra.Status.FindHostsByRoles(h.Roles) - if index == -1 { - status = append(status, HostStatus{Roles: h.Roles, Arch: h.Arch}) - } - } - infra.Status.Hosts = status -} - -const ( - digits = "0123456789" - specials = "~=+%^*/()[]{}/!@#$?|" - letter = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" - passwordLength = 16 - Master = "master" - Node = "node" -) - -type HostRole string - -func createPassword() string { - rand.Seed(time.Now().UnixNano()) - all := digits + specials + letter - length := passwordLength - buf := make([]byte, length) - buf[0] = digits[rand.Intn(len(digits))] - buf[1] = specials[rand.Intn(len(specials))] - for i := 2; i < length; i++ { - buf[i] = all[rand.Intn(len(all))] - } - rand.Shuffle(len(buf), func(i, j int) { - buf[i], buf[j] = buf[j], buf[i] - }) - return string(buf) -} - -func IsMaster(roles []string) bool { - return In(Master, roles) -} - -func IsNode(roles []string) bool { - return In(Node, roles) -} - func In(key string, slice []string) bool { for _, s := range slice { if key == s { diff --git a/pkg/types/v1beta1/defaults_infra.go b/pkg/types/v1beta1/defaults_infra.go new file mode 100644 index 000000000..8d4906ae0 --- /dev/null +++ b/pkg/types/v1beta1/defaults_infra.go @@ -0,0 +1,156 @@ +/* +Copyright 2022 cuisongliu@qq.com. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1beta1 + +import ( + "github.com/fanux/sealos/pkg/utils/strings" + "k8s.io/apimachinery/pkg/util/sets" + "math/rand" + "time" +) + +const ( + SourceCidrIP = "0.0.0.0/0" + SSHPort = "22/22" + APIServerPort = "6443/6443" +) + +var ( + sshExportPort = ExportPort{ + Protocol: ProtocolTCP, + CidrIP: SourceCidrIP, + PortRange: SSHPort, + } + apiserverExportPort = ExportPort{ + Protocol: ProtocolTCP, + CidrIP: SourceCidrIP, + PortRange: APIServerPort, + } +) + +func defaultCluster(infra *Infra) { + infra.Spec.Cluster.AccessChannels.SSH.Port = 22 + if infra.Spec.Cluster.AccessChannels.SSH.Passwd == "" { + infra.Spec.Cluster.AccessChannels.SSH.Passwd = createPassword() + } + if infra.Spec.Cluster.Annotations == nil { + infra.Spec.Cluster.Annotations = make(map[string]string) + } + + if infra.Spec.Cluster.RegionIDs != nil { + infra.Spec.Cluster.RegionIDs = strings.RemoveSliceEmpty(infra.Spec.Cluster.RegionIDs) + } + if infra.Spec.Cluster.ZoneIDs != nil { + infra.Spec.Cluster.ZoneIDs = strings.RemoveSliceEmpty(infra.Spec.Cluster.ZoneIDs) + } + if infra.Spec.Cluster.Metadata.Network.Bandwidth == "" { + infra.Spec.Cluster.Metadata.Network.Bandwidth = "100" + } + if len(infra.Spec.Cluster.Metadata.Network.ExportPorts) == 0 { + infra.Spec.Cluster.Metadata.Network.ExportPorts = []ExportPort{ + sshExportPort, + apiserverExportPort, + } + } else { + ports := sets.NewString() + for _, port := range infra.Spec.Cluster.Metadata.Network.ExportPorts { + ports.Insert(port.PortRange) + } + if !ports.Has(SSHPort) { + infra.Spec.Cluster.Metadata.Network.ExportPorts = append(infra.Spec.Cluster.Metadata.Network.ExportPorts, sshExportPort) + } + if !ports.Has(APIServerPort) { + infra.Spec.Cluster.Metadata.Network.ExportPorts = append(infra.Spec.Cluster.Metadata.Network.ExportPorts, apiserverExportPort) + } + } + if infra.Spec.Cluster.Metadata.Network.PrivateCidrIP == "" { + infra.Spec.Cluster.Metadata.Network.PrivateCidrIP = "172.16.0.0/24" + } +} + +func defaultHosts(infra *Infra) { + for i, h := range infra.Spec.Hosts { + if string(h.Arch) == "" { + infra.Spec.Hosts[i].Arch = AMD64 + } + if h.Memory <= 0 { + infra.Spec.Hosts[i].Memory = 4 + } + if h.CPU <= 0 { + infra.Spec.Hosts[i].CPU = 2 + } + if len(h.Disks) == 0 { + disks := make([]Disk, 0) + disks = append(disks, Disk{ + Capacity: 50, + }) + infra.Spec.Hosts[i].Disks = disks + } + } +} + +func defaultToStatus(infra *Infra) { + if infra.Status.Cluster.Annotations == nil { + infra.Status.Cluster.Annotations = make(map[string]string) + } + status := infra.Status.Hosts + if status == nil { + status = make([]HostStatus, 0) + } + for _, h := range infra.Spec.Hosts { + index := infra.Status.FindHostsByRoles(h.Roles) + if index == -1 { + status = append(status, HostStatus{Roles: h.Roles, Arch: h.Arch}) + } + } + infra.Status.Hosts = status +} + +const ( + digits = "0123456789" + specials = "~=+%^*/()[]{}/!@#$?|" + letter = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + passwordLength = 16 + Master = "master" + Node = "node" +) + +type HostRole string + +func createPassword() string { + rand.Seed(time.Now().UnixNano()) + all := digits + specials + letter + length := passwordLength + buf := make([]byte, length) + buf[0] = digits[rand.Intn(len(digits))] + buf[1] = specials[rand.Intn(len(specials))] + for i := 2; i < length; i++ { + buf[i] = all[rand.Intn(len(all))] + } + rand.Shuffle(len(buf), func(i, j int) { + buf[i], buf[j] = buf[j], buf[i] + }) + return string(buf) +} + +func IsMaster(roles []string) bool { + return In(Master, roles) +} + +func IsNode(roles []string) bool { + return In(Node, roles) +} diff --git a/pkg/types/v1beta1/types.go b/pkg/types/v1beta1/infra.go similarity index 100% rename from pkg/types/v1beta1/types.go rename to pkg/types/v1beta1/infra.go diff --git a/pkg/types/v1beta1/zz_generated.deepcopy.go b/pkg/types/v1beta1/zz_generated.deepcopy.go index 8c9a226c2..65fce52ac 100644 --- a/pkg/types/v1beta1/zz_generated.deepcopy.go +++ b/pkg/types/v1beta1/zz_generated.deepcopy.go @@ -135,6 +135,324 @@ func (in *ClusterStatus) DeepCopy() *ClusterStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Config) DeepCopyInto(out *Config) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Config. +func (in *Config) DeepCopy() *Config { + if in == nil { + return nil + } + out := new(Config) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Config) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ConfigCNI) DeepCopyInto(out *ConfigCNI) { + *out = *in + if in.Data != nil { + in, out := &in.Data, &out.Data + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConfigCNI. +func (in *ConfigCNI) DeepCopy() *ConfigCNI { + if in == nil { + return nil + } + out := new(ConfigCNI) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ConfigCRI) DeepCopyInto(out *ConfigCRI) { + *out = *in + in.Registry.DeepCopyInto(&out.Registry) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConfigCRI. +func (in *ConfigCRI) DeepCopy() *ConfigCRI { + if in == nil { + return nil + } + out := new(ConfigCRI) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ConfigData) DeepCopyInto(out *ConfigData) { + *out = *in + in.CRI.DeepCopyInto(&out.CRI) + in.Network.DeepCopyInto(&out.Network) + out.Files = in.Files + in.Token.DeepCopyInto(&out.Token) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConfigData. +func (in *ConfigData) DeepCopy() *ConfigData { + if in == nil { + return nil + } + out := new(ConfigData) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ConfigFiles) DeepCopyInto(out *ConfigFiles) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConfigFiles. +func (in *ConfigFiles) DeepCopy() *ConfigFiles { + if in == nil { + return nil + } + out := new(ConfigFiles) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ConfigHost) DeepCopyInto(out *ConfigHost) { + *out = *in + if in.IPS != nil { + in, out := &in.IPS, &out.IPS + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Roles != nil { + in, out := &in.Roles, &out.Roles + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConfigHost. +func (in *ConfigHost) DeepCopy() *ConfigHost { + if in == nil { + return nil + } + out := new(ConfigHost) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ConfigJoinMaster) DeepCopyInto(out *ConfigJoinMaster) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConfigJoinMaster. +func (in *ConfigJoinMaster) DeepCopy() *ConfigJoinMaster { + if in == nil { + return nil + } + out := new(ConfigJoinMaster) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ConfigJoinNode) DeepCopyInto(out *ConfigJoinNode) { + *out = *in + if in.Expires != nil { + in, out := &in.Expires, &out.Expires + *out = (*in).DeepCopy() + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConfigJoinNode. +func (in *ConfigJoinNode) DeepCopy() *ConfigJoinNode { + if in == nil { + return nil + } + out := new(ConfigJoinNode) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ConfigMetadata) DeepCopyInto(out *ConfigMetadata) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConfigMetadata. +func (in *ConfigMetadata) DeepCopy() *ConfigMetadata { + if in == nil { + return nil + } + out := new(ConfigMetadata) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ConfigNetwork) DeepCopyInto(out *ConfigNetwork) { + *out = *in + if in.APIServerCertSANs != nil { + in, out := &in.APIServerCertSANs, &out.APIServerCertSANs + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConfigNetwork. +func (in *ConfigNetwork) DeepCopy() *ConfigNetwork { + if in == nil { + return nil + } + out := new(ConfigNetwork) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ConfigRegistry) DeepCopyInto(out *ConfigRegistry) { + *out = *in + if in.Host != nil { + in, out := &in.Host, &out.Host + *out = new(string) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConfigRegistry. +func (in *ConfigRegistry) DeepCopy() *ConfigRegistry { + if in == nil { + return nil + } + out := new(ConfigRegistry) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ConfigSSH) DeepCopyInto(out *ConfigSSH) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConfigSSH. +func (in *ConfigSSH) DeepCopy() *ConfigSSH { + if in == nil { + return nil + } + out := new(ConfigSSH) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ConfigSpec) DeepCopyInto(out *ConfigSpec) { + *out = *in + out.SSH = in.SSH + if in.Hosts != nil { + in, out := &in.Hosts, &out.Hosts + *out = make([]ConfigHost, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + in.System.DeepCopyInto(&out.System) + in.Data.DeepCopyInto(&out.Data) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConfigSpec. +func (in *ConfigSpec) DeepCopy() *ConfigSpec { + if in == nil { + return nil + } + out := new(ConfigSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ConfigSystem) DeepCopyInto(out *ConfigSystem) { + *out = *in + in.CNI.DeepCopyInto(&out.CNI) + out.Metadata = in.Metadata + out.Template = in.Template + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConfigSystem. +func (in *ConfigSystem) DeepCopy() *ConfigSystem { + if in == nil { + return nil + } + out := new(ConfigSystem) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ConfigTemplate) DeepCopyInto(out *ConfigTemplate) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConfigTemplate. +func (in *ConfigTemplate) DeepCopy() *ConfigTemplate { + if in == nil { + return nil + } + out := new(ConfigTemplate) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ConfigToken) DeepCopyInto(out *ConfigToken) { + *out = *in + in.JoinNode.DeepCopyInto(&out.JoinNode) + out.JoinMaster = in.JoinMaster + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConfigToken. +func (in *ConfigToken) DeepCopy() *ConfigToken { + if in == nil { + return nil + } + out := new(ConfigToken) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Credential) DeepCopyInto(out *Credential) { *out = *in diff --git a/pkg/types/validation/validation.go b/pkg/types/validation/validation_infra.go similarity index 100% rename from pkg/types/validation/validation.go rename to pkg/types/validation/validation_infra.go diff --git a/pkg/utils/assh/connect.go b/pkg/utils/assh/connect.go deleted file mode 100644 index 994632222..000000000 --- a/pkg/utils/assh/connect.go +++ /dev/null @@ -1,168 +0,0 @@ -// Copyright © 2021 Alibaba Group Holding Ltd. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package ssh - -import ( - "fmt" - "io/ioutil" - "net" - "os" - "path" - "path/filepath" - "strconv" - "strings" - "time" - - "github.com/fanux/sealos/pkg/utils/iputils" - - "github.com/fanux/sealos/pkg/utils/logger" - - "golang.org/x/crypto/ssh" -) - -/** - SSH connection operation -*/ -func (s *SSH) connect(host string) (*ssh.Client, error) { - auth := s.sshAuthMethod(s.Password, s.PkFile, s.PkPassword) - config := ssh.Config{ - Ciphers: []string{"aes128-ctr", "aes192-ctr", "aes256-ctr", "aes128-gcm@openssh.com", "arcfour256", "arcfour128", "aes128-cbc", "3des-cbc", "aes192-cbc", "aes256-cbc"}, - } - DefaultTimeout := time.Duration(15) * time.Second - if s.Timeout == nil { - s.Timeout = &DefaultTimeout - } - clientConfig := &ssh.ClientConfig{ - User: s.User, - Auth: auth, - Timeout: *s.Timeout, - Config: config, - HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error { - return nil - }, - } - ip, port := iputils.GetSSHHostIPAndPort(host) - addr := s.addrReformat(ip, port) - return ssh.Dial("tcp", addr, clientConfig) -} - -func (s *SSH) Connect(host string) (*ssh.Client, *ssh.Session, error) { - client, err := s.connect(host) - if err != nil { - return nil, nil, err - } - - session, err := client.NewSession() - if err != nil { - _ = client.Close() - return nil, nil, err - } - - modes := ssh.TerminalModes{ - ssh.ECHO: 0, //disable echoing - ssh.TTY_OP_ISPEED: 14400, // input speed = 14.4kbaud - ssh.TTY_OP_OSPEED: 14400, // output speed = 14.4kbaud - } - - if err := session.RequestPty("xterm", 80, 40, modes); err != nil { - _ = session.Close() - _ = client.Close() - return nil, nil, err - } - - return client, session, nil -} - -func (s *SSH) sshAuthMethod(password, pkFile, pkPasswd string) (auth []ssh.AuthMethod) { - if fileExist(pkFile) { - am, err := s.sshPrivateKeyMethod(pkFile, pkPasswd) - if err == nil { - auth = append(auth, am) - } - } - if password != "" { - auth = append(auth, s.sshPasswordMethod(password)) - } - return auth -} - -//Authentication with a private key,private key has password and no password to verify in this -func (s *SSH) sshPrivateKeyMethod(pkFile, pkPassword string) (am ssh.AuthMethod, err error) { - pkData, err := ioutil.ReadFile(filepath.Clean(pkFile)) - if err != nil { - return nil, err - } - - var pk ssh.Signer - if pkPassword == "" { - pk, err = ssh.ParsePrivateKey(pkData) - if err != nil { - return nil, err - } - } else { - bufPwd := []byte(pkPassword) - pk, err = ssh.ParsePrivateKeyWithPassphrase(pkData, bufPwd) - if err != nil { - return nil, err - } - } - return ssh.PublicKeys(pk), nil -} - -func fileExist(path string) bool { - _, err := os.Stat(path) - return err == nil || os.IsExist(err) -} -func (s *SSH) sshPasswordMethod(password string) ssh.AuthMethod { - return ssh.Password(password) -} - -func (s *SSH) addrReformat(host, port string) string { - if !strings.Contains(host, ":") { - host = fmt.Sprintf("%s:%s", host, port) - } - return host -} - -//RemoteFileExist is -func (s *SSH) IsFileExist(host, remoteFilePath string) bool { - // if remote file is - // ls -l | grep aa | wc -l - remoteFileName := path.Base(remoteFilePath) // aa - remoteFileDirName := path.Dir(remoteFilePath) - //it's bug: if file is aa.bak, `ls -l | grep aa | wc -l` is 1 ,should use `ll aa 2>/dev/null |wc -l` - //remoteFileCommand := fmt.Sprintf("ls -l %s| grep %s | grep -v grep |wc -l", remoteFileDirName, remoteFileName) - remoteFileCommand := fmt.Sprintf("ls -l %s/%s 2>/dev/null |wc -l", remoteFileDirName, remoteFileName) - - data, err := s.CmdToString(host, remoteFileCommand, " ") - defer func() { - if r := recover(); r != nil { - logger.Error("[ssh][%s]remoteFileCommand err:%s", host, err) - } - }() - if err != nil { - panic(1) - } - count, err := strconv.Atoi(strings.TrimSpace(data)) - defer func() { - if r := recover(); r != nil { - logger.Error("[ssh][%s]RemoteFileExist:%s", host, err) - } - }() - if err != nil { - panic(1) - } - return count != 0 -} diff --git a/pkg/utils/assh/scp.go b/pkg/utils/assh/scp.go deleted file mode 100644 index b7f81be78..000000000 --- a/pkg/utils/assh/scp.go +++ /dev/null @@ -1,258 +0,0 @@ -// Copyright © 2021 Alibaba Group Holding Ltd. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package ssh - -import ( - "fmt" - "io" - "io/ioutil" - "os" - "path" - "path/filepath" - "strings" - - "github.com/fanux/sealos/pkg/utils/hash" - - "github.com/fanux/sealos/pkg/utils/file" - "github.com/fanux/sealos/pkg/utils/iputils" - - "github.com/fanux/sealos/pkg/utils/logger" - "github.com/fanux/sealos/pkg/utils/progress" - "github.com/schollz/progressbar/v3" - - "github.com/pkg/sftp" - "golang.org/x/crypto/ssh" -) - -func (s *SSH) RemoteMd5Sum(host, remoteFilePath string) string { - cmd := fmt.Sprintf("md5sum %s | cut -d\" \" -f1", remoteFilePath) - remoteMD5, err := s.CmdToString(host, cmd, "") - if err != nil { - logger.Error("count remote md5 failed %s %s %v", host, remoteFilePath, err) - } - return remoteMD5 -} - -//CmdToString is in host exec cmd and replace to spilt str -func (s *SSH) CmdToString(host, cmd, spilt string) (string, error) { - data, err := s.Cmd(host, cmd) - str := string(data) - if err != nil { - return str, fmt.Errorf("exec remote command failed %s %s %v", host, cmd, err) - } - if data != nil { - str = strings.ReplaceAll(str, "\r\n", spilt) - str = strings.ReplaceAll(str, "\n", spilt) - return str, nil - } - return str, fmt.Errorf("command %s %s return nil", host, cmd) -} - -//SftpConnect is -func (s *SSH) sftpConnect(host string) (*ssh.Client, *sftp.Client, error) { - sshClient, err := s.connect(host) - if err != nil { - return nil, nil, err - } - - // create sftp client - sftpClient, err := sftp.NewClient(sshClient) - return sshClient, sftpClient, err -} - -// CopyRemoteFileToLocal is scp remote file to local -func (s *SSH) Fetch(host, localFilePath, remoteFilePath string) error { - if iputils.IsLocalIP(host, s.LocalAddress) { - if remoteFilePath != localFilePath { - logger.Debug("local copy files src %s to dst %s", remoteFilePath, localFilePath) - return file.RecursionCopy(remoteFilePath, localFilePath) - } - return nil - } - sshClient, sftpClient, err := s.sftpConnect(host) - if err != nil { - return fmt.Errorf("new sftp client failed %v", err) - } - defer func() { - _ = sftpClient.Close() - _ = sshClient.Close() - }() - // open remote source file - srcFile, err := sftpClient.Open(remoteFilePath) - if err != nil { - return fmt.Errorf("open remote file failed %v, remote path: %s", err, remoteFilePath) - } - defer srcFile.Close() - - err = file.MkFileFullPathDir(localFilePath) - if err != nil { - return err - } - // open local Destination file - dstFile, err := os.Create(localFilePath) - if err != nil { - return fmt.Errorf("create local file failed %v", err) - } - defer dstFile.Close() - // copy to local file - _, err = srcFile.WriteTo(dstFile) - return err -} - -// CopyLocalToRemote is copy file or dir to remotePath, add md5 validate -func (s *SSH) Copy(host, localPath, remotePath string) error { - if iputils.IsLocalIP(host, s.LocalAddress) { - logger.Debug("local copy files src %s to dst %s", localPath, remotePath) - return file.RecursionCopy(localPath, remotePath) - } - logger.Debug("remote copy files src %s to dst %s", localPath, remotePath) - sshClient, sftpClient, err := s.sftpConnect(host) - if err != nil { - return fmt.Errorf("new sftp client failed %s", err) - } - defer func() { - _ = sftpClient.Close() - _ = sshClient.Close() - }() - - f, err := os.Stat(localPath) - if err != nil { - return fmt.Errorf("get file stat failed %s", err) - } - - baseRemoteFilePath := filepath.Dir(remotePath) - _, err = sftpClient.ReadDir(baseRemoteFilePath) - if err != nil { - if err = sftpClient.MkdirAll(baseRemoteFilePath); err != nil { - return err - } - } - number := 1 - if f.IsDir() { - number = file.CountDirFiles(localPath) - } - // no file in dir, do need to send - if number == 0 { - return nil - } - var ( - bar = progress.Simple("copying files to "+host, number) - ) - defer func() { - _ = bar.Close() - }() - - if f.IsDir() { - s.copyLocalDirToRemote(host, sftpClient, localPath, remotePath, bar) - } else { - err = s.copyLocalFileToRemote(host, sftpClient, localPath, remotePath) - if err != nil { - errMsg := fmt.Sprintf("copy local file to remote failed %v %s %s %s", err, host, localPath, remotePath) - logger.Error(errMsg) - } - _ = bar.Add(1) - } - return nil -} - -func (s *SSH) copyLocalDirToRemote(host string, sftpClient *sftp.Client, localPath, remotePath string, epu *progressbar.ProgressBar) { - localFiles, err := ioutil.ReadDir(localPath) - if err != nil { - logger.Error("read local path dir failed %s %s", host, localPath) - return - } - if err = sftpClient.MkdirAll(remotePath); err != nil { - logger.Error("failed to create remote path %s:%v", remotePath, err) - return - } - for _, file := range localFiles { - lfp := path.Join(localPath, file.Name()) - rfp := path.Join(remotePath, file.Name()) - if file.IsDir() { - if err = sftpClient.MkdirAll(rfp); err != nil { - logger.Error("failed to create remote path %s:%v", rfp, err) - return - } - s.copyLocalDirToRemote(host, sftpClient, lfp, rfp, epu) - } else { - err := s.copyLocalFileToRemote(host, sftpClient, lfp, rfp) - if err != nil { - errMsg := fmt.Sprintf("copy local file to remote failed %v %s %s %s", err, host, lfp, rfp) - logger.Error(errMsg) - return - } - _ = epu.Add(1) - } - } -} - -// check the remote file existence before copying -// solve the sesion -func (s *SSH) copyLocalFileToRemote(host string, sftpClient *sftp.Client, localPath, remotePath string) error { - var ( - srcMd5, dstMd5 string - ) - srcMd5 = hash.FileMD5(localPath) - if s.IsFileExist(host, remotePath) { - dstMd5 = s.RemoteMd5Sum(host, remotePath) - if srcMd5 == dstMd5 { - logger.Debug("remote dst %s already exists and is the latest version , skip copying process", remotePath) - return nil - } - } - srcFile, err := os.Open(filepath.Clean(localPath)) - if err != nil { - return err - } - defer srcFile.Close() - dstFile, err := sftpClient.Create(remotePath) - if err != nil { - return err - } - fileStat, err := srcFile.Stat() - if err != nil { - return fmt.Errorf("get file stat failed %v", err) - } - // TODO seems not work - if err := dstFile.Chmod(fileStat.Mode()); err != nil { - return fmt.Errorf("chmod remote file failed %v", err) - } - defer dstFile.Close() - _, err = io.Copy(dstFile, srcFile) - if err != nil { - return err - } - dstMd5 = s.RemoteMd5Sum(host, remotePath) - if srcMd5 != dstMd5 { - return fmt.Errorf("[ssh][%s] validate md5sum failed %s != %s", host, srcMd5, dstMd5) - } - return nil -} - -//if remote file not exist return false and nil -func (s *SSH) RemoteDirExist(host, remoteDirpath string) (bool, error) { - sshClient, sftpClient, err := s.sftpConnect(host) - if err != nil { - return false, err - } - defer func() { - _ = sftpClient.Close() - _ = sshClient.Close() - }() - if _, err := sftpClient.ReadDir(remoteDirpath); err != nil { - return false, err - } - return true, nil -} diff --git a/pkg/utils/assh/ssh.go b/pkg/utils/assh/ssh.go deleted file mode 100644 index 88db02407..000000000 --- a/pkg/utils/assh/ssh.go +++ /dev/null @@ -1,155 +0,0 @@ -// Copyright © 2021 Alibaba Group Holding Ltd. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package ssh - -import ( - "fmt" - "net" - "sync" - "time" -) - -type Interface interface { - // Copy is copy local files to remote host - // scp -r /tmp root@192.168.0.2:/root/tmp => Copy("192.168.0.2","tmp","/root/tmp") - // need check md5sum - Copy(host, srcFilePath, dstFilePath string) error - // Fetch is copy remote host files to localhost - Fetch(host, srcFilePath, dstFilePath string) error - // CmdAsync is exec command on remote host, and asynchronous return logs - CmdAsync(host string, cmd ...string) error - // Cmd is exec command on remote host, and return combined standard output and standard error - Cmd(host, cmd string) ([]byte, error) - // IsFileExist is check remote file exist or not - IsFileExist(host, remoteFilePath string) bool - //RemoteDirExist is remote file existence returns true, nil - RemoteDirExist(host, remoteDirpath string) (bool, error) - //CmdToString is exec command on remote host, and return spilt standard output and standard error - CmdToString(host, cmd, spilt string) (string, error) - Ping(host string) error -} - -type SSH struct { - User string - Password string - PkFile string - PkPassword string - Timeout *time.Duration - LocalAddress *[]net.Addr -} - -//func NewSSHByCluster(cluster *v1.Cluster) Interface { -// if cluster.Spec.SSH.User == "" { -// cluster.Spec.SSH.User = common.ROOT -// } -// address, err := utils.IsLocalHostAddrs() -// if err != nil { -// logger.Warn("failed to get local address, %v", err) -// } -// return &SSH{ -// User: cluster.Spec.SSH.User, -// Password: cluster.Spec.SSH.Passwd, -// PkFile: cluster.Spec.SSH.Pk, -// PkPassword: cluster.Spec.SSH.PkPasswd, -// LocalAddress: address, -// } -//} -// -//func NewSSHClient(ssh *v1.SSH) Interface { -// if ssh.User == "" { -// ssh.User = common.ROOT -// } -// address, err := utils.IsLocalHostAddrs() -// if err != nil { -// logger.Warn("failed to get local address, %v", err) -// } -// return &SSH{ -// User: ssh.User, -// Password: ssh.Passwd, -// PkFile: ssh.Pk, -// PkPassword: ssh.PkPasswd, -// LocalAddress: address, -// } -//} -// -//func GetHostSSHClient(hostIP string, cluster *v2.Cluster) (Interface, error) { -// for _, host := range cluster.Spec.Hosts { -// for _, ip := range host.IPS { -// if hostIP == ip { -// if err := mergo.Merge(&host.SSH, &cluster.Spec.SSH); err != nil { -// return nil, err -// } -// -// return NewSSHClient(&host.SSH), nil -// } -// } -// } -// return nil, fmt.Errorf("get host ssh client failed, host ip %s not in hosts ip list", hostIP) -//} -// -//type Client struct { -// SSH Interface -// Host string -//} -// -//func NewSSHClientWithCluster(cluster *v1.Cluster) (*Client, error) { -// var ( -// ipList []string -// host string -// ) -// sshClient := NewSSHByCluster(cluster) -// if cluster.Spec.Provider == common.AliCloud { -// host = cluster.GetAnnotationsByKey(common.Eip) -// if host == "" { -// return nil, fmt.Errorf("get cluster EIP failed") -// } -// ipList = append(ipList, host) -// } else { -// host = cluster.Spec.Masters.IPList[0] -// ipList = append(ipList, append(cluster.Spec.Masters.IPList, cluster.Spec.Nodes.IPList...)...) -// } -// err := WaitSSHReady(sshClient, 6, ipList...) -// if err != nil { -// return nil, err -// } -// if sshClient == nil { -// return nil, fmt.Errorf("cloud build init ssh client failed") -// } -// return &Client{ -// SSH: sshClient, -// Host: host, -// }, nil -//} - -func WaitSSHReady(ssh Interface, tryTimes int, hosts ...string) error { - var err error - var wg sync.WaitGroup - for _, h := range hosts { - wg.Add(1) - go func(host string) { - defer wg.Done() - for i := 0; i < tryTimes; i++ { - err = ssh.Ping(host) - if err == nil { - return - } - time.Sleep(time.Duration(i) * time.Second) - } - err = fmt.Errorf("wait for [%s] ssh ready timeout: %v, ensure that the IP address or password is correct", host, err) - }(h) - } - wg.Wait() - return err -} diff --git a/pkg/utils/file/contants.go b/pkg/utils/file/contants.go new file mode 100644 index 000000000..e7c5bb15c --- /dev/null +++ b/pkg/utils/file/contants.go @@ -0,0 +1,19 @@ +/* +Copyright 2022 cuisongliu@qq.com. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package file + +const DefaultTmpDir = "/var/lib/sealyun/tmp" diff --git a/pkg/utils/file/file_v2.go b/pkg/utils/file/file_v2.go index e932cdadd..a34438910 100644 --- a/pkg/utils/file/file_v2.go +++ b/pkg/utils/file/file_v2.go @@ -25,7 +25,6 @@ import ( "path/filepath" "strings" - "github.com/fanux/sealos/pkg/types/contants" "github.com/fanux/sealos/pkg/utils/logger" "github.com/pkg/errors" @@ -109,7 +108,7 @@ func MkDirs(dirs ...string) error { } func MkTmpdir() (string, error) { - tempDir, err := ioutil.TempDir(contants.DefaultTmpDir, ".DTmp-") + tempDir, err := ioutil.TempDir(DefaultTmpDir, ".DTmp-") if err != nil { return "", err } diff --git a/pkg/utils/kubernetes/apis/kubeadm/types.go b/pkg/utils/kubernetes/apis/kubeadm/types.go deleted file mode 100644 index d8b96e0de..000000000 --- a/pkg/utils/kubernetes/apis/kubeadm/types.go +++ /dev/null @@ -1,168 +0,0 @@ -// Copyright © 2021 sealos. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package kubeadm - -import ( - "strings" - "time" - - "github.com/pkg/errors" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - bootstrapapi "k8s.io/cluster-bootstrap/token/api" - bootstraputil "k8s.io/cluster-bootstrap/token/util" -) - -const ( - // When a token is matched with 'BootstrapTokenPattern', the size of validated substrings returned by - // regexp functions which contains 'Submatch' in their names will be 3. - // Submatch 0 is the match of the entire expression, submatch 1 is - // the match of the first parenthesized subexpression, and so on. - // e.g.: - // result := bootstraputil.BootstrapTokenRegexp.FindStringSubmatch("abcdef.1234567890123456") - // result == []string{"abcdef.1234567890123456","abcdef","1234567890123456"} - // len(result) == 3 - validatedSubstringsSize = 3 -) - -// NewBootstrapTokenString converts the given Bootstrap Token as a string -// to the BootstrapTokenString object used for serialization/deserialization -// and internal usage. It also automatically validates that the given token -// is of the right format -func NewBootstrapTokenString(token string) (*BootstrapTokenString, error) { - substrs := bootstraputil.BootstrapTokenRegexp.FindStringSubmatch(token) - if len(substrs) != validatedSubstringsSize { - return nil, errors.Errorf("the bootstrap token %q was not of the form %q", token, bootstrapapi.BootstrapTokenPattern) - } - - return &BootstrapTokenString{ID: substrs[1], Secret: substrs[2]}, nil -} - -type BootstrapTokenString struct { - ID string `json:"-"` - Secret string `json:"-"` -} - -func (bts *BootstrapTokenString) ToString() string { - if len(bts.ID) > 0 && len(bts.Secret) > 0 { - return bootstraputil.TokenFromIDAndSecret(bts.ID, bts.Secret) - } - return "" -} - -// BootstrapToken describes one bootstrap token, stored as a Secret in the cluster -type BootstrapToken struct { - // Token is used for establishing bidirectional trust between nodes and control-planes. - // Used for joining nodes in the cluster. - Token *BootstrapTokenString `json:"token"` - // Description sets a human-friendly message why this token exists and what it's used - // for, so other administrators can know its purpose. - Description string `json:"description,omitempty"` - // TTL defines the time to live for this token. Defaults to 24h. - // Expires and TTL are mutually exclusive. - TTL *metav1.Duration `json:"ttl,omitempty"` - // Expires specifies the timestamp when this token expires. Defaults to being set - // dynamically at runtime based on the TTL. Expires and TTL are mutually exclusive. - Expires *metav1.Time `json:"expires,omitempty"` - // Usages describes the ways in which this token can be used. Can by default be used - // for establishing bidirectional trust, but that can be changed here. - Usages []string `json:"usages,omitempty"` - // Groups specifies the extra groups that this token will authenticate as when/if - // used for authentication - Groups []string `json:"groups,omitempty"` -} - -// ToSecret converts the given BootstrapToken object to its Secret representation that -// may be submitted to the API Server in order to be stored. -func (bt *BootstrapToken) ToSecret() *v1.Secret { - return &v1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: bootstraputil.BootstrapTokenSecretName(bt.Token.ID), - Namespace: metav1.NamespaceSystem, - }, - Type: bootstrapapi.SecretTypeBootstrapToken, - //Type: v1.SecretTypeOpaque, - Data: encodeTokenSecretData(bt, time.Now()), - } -} - -// encodeTokenSecretData takes the token discovery object and an optional duration and returns the .Data for the Secret -// now is passed in order to be able to used in unit testing -func encodeTokenSecretData(token *BootstrapToken, now time.Time) map[string][]byte { - data := map[string][]byte{ - bootstrapapi.BootstrapTokenIDKey: []byte(token.Token.ID), - bootstrapapi.BootstrapTokenSecretKey: []byte(token.Token.Secret), - } - - if len(token.Description) > 0 { - data[bootstrapapi.BootstrapTokenDescriptionKey] = []byte(token.Description) - } - - // If for some strange reason both token.TTL and token.Expires would be set - // (they are mutually exclusive in validation so this shouldn't be the case), - // token.Expires has higher priority, as can be seen in the logic here. - if token.Expires != nil { - // Format the expiration date accordingly - // TODO: This maybe should be a helper function in bootstraputil? - expirationString := token.Expires.Time.Format(time.RFC3339) - data[bootstrapapi.BootstrapTokenExpirationKey] = []byte(expirationString) - } else if token.TTL != nil && token.TTL.Duration > 0 { - // Only if .Expires is unset, TTL might have an effect - // Get the current time, add the specified duration, and format it accordingly - expirationString := now.Add(token.TTL.Duration).Format(time.RFC3339) - data[bootstrapapi.BootstrapTokenExpirationKey] = []byte(expirationString) - } - for _, usage := range token.Usages { - data[bootstrapapi.BootstrapTokenUsagePrefix+usage] = []byte("true") - } - - if len(token.Groups) > 0 { - data[bootstrapapi.BootstrapTokenExtraGroupsKey] = []byte(strings.Join(token.Groups, ",")) - } - return data -} - -// ClusterConfiguration contains cluster-wide configuration for a kubeadm cluster -type ClusterConfiguration struct { - metav1.TypeMeta - - // ExternalEtcd holds configuration for etcd. - ExternalEtcd *ExternalEtcd - - // KubernetesVersion is the target version of the control plane. - KubernetesVersion string - - // CertificatesDir specifies where to store or look for all required certificates. - CertificatesDir string - - // ImageRepository sets the container registry to pull images from. - // If empty, `k8s.gcr.io` will be used by default; in case of kubernetes version is a CI build (kubernetes version starts with `ci/` or `ci-cross/`) - // `gcr.io/k8s-staging-ci-images` will be used as a default for control plane components and for kube-proxy, while `k8s.gcr.io` - // will be used for all the other images. - ImageRepository string -} - -type CNI struct { -} - -// ExternalEtcd describes an external etcd cluster -type ExternalEtcd struct { - // CAFile is an SSL Certificate Authority file used to secure etcd communication. - CAFile string - // CertFile is an SSL certification file used to secure etcd communication. - CertFile string - // KeyFile is an SSL key file used to secure etcd communication. - KeyFile string -} diff --git a/pkg/utils/kubernetes/cert/cert_cmd.go b/pkg/utils/kubernetes/cert/cert_cmd.go new file mode 100644 index 000000000..c07bc37e2 --- /dev/null +++ b/pkg/utils/kubernetes/cert/cert_cmd.go @@ -0,0 +1,43 @@ +/* +Copyright 2022 cuisongliu@qq.com. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cert + +import "fmt" + +// GenerateCert generate all cert. +func GenerateCert(certPATH, certEtcdPATH string, altNames []string, hostIP, hostName, serviceCIRD, DNSDomain string) error { + certConfig, err := NewSealosCertMetaData(certPATH, certEtcdPATH, altNames, serviceCIRD, hostName, hostIP, DNSDomain) + if err != nil { + return fmt.Errorf("generator cert config failed %v", err) + } + return certConfig.GenerateAll() +} + +func GenerateRegistryCert(registryCertPath string, BaseName string) error { + regCertConfig := Config{ + Path: registryCertPath, + BaseName: BaseName, + CommonName: BaseName, + Organization: []string{"sealyun"}, + Year: 100, + } + cert, key, err := NewCaCertAndKey(regCertConfig) + if err != nil { + return err + } + return WriteCertAndKey(regCertConfig.Path, regCertConfig.BaseName, cert, key) +} diff --git a/pkg/utils/kubernetes/copycerts/copycerts.go b/pkg/utils/kubernetes/copycerts/copycerts.go deleted file mode 100644 index c7b5ad15c..000000000 --- a/pkg/utils/kubernetes/copycerts/copycerts.go +++ /dev/null @@ -1,292 +0,0 @@ -/* -Copyright 2019 The Kubernetes Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package copycerts - -import ( - "context" - "encoding/hex" - "fmt" - "io/ioutil" - "os" - "path" - "strings" - - "github.com/fanux/sealos/pkg/utils/kubernetes/apiclient" - "github.com/fanux/sealos/pkg/utils/kubernetes/apis/kubeadm" - cryptoutil "github.com/fanux/sealos/pkg/utils/kubernetes/crypto" - "github.com/fanux/sealos/pkg/utils/logger" - - "github.com/pkg/errors" - - v1 "k8s.io/api/core/v1" - rbac "k8s.io/api/rbac/v1" - apierrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime/schema" - clientset "k8s.io/client-go/kubernetes" - certutil "k8s.io/client-go/util/cert" - keyutil "k8s.io/client-go/util/keyutil" - bootstraputil "k8s.io/cluster-bootstrap/token/util" -) - -const ( - externalEtcdCA = "external-etcd-ca.crt" - externalEtcdCert = "external-etcd.crt" - externalEtcdKey = "external-etcd.key" - CertificateKeySize = 32 - KubeadmCertsSecret = "kubeadm-certs" - // NodeBootstrapTokenAuthGroup specifies which group a Node Bootstrap Token should be authenticated in - NodeBootstrapTokenAuthGroup = "system:bootstrappers:kubeadm:default-node-token" - // CACertName defines certificate name - CACertName = "ca.crt" - // CAKeyName defines certificate name - CAKeyName = "ca.key" - // EtcdCACertName defines etcd's CA certificate name - EtcdCACertName = "etcd/ca.crt" - // EtcdCAKeyName defines etcd's CA key name - EtcdCAKeyName = "etcd/ca.key" - // ServiceAccountPublicKeyName defines SA public key base name - ServiceAccountPublicKeyName = "sa.pub" - // ServiceAccountPrivateKeyName defines SA private key base name - ServiceAccountPrivateKeyName = "sa.key" - - // FrontProxyCACertName defines front proxy CA certificate name - FrontProxyCACertName = "front-proxy-ca.crt" - // FrontProxyCAKeyName defines front proxy CA key name - FrontProxyCAKeyName = "front-proxy-ca.key" -) - -var ( - // KubeadmCertsClusterRoleName sets the name for the ClusterRole that allows - // the bootstrap tokens to access the kubeadm-certs Secret during the join of a new control-plane - KubeadmCertsClusterRoleName = fmt.Sprintf("kubeadm:%s", KubeadmCertsSecret) -) - -//CreateCertificateKey returns a cryptographically secure random key -func CreateCertificateKey() (string, error) { - randBytes, err := cryptoutil.CreateRandBytes(CertificateKeySize) - if err != nil { - return "", err - } - return hex.EncodeToString(randBytes), nil -} - -//UploadCerts save certs needs to join a new control-plane on kubeadm-certs sercret. -func UploadCerts(client clientset.Interface, cfg *kubeadm.ClusterConfiguration, key, tokenID string) error { - fmt.Printf("[upload-certs] Storing the certificates in Secret %q in the %q Namespace\n", KubeadmCertsSecret, metav1.NamespaceSystem) - decodedKey, err := hex.DecodeString(key) - if err != nil { - return errors.Wrap(err, "error decoding certificate key") - } - - secretData, err := getDataFromDisk(cfg, decodedKey) - if err != nil { - return err - } - ref, err := getSecretOwnerRef(client, tokenID) - if err != nil { - return err - } - - err = apiclient.CreateOrUpdateSecret(client, &v1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: KubeadmCertsSecret, - Namespace: metav1.NamespaceSystem, - OwnerReferences: ref, - }, - Data: secretData, - }) - if err != nil { - return err - } - - return createRBAC(client) -} - -func createRBAC(client clientset.Interface) error { - err := apiclient.CreateOrUpdateRole(client, &rbac.Role{ - ObjectMeta: metav1.ObjectMeta{ - Name: KubeadmCertsClusterRoleName, - Namespace: metav1.NamespaceSystem, - }, - Rules: []rbac.PolicyRule{ - { - Verbs: []string{"get"}, - APIGroups: []string{""}, - Resources: []string{"secrets"}, - ResourceNames: []string{KubeadmCertsSecret}, - }, - }, - }) - if err != nil { - return err - } - - return apiclient.CreateOrUpdateRoleBinding(client, &rbac.RoleBinding{ - ObjectMeta: metav1.ObjectMeta{ - Name: KubeadmCertsClusterRoleName, - Namespace: metav1.NamespaceSystem, - }, - RoleRef: rbac.RoleRef{ - APIGroup: rbac.GroupName, - Kind: "Role", - Name: KubeadmCertsClusterRoleName, - }, - Subjects: []rbac.Subject{ - { - Kind: rbac.GroupKind, - Name: NodeBootstrapTokenAuthGroup, - }, - }, - }) -} - -func getSecretOwnerRef(client clientset.Interface, tokenID string) ([]metav1.OwnerReference, error) { - secretName := bootstraputil.BootstrapTokenSecretName(tokenID) - secret, err := client.CoreV1().Secrets(metav1.NamespaceSystem).Get(context.TODO(), secretName, metav1.GetOptions{}) - if err != nil { - return nil, errors.Wrap(err, "error to get token reference") - } - - gvk := schema.GroupVersionKind{Version: "v1", Kind: "Secret"} - ref := metav1.NewControllerRef(secret, gvk) - return []metav1.OwnerReference{*ref}, nil -} - -func loadAndEncryptCert(certPath string, key []byte) ([]byte, error) { - cert, err := ioutil.ReadFile(certPath) - if err != nil { - return nil, err - } - return cryptoutil.EncryptBytes(cert, key) -} - -func certsToTransfer(cfg *kubeadm.ClusterConfiguration) map[string]string { - certsDir := cfg.CertificatesDir - certs := map[string]string{ - CACertName: path.Join(certsDir, CACertName), - CAKeyName: path.Join(certsDir, CAKeyName), - FrontProxyCACertName: path.Join(certsDir, FrontProxyCACertName), - FrontProxyCAKeyName: path.Join(certsDir, FrontProxyCAKeyName), - ServiceAccountPublicKeyName: path.Join(certsDir, ServiceAccountPublicKeyName), - ServiceAccountPrivateKeyName: path.Join(certsDir, ServiceAccountPrivateKeyName), - } - - if cfg.ExternalEtcd == nil { - certs[EtcdCACertName] = path.Join(certsDir, EtcdCACertName) - certs[EtcdCAKeyName] = path.Join(certsDir, EtcdCAKeyName) - } else { - certs[externalEtcdCA] = cfg.ExternalEtcd.CAFile - certs[externalEtcdCert] = cfg.ExternalEtcd.CertFile - certs[externalEtcdKey] = cfg.ExternalEtcd.KeyFile - } - - return certs -} - -func getDataFromDisk(cfg *kubeadm.ClusterConfiguration, key []byte) (map[string][]byte, error) { - secretData := map[string][]byte{} - for certName, certPath := range certsToTransfer(cfg) { - cert, err := loadAndEncryptCert(certPath, key) - if err == nil || os.IsNotExist(err) { - secretData[certOrKeyNameToSecretName(certName)] = cert - } else { - return nil, err - } - } - return secretData, nil -} - -// DownloadCerts downloads the certificates needed to join a new control plane. -func DownloadCerts(client clientset.Interface, cfg *kubeadm.ClusterConfiguration, key string) error { - fmt.Printf("[download-certs] Downloading the certificates in Secret %q in the %q Namespace\n", KubeadmCertsSecret, metav1.NamespaceSystem) - - decodedKey, err := hex.DecodeString(key) - if err != nil { - return errors.Wrap(err, "error decoding certificate key") - } - - secret, err := getSecret(client) - if err != nil { - return errors.Wrap(err, "error downloading the secret") - } - - secretData, err := getDataFromSecret(secret, decodedKey) - if err != nil { - return errors.Wrap(err, "error decoding secret data with provided key") - } - - for certOrKeyName, certOrKeyPath := range certsToTransfer(cfg) { - certOrKeyData, found := secretData[certOrKeyNameToSecretName(certOrKeyName)] - if !found { - return errors.Errorf("the Secret does not include the required certificate or key - name: %s, path: %s", certOrKeyName, certOrKeyPath) - } - if len(certOrKeyData) == 0 { - logger.Info("[download-certs] Not saving %q to disk, since it is empty in the %q Secret\n", certOrKeyName, KubeadmCertsSecret) - continue - } - if err := writeCertOrKey(certOrKeyPath, certOrKeyData); err != nil { - return err - } - } - - return nil -} - -func writeCertOrKey(certOrKeyPath string, certOrKeyData []byte) error { - if _, err := keyutil.ParsePublicKeysPEM(certOrKeyData); err == nil { - return keyutil.WriteKey(certOrKeyPath, certOrKeyData) - } else if _, err := certutil.ParseCertsPEM(certOrKeyData); err == nil { - return certutil.WriteCert(certOrKeyPath, certOrKeyData) - } - return errors.New("unknown data found in Secret entry") -} - -func getSecret(client clientset.Interface) (*v1.Secret, error) { - secret, err := client.CoreV1().Secrets(metav1.NamespaceSystem).Get(context.TODO(), KubeadmCertsSecret, metav1.GetOptions{}) - if err != nil { - if apierrors.IsNotFound(err) { - return nil, errors.Errorf("Secret %q was not found in the %q Namespace. This Secret might have expired. Please, run `kubeadm init phase upload-certs --upload-certs` on a control plane to generate a new one", KubeadmCertsSecret, metav1.NamespaceSystem) - } - return nil, err - } - return secret, nil -} - -func getDataFromSecret(secret *v1.Secret, key []byte) (map[string][]byte, error) { - secretData := map[string][]byte{} - for secretName, encryptedSecret := range secret.Data { - // In some cases the secret might have empty data if the secrets were not present on disk - // when uploading. This can specially happen with external insecure etcd (no certs) - if len(encryptedSecret) > 0 { - cert, err := cryptoutil.DecryptBytes(encryptedSecret, key) - if err != nil { - // If any of the decrypt operations fail do not return a partial result, - // return an empty result immediately - return map[string][]byte{}, err - } - secretData[secretName] = cert - } else { - secretData[secretName] = []byte{} - } - } - return secretData, nil -} - -func certOrKeyNameToSecretName(certOrKeyName string) string { - return strings.Replace(certOrKeyName, "/", "-", -1) -} diff --git a/pkg/utils/kubernetes/crypto/crypto.go b/pkg/utils/kubernetes/crypto/crypto.go deleted file mode 100644 index c3b866bee..000000000 --- a/pkg/utils/kubernetes/crypto/crypto.go +++ /dev/null @@ -1,76 +0,0 @@ -/* -Copyright 2019 The Kubernetes Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package crypto - -import ( - "github.com/pkg/errors" - - "crypto/aes" - "crypto/cipher" - "crypto/rand" -) - -// CreateRandBytes returns a cryptographically secure slice of random bytes with a given size -func CreateRandBytes(size uint32) ([]byte, error) { - bytes := make([]byte, size) - if _, err := rand.Read(bytes); err != nil { - return nil, err - } - return bytes, nil -} - -// EncryptBytes takes a byte slice of raw data and an encryption key and returns an encrypted byte slice of data. -// The key must be an AES key, either 16, 24, or 32 bytes to select AES-128, AES-192, or AES-256 -func EncryptBytes(data, key []byte) ([]byte, error) { - block, err := aes.NewCipher(key) - if err != nil { - return nil, err - } - gcm, err := cipher.NewGCM(block) - if err != nil { - return nil, err - } - nonce, err := CreateRandBytes(uint32(gcm.NonceSize())) - if err != nil { - return nil, err - } - return gcm.Seal(nonce, nonce, data, nil), nil -} - -// DecryptBytes takes a byte slice of encrypted data and an encryption key and returns a decrypted byte slice of data. -// The key must be an AES key, either 16, 24, or 32 bytes to select AES-128, AES-192, or AES-256 -func DecryptBytes(data, key []byte) ([]byte, error) { - block, err := aes.NewCipher(key) - if err != nil { - return nil, err - } - gcm, err := cipher.NewGCM(block) - if err != nil { - return nil, err - } - nonceSize := gcm.NonceSize() - if len(data) < nonceSize { - return nil, errors.New("size of data is less than the nonce") - } - - nonce, out := data[:nonceSize], data[nonceSize:] - out, err = gcm.Open(nil, nonce, out, nil) - if err != nil { - return nil, err - } - return out, nil -} diff --git a/pkg/utils/kubernetes/join/bootstraptoken.go b/pkg/utils/kubernetes/join/bootstraptoken.go deleted file mode 100644 index dfed6a20f..000000000 --- a/pkg/utils/kubernetes/join/bootstraptoken.go +++ /dev/null @@ -1,51 +0,0 @@ -/* -Copyright 2017 The Kubernetes Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package join - -import ( - "github.com/fanux/sealos/pkg/utils/kubernetes/apis/kubeadm" - "github.com/fanux/sealos/pkg/utils/kubernetes/copycerts" - "github.com/fanux/sealos/pkg/utils/kubernetes/token" - "github.com/pkg/errors" - - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - clientset "k8s.io/client-go/kubernetes" - bootstraputil "k8s.io/cluster-bootstrap/token/util" -) - -func CreateShortLivedBootstrapToken(client clientset.Interface, ttl *metav1.Duration) (*kubeadm.BootstrapTokenString, error) { - tokenStr, err := bootstraputil.GenerateBootstrapToken() - if err != nil { - return nil, errors.Wrap(err, "error generator bootstrap bootstrapToken") - } - bootstrapToken, err := kubeadm.NewBootstrapTokenString(tokenStr) - if err != nil { - return nil, errors.Wrap(err, "error creating upload certs bootstrapToken") - } - btoken := kubeadm.BootstrapToken{ - Token: bootstrapToken, - Description: "Proxy for managing TTL for the sealos secret", - TTL: ttl, - Usages: []string{"authentication", "signing"}, - Groups: []string{copycerts.NodeBootstrapTokenAuthGroup}, - } - err = token.CreateNewTokens(client, btoken) - if err != nil { - return nil, err - } - return btoken.Token, nil -} diff --git a/pkg/utils/kubernetes/join/ca_cert_hash.go b/pkg/utils/kubernetes/join/ca_cert_hash.go deleted file mode 100644 index e27248ea8..000000000 --- a/pkg/utils/kubernetes/join/ca_cert_hash.go +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright © 2021 sealos. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package join - -import ( - "crypto/x509" - - "github.com/fanux/sealos/pkg/utils/kubernetes/kubeconfig" - "github.com/fanux/sealos/pkg/utils/kubernetes/pubkeypin" - - "github.com/pkg/errors" - "k8s.io/client-go/tools/clientcmd" - clientcertutil "k8s.io/client-go/util/cert" -) - -func GetCaCertHash(kubeConfigFile string) ([]string, error) { - // load the kubeconfig file to get the CA certificate and endpoint - config, err := clientcmd.LoadFromFile(kubeConfigFile) - if err != nil { - return nil, errors.Wrap(err, "failed to load kubeconfig") - } - - // load the default cluster config - clusterConfig := kubeconfig.GetClusterFromKubeConfig(config) - if clusterConfig == nil { - return nil, errors.New("failed to get default cluster config") - } - - // load CA certificates from the kubeconfig (either from PEM data or by file path) - var caCerts []*x509.Certificate - if clusterConfig.CertificateAuthorityData != nil { - caCerts, err = clientcertutil.ParseCertsPEM(clusterConfig.CertificateAuthorityData) - if err != nil { - return nil, errors.Wrap(err, "failed to parse CA certificate from kubeconfig") - } - } else if clusterConfig.CertificateAuthority != "" { - caCerts, err = clientcertutil.CertsFromFile(clusterConfig.CertificateAuthority) - if err != nil { - return nil, errors.Wrap(err, "failed to load CA certificate referenced by kubeconfig") - } - } else { - return nil, errors.New("no CA certificates found in kubeconfig") - } - - // hash all the CA certs and include their public key pins as trusted values - publicKeyPins := make([]string, 0, len(caCerts)) - for _, caCert := range caCerts { - publicKeyPins = append(publicKeyPins, pubkeypin.Hash(caCert)) - } - - //ctx := map[string]interface{}{ - // "Token": token, - // "CAPubKeyPins": publicKeyPins, - // "ControlPlaneHostPort": strings.Replace(clusterConfig.Server, "https://", "", -1), - // "CertificateKey": key, - // "ControlPlane": controlPlane, - //} - return publicKeyPins, nil -} diff --git a/pkg/utils/kubernetes/kubeconfig/kubeconfig.go b/pkg/utils/kubernetes/kubeconfig/kubeconfig.go deleted file mode 100644 index 670b0509d..000000000 --- a/pkg/utils/kubernetes/kubeconfig/kubeconfig.go +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright © 2021 sealos. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package kubeconfig - -import clientcmdapi "k8s.io/client-go/tools/clientcmd/api" - -// GetClusterFromKubeConfig returns the default Infra of the specified KubeConfig -func GetClusterFromKubeConfig(config *clientcmdapi.Config) *clientcmdapi.Cluster { - // If there is an unnamed cluster object, use it - if config.Clusters[""] != nil { - return config.Clusters[""] - } - if config.Contexts[config.CurrentContext] != nil { - return config.Clusters[config.Contexts[config.CurrentContext].Cluster] - } - return nil -} diff --git a/pkg/utils/kubernetes/nodeclient/node.go b/pkg/utils/kubernetes/nodeclient/node.go index 5785ca208..dc51eed8e 100644 --- a/pkg/utils/kubernetes/nodeclient/node.go +++ b/pkg/utils/kubernetes/nodeclient/node.go @@ -17,13 +17,13 @@ package nodeclient import ( "context" "fmt" + "github.com/fanux/sealos/pkg/types/contants" "time" "github.com/fanux/sealos/pkg/utils/strings" "github.com/fanux/sealos/pkg/utils/iputils" - v12 "github.com/fanux/sealos/pkg/types/v1alpha1" v1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -43,7 +43,7 @@ const ( ) var ( - KubeDefaultConfigPath = v12.DefaultConfigPath + "/admin.conf" + KubeDefaultConfigPath = contants.DefaultConfigPath + "/admin.conf" ) // NewClient is get clientSet by kubeConfig diff --git a/pkg/utils/kubernetes/pubkeypin/pubkeypin.go b/pkg/utils/kubernetes/pubkeypin/pubkeypin.go deleted file mode 100644 index fb4fdc52a..000000000 --- a/pkg/utils/kubernetes/pubkeypin/pubkeypin.go +++ /dev/null @@ -1,114 +0,0 @@ -/* -Copyright 2017 The Kubernetes Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -// Package pubkeypin provides primitives for x509 public key pinning in the -// style of RFC7469. -package pubkeypin - -import ( - "crypto/sha256" - "crypto/x509" - "encoding/hex" - "strings" - - "github.com/pkg/errors" -) - -const ( - // formatSHA256 is the prefix for pins that are full-length SHA-256 hashes encoded in base 16 (hex) - formatSHA256 = "sha256" -) - -// Set is a set of pinned x509 public keys. -type Set struct { - sha256Hashes map[string]bool -} - -// NewSet returns a new, empty PubKeyPinSet -func NewSet() *Set { - return &Set{make(map[string]bool)} -} - -// Allow adds an allowed public key hash to the Set -func (s *Set) Allow(pubKeyHashes ...string) error { - for _, pubKeyHash := range pubKeyHashes { - parts := strings.Split(pubKeyHash, ":") - if len(parts) != 2 { - return errors.New("invalid public key hash, expected \"format:value\"") - } - format, value := parts[0], parts[1] - - switch strings.ToLower(format) { - case "sha256": - return s.allowSHA256(value) - default: - return errors.Errorf("unknown hash format %q", format) - } - } - return nil -} - -// CheckAny checks if at least one certificate matches one of the public keys in the set -func (s *Set) CheckAny(certificates []*x509.Certificate) error { - var hashes []string - - for _, certificate := range certificates { - if s.checkSHA256(certificate) { - return nil - } - - hashes = append(hashes, Hash(certificate)) - } - return errors.Errorf("none of the public keys %q are pinned", strings.Join(hashes, ":")) -} - -// Empty returns true if the Set contains no pinned public keys. -func (s *Set) Empty() bool { - return len(s.sha256Hashes) == 0 -} - -// Hash calculates the SHA-256 hash of the Subject Public Key Information (SPKI) -// object in an x509 certificate (in DER encoding). It returns the full hash as a -// hex encoded string (suitable for passing to Set.Allow). -func Hash(certificate *x509.Certificate) string { - spkiHash := sha256.Sum256(certificate.RawSubjectPublicKeyInfo) - return formatSHA256 + ":" + strings.ToLower(hex.EncodeToString(spkiHash[:])) -} - -// allowSHA256 validates a "sha256" format hash and adds a canonical version of it into the Set -func (s *Set) allowSHA256(hash string) error { - // validate that the hash is the right length to be a full SHA-256 hash - hashLength := hex.DecodedLen(len(hash)) - if hashLength != sha256.Size { - return errors.Errorf("expected a %d byte SHA-256 hash, found %d bytes", sha256.Size, hashLength) - } - - // validate that the hash is valid hex - if _, err := hex.DecodeString(hash); err != nil { - return err - } - - // in the end, just store the original hex string in memory (in lowercase) - s.sha256Hashes[strings.ToLower(hash)] = true - return nil -} - -// checkSHA256 returns true if the certificate's "sha256" hash is pinned in the Set -func (s *Set) checkSHA256(certificate *x509.Certificate) bool { - actualHash := sha256.Sum256(certificate.RawSubjectPublicKeyInfo) - actualHashHex := strings.ToLower(hex.EncodeToString(actualHash[:])) - return s.sha256Hashes[actualHashHex] -} diff --git a/pkg/utils/kubernetes/token/bootstraptoken/doc.go b/pkg/utils/kubernetes/token/bootstraptoken/doc.go new file mode 100644 index 000000000..b7be16ba4 --- /dev/null +++ b/pkg/utils/kubernetes/token/bootstraptoken/doc.go @@ -0,0 +1,21 @@ +/* +Copyright 2021 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// +groupName=bootstraptoken.kubeadm.k8s.io + +// Package bootstraptoken contains an API and utilities wrapping the +// "bootstrap.kubernetes.io/token" Secret type to ease its usage in kubeadm. +package bootstraptoken // import "k8s.io/kubernetes/cmd/kubeadm/app/apis/bootstraptoken" diff --git a/pkg/utils/kubernetes/token/bootstraptoken/v1/types.go b/pkg/utils/kubernetes/token/bootstraptoken/v1/types.go new file mode 100644 index 000000000..8ebaf77f3 --- /dev/null +++ b/pkg/utils/kubernetes/token/bootstraptoken/v1/types.go @@ -0,0 +1,58 @@ +/* +Copyright 2021 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// BootstrapToken describes one bootstrap token, stored as a Secret in the cluster +// +k8s:deepcopy-gen=true +type BootstrapToken struct { + // Token is used for establishing bidirectional trust between nodes and control-planes. + // Used for joining nodes in the cluster. + Token *BootstrapTokenString `json:"token" datapolicy:"token"` + // Description sets a human-friendly message why this token exists and what it's used + // for, so other administrators can know its purpose. + // +optional + Description string `json:"description,omitempty"` + // TTL defines the time to live for this token. Defaults to 24h. + // Expires and TTL are mutually exclusive. + // +optional + TTL *metav1.Duration `json:"ttl,omitempty"` + // Expires specifies the timestamp when this token expires. Defaults to being set + // dynamically at runtime based on the TTL. Expires and TTL are mutually exclusive. + // +optional + Expires *metav1.Time `json:"expires,omitempty"` + // Usages describes the ways in which this token can be used. Can by default be used + // for establishing bidirectional trust, but that can be changed here. + // +optional + Usages []string `json:"usages,omitempty"` + // Groups specifies the extra groups that this token will authenticate as when/if + // used for authentication + // +optional + Groups []string `json:"groups,omitempty"` +} + +// BootstrapTokenString is a token of the format abcdef.abcdef0123456789 that is used +// for both validation of the practically of the API server from a joining node's point +// of view and as an authentication method for the node in the bootstrap phase of +// "kubeadm join". This token is and should be short-lived +type BootstrapTokenString struct { + ID string `json:"-"` + Secret string `json:"-" datapolicy:"token"` +} diff --git a/pkg/utils/kubernetes/token/bootstraptoken/v1/utils.go b/pkg/utils/kubernetes/token/bootstraptoken/v1/utils.go new file mode 100644 index 000000000..19d6394b3 --- /dev/null +++ b/pkg/utils/kubernetes/token/bootstraptoken/v1/utils.go @@ -0,0 +1,223 @@ +/* +Copyright 2021 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1 + +import ( + "fmt" + "sort" + "strings" + "time" + + "github.com/pkg/errors" + + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + bootstrapapi "k8s.io/cluster-bootstrap/token/api" + bootstraputil "k8s.io/cluster-bootstrap/token/util" + bootstrapsecretutil "k8s.io/cluster-bootstrap/util/secrets" +) + +const ( + // When a token is matched with 'BootstrapTokenPattern', the size of validated substrings returned by + // regexp functions which contains 'Submatch' in their names will be 3. + // Submatch 0 is the match of the entire expression, submatch 1 is + // the match of the first parenthesized subexpression, and so on. + // e.g.: + // result := bootstraputil.BootstrapTokenRegexp.FindStringSubmatch("abcdef.1234567890123456") + // result == []string{"abcdef.1234567890123456","abcdef","1234567890123456"} + // len(result) == 3 + validatedSubstringsSize = 3 +) + +// MarshalJSON implements the json.Marshaler interface. +func (bts BootstrapTokenString) MarshalJSON() ([]byte, error) { + return []byte(fmt.Sprintf(`"%s"`, bts.String())), nil +} + +// UnmarshalJSON implements the json.Unmarshaller interface. +func (bts *BootstrapTokenString) UnmarshalJSON(b []byte) error { + // If the token is represented as "", just return quickly without an error + if len(b) == 0 { + return nil + } + + // Remove unnecessary " characters coming from the JSON parser + token := strings.Replace(string(b), `"`, ``, -1) + // Convert the string Token to a BootstrapTokenString object + newbts, err := NewBootstrapTokenString(token) + if err != nil { + return err + } + bts.ID = newbts.ID + bts.Secret = newbts.Secret + return nil +} + +// String returns the string representation of the BootstrapTokenString +func (bts BootstrapTokenString) String() string { + if len(bts.ID) > 0 && len(bts.Secret) > 0 { + return bootstraputil.TokenFromIDAndSecret(bts.ID, bts.Secret) + } + return "" +} + +// NewBootstrapTokenString converts the given Bootstrap Token as a string +// to the BootstrapTokenString object used for serialization/deserialization +// and internal usage. It also automatically validates that the given token +// is of the right format +func NewBootstrapTokenString(token string) (*BootstrapTokenString, error) { + substrs := bootstraputil.BootstrapTokenRegexp.FindStringSubmatch(token) + if len(substrs) != validatedSubstringsSize { + return nil, errors.Errorf("the bootstrap token %q was not of the form %q", token, bootstrapapi.BootstrapTokenPattern) + } + + return &BootstrapTokenString{ID: substrs[1], Secret: substrs[2]}, nil +} + +// NewBootstrapTokenStringFromIDAndSecret is a wrapper around NewBootstrapTokenString +// that allows the caller to specify the ID and Secret separately +func NewBootstrapTokenStringFromIDAndSecret(id, secret string) (*BootstrapTokenString, error) { + return NewBootstrapTokenString(bootstraputil.TokenFromIDAndSecret(id, secret)) +} + +// BootstrapTokenToSecret converts the given BootstrapToken object to its Secret representation that +// may be submitted to the API Server in order to be stored. +func BootstrapTokenToSecret(bt *BootstrapToken) *v1.Secret { + return &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: bootstraputil.BootstrapTokenSecretName(bt.Token.ID), + Namespace: metav1.NamespaceSystem, + }, + Type: v1.SecretType(bootstrapapi.SecretTypeBootstrapToken), + Data: encodeTokenSecretData(bt, time.Now()), + } +} + +// encodeTokenSecretData takes the token discovery object and an optional duration and returns the .Data for the Secret +// now is passed in order to be able to used in unit testing +func encodeTokenSecretData(token *BootstrapToken, now time.Time) map[string][]byte { + data := map[string][]byte{ + bootstrapapi.BootstrapTokenIDKey: []byte(token.Token.ID), + bootstrapapi.BootstrapTokenSecretKey: []byte(token.Token.Secret), + } + + if len(token.Description) > 0 { + data[bootstrapapi.BootstrapTokenDescriptionKey] = []byte(token.Description) + } + + // If for some strange reason both token.TTL and token.Expires would be set + // (they are mutually exclusive in validation so this shouldn't be the case), + // token.Expires has higher priority, as can be seen in the logic here. + if token.Expires != nil { + // Format the expiration date accordingly + // TODO: This maybe should be a helper function in bootstraputil? + expirationString := token.Expires.Time.UTC().Format(time.RFC3339) + data[bootstrapapi.BootstrapTokenExpirationKey] = []byte(expirationString) + + } else if token.TTL != nil && token.TTL.Duration > 0 { + // Only if .Expires is unset, TTL might have an effect + // Get the current time, add the specified duration, and format it accordingly + expirationString := now.Add(token.TTL.Duration).UTC().Format(time.RFC3339) + data[bootstrapapi.BootstrapTokenExpirationKey] = []byte(expirationString) + } + + for _, usage := range token.Usages { + data[bootstrapapi.BootstrapTokenUsagePrefix+usage] = []byte("true") + } + + if len(token.Groups) > 0 { + data[bootstrapapi.BootstrapTokenExtraGroupsKey] = []byte(strings.Join(token.Groups, ",")) + } + return data +} + +// BootstrapTokenFromSecret returns a BootstrapToken object from the given Secret +func BootstrapTokenFromSecret(secret *v1.Secret) (*BootstrapToken, error) { + // Get the Token ID field from the Secret data + tokenID := bootstrapsecretutil.GetData(secret, bootstrapapi.BootstrapTokenIDKey) + if len(tokenID) == 0 { + return nil, errors.Errorf("bootstrap Token Secret has no token-id data: %s", secret.Name) + } + + // Enforce the right naming convention + if secret.Name != bootstraputil.BootstrapTokenSecretName(tokenID) { + return nil, errors.Errorf("bootstrap token name is not of the form '%s(token-id)'. Actual: %q. Expected: %q", + bootstrapapi.BootstrapTokenSecretPrefix, secret.Name, bootstraputil.BootstrapTokenSecretName(tokenID)) + } + + tokenSecret := bootstrapsecretutil.GetData(secret, bootstrapapi.BootstrapTokenSecretKey) + if len(tokenSecret) == 0 { + return nil, errors.Errorf("bootstrap Token Secret has no token-secret data: %s", secret.Name) + } + + // Create the BootstrapTokenString object based on the ID and Secret + bts, err := NewBootstrapTokenStringFromIDAndSecret(tokenID, tokenSecret) + if err != nil { + return nil, errors.Wrap(err, "bootstrap Token Secret is invalid and couldn't be parsed") + } + + // Get the description (if any) from the Secret + description := bootstrapsecretutil.GetData(secret, bootstrapapi.BootstrapTokenDescriptionKey) + + // Expiration time is optional, if not specified this implies the token + // never expires. + secretExpiration := bootstrapsecretutil.GetData(secret, bootstrapapi.BootstrapTokenExpirationKey) + var expires *metav1.Time + if len(secretExpiration) > 0 { + expTime, err := time.Parse(time.RFC3339, secretExpiration) + if err != nil { + return nil, errors.Wrapf(err, "can't parse expiration time of bootstrap token %q", secret.Name) + } + expires = &metav1.Time{Time: expTime} + } + + // Build an usages string slice from the Secret data + var usages []string + for k, v := range secret.Data { + // Skip all fields that don't include this prefix + if !strings.HasPrefix(k, bootstrapapi.BootstrapTokenUsagePrefix) { + continue + } + // Skip those that don't have this usage set to true + if string(v) != "true" { + continue + } + usages = append(usages, strings.TrimPrefix(k, bootstrapapi.BootstrapTokenUsagePrefix)) + } + // Only sort the slice if defined + if usages != nil { + sort.Strings(usages) + } + + // Get the extra groups information from the Secret + // It's done this way to make .Groups be nil in case there is no items, rather than an + // empty slice or an empty slice with a "" string only + var groups []string + groupsString := bootstrapsecretutil.GetData(secret, bootstrapapi.BootstrapTokenExtraGroupsKey) + g := strings.Split(groupsString, ",") + if len(g) > 0 && len(g[0]) > 0 { + groups = g + } + + return &BootstrapToken{ + Token: bts, + Description: description, + Expires: expires, + Usages: usages, + Groups: groups, + }, nil +} diff --git a/pkg/utils/kubernetes/token/bootstraptoken/v1/utils_test.go b/pkg/utils/kubernetes/token/bootstraptoken/v1/utils_test.go new file mode 100644 index 000000000..490869c91 --- /dev/null +++ b/pkg/utils/kubernetes/token/bootstraptoken/v1/utils_test.go @@ -0,0 +1,681 @@ +/* +Copyright 2021 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1 + +import ( + "encoding/json" + "reflect" + "testing" + "time" + + "github.com/pkg/errors" + + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestMarshalJSON(t *testing.T) { + var tests = []struct { + bts BootstrapTokenString + expected string + }{ + {BootstrapTokenString{ID: "abcdef", Secret: "abcdef0123456789"}, `"abcdef.abcdef0123456789"`}, + {BootstrapTokenString{ID: "foo", Secret: "bar"}, `"foo.bar"`}, + {BootstrapTokenString{ID: "h", Secret: "b"}, `"h.b"`}, + } + for _, rt := range tests { + t.Run(rt.bts.ID, func(t *testing.T) { + b, err := json.Marshal(rt.bts) + if err != nil { + t.Fatalf("json.Marshal returned an unexpected error: %v", err) + } + if string(b) != rt.expected { + t.Errorf( + "failed BootstrapTokenString.MarshalJSON:\n\texpected: %s\n\t actual: %s", + rt.expected, + string(b), + ) + } + }) + } +} + +func TestUnmarshalJSON(t *testing.T) { + var tests = []struct { + input string + bts *BootstrapTokenString + expectedError bool + }{ + {`"f.s"`, &BootstrapTokenString{}, true}, + {`"abcdef."`, &BootstrapTokenString{}, true}, + {`"abcdef:abcdef0123456789"`, &BootstrapTokenString{}, true}, + {`abcdef.abcdef0123456789`, &BootstrapTokenString{}, true}, + {`"abcdef.abcdef0123456789`, &BootstrapTokenString{}, true}, + {`"abcdef.ABCDEF0123456789"`, &BootstrapTokenString{}, true}, + {`"abcdef.abcdef0123456789"`, &BootstrapTokenString{ID: "abcdef", Secret: "abcdef0123456789"}, false}, + {`"123456.aabbccddeeffgghh"`, &BootstrapTokenString{ID: "123456", Secret: "aabbccddeeffgghh"}, false}, + } + for _, rt := range tests { + t.Run(rt.input, func(t *testing.T) { + newbts := &BootstrapTokenString{} + err := json.Unmarshal([]byte(rt.input), newbts) + if (err != nil) != rt.expectedError { + t.Errorf("failed BootstrapTokenString.UnmarshalJSON:\n\texpected error: %t\n\t actual error: %v", rt.expectedError, err) + } else if !reflect.DeepEqual(rt.bts, newbts) { + t.Errorf( + "failed BootstrapTokenString.UnmarshalJSON:\n\texpected: %v\n\t actual: %v", + rt.bts, + newbts, + ) + } + }) + } +} + +func TestJSONRoundtrip(t *testing.T) { + var tests = []struct { + input string + bts *BootstrapTokenString + }{ + {`"abcdef.abcdef0123456789"`, nil}, + {"", &BootstrapTokenString{ID: "abcdef", Secret: "abcdef0123456789"}}, + } + for _, rt := range tests { + t.Run(rt.input, func(t *testing.T) { + if err := roundtrip(rt.input, rt.bts); err != nil { + t.Errorf("failed BootstrapTokenString JSON roundtrip with error: %v", err) + } + }) + } +} + +func roundtrip(input string, bts *BootstrapTokenString) error { + var b []byte + var err error + newbts := &BootstrapTokenString{} + // If string input was specified, roundtrip like this: string -> (unmarshal) -> object -> (marshal) -> string + if len(input) > 0 { + if err := json.Unmarshal([]byte(input), newbts); err != nil { + return errors.Wrap(err, "expected no unmarshal error, got error") + } + if b, err = json.Marshal(newbts); err != nil { + return errors.Wrap(err, "expected no marshal error, got error") + } + if input != string(b) { + return errors.Errorf( + "expected token: %s\n\t actual: %s", + input, + string(b), + ) + } + } else { // Otherwise, roundtrip like this: object -> (marshal) -> string -> (unmarshal) -> object + if b, err = json.Marshal(bts); err != nil { + return errors.Wrap(err, "expected no marshal error, got error") + } + if err := json.Unmarshal(b, newbts); err != nil { + return errors.Wrap(err, "expected no unmarshal error, got error") + } + if !reflect.DeepEqual(bts, newbts) { + return errors.Errorf( + "expected object: %v\n\t actual: %v", + bts, + newbts, + ) + } + } + return nil +} + +func TestTokenFromIDAndSecret(t *testing.T) { + var tests = []struct { + bts BootstrapTokenString + expected string + }{ + {BootstrapTokenString{ID: "foo", Secret: "bar"}, "foo.bar"}, + {BootstrapTokenString{ID: "abcdef", Secret: "abcdef0123456789"}, "abcdef.abcdef0123456789"}, + {BootstrapTokenString{ID: "h", Secret: "b"}, "h.b"}, + } + for _, rt := range tests { + t.Run(rt.bts.ID, func(t *testing.T) { + actual := rt.bts.String() + if actual != rt.expected { + t.Errorf( + "failed BootstrapTokenString.String():\n\texpected: %s\n\t actual: %s", + rt.expected, + actual, + ) + } + }) + } +} + +func TestNewBootstrapTokenString(t *testing.T) { + var tests = []struct { + token string + expectedError bool + bts *BootstrapTokenString + }{ + {token: "", expectedError: true, bts: nil}, + {token: ".", expectedError: true, bts: nil}, + {token: "1234567890123456789012", expectedError: true, bts: nil}, // invalid parcel size + {token: "12345.1234567890123456", expectedError: true, bts: nil}, // invalid parcel size + {token: ".1234567890123456", expectedError: true, bts: nil}, // invalid parcel size + {token: "123456.", expectedError: true, bts: nil}, // invalid parcel size + {token: "123456:1234567890.123456", expectedError: true, bts: nil}, // invalid separation + {token: "abcdef:1234567890123456", expectedError: true, bts: nil}, // invalid separation + {token: "Abcdef.1234567890123456", expectedError: true, bts: nil}, // invalid token id + {token: "123456.AABBCCDDEEFFGGHH", expectedError: true, bts: nil}, // invalid token secret + {token: "123456.AABBCCD-EEFFGGHH", expectedError: true, bts: nil}, // invalid character + {token: "abc*ef.1234567890123456", expectedError: true, bts: nil}, // invalid character + {token: "abcdef.1234567890123456", expectedError: false, bts: &BootstrapTokenString{ID: "abcdef", Secret: "1234567890123456"}}, + {token: "123456.aabbccddeeffgghh", expectedError: false, bts: &BootstrapTokenString{ID: "123456", Secret: "aabbccddeeffgghh"}}, + {token: "abcdef.abcdef0123456789", expectedError: false, bts: &BootstrapTokenString{ID: "abcdef", Secret: "abcdef0123456789"}}, + {token: "123456.1234560123456789", expectedError: false, bts: &BootstrapTokenString{ID: "123456", Secret: "1234560123456789"}}, + } + for _, rt := range tests { + t.Run(rt.token, func(t *testing.T) { + actual, err := NewBootstrapTokenString(rt.token) + if (err != nil) != rt.expectedError { + t.Errorf( + "failed NewBootstrapTokenString for the token %q\n\texpected error: %t\n\t actual error: %v", + rt.token, + rt.expectedError, + err, + ) + } else if !reflect.DeepEqual(actual, rt.bts) { + t.Errorf( + "failed NewBootstrapTokenString for the token %q\n\texpected: %v\n\t actual: %v", + rt.token, + rt.bts, + actual, + ) + } + }) + } +} + +func TestNewBootstrapTokenStringFromIDAndSecret(t *testing.T) { + var tests = []struct { + id, secret string + expectedError bool + bts *BootstrapTokenString + }{ + {id: "", secret: "", expectedError: true, bts: nil}, + {id: "1234567890123456789012", secret: "", expectedError: true, bts: nil}, // invalid parcel size + {id: "12345", secret: "1234567890123456", expectedError: true, bts: nil}, // invalid parcel size + {id: "", secret: "1234567890123456", expectedError: true, bts: nil}, // invalid parcel size + {id: "123456", secret: "", expectedError: true, bts: nil}, // invalid parcel size + {id: "Abcdef", secret: "1234567890123456", expectedError: true, bts: nil}, // invalid token id + {id: "123456", secret: "AABBCCDDEEFFGGHH", expectedError: true, bts: nil}, // invalid token secret + {id: "123456", secret: "AABBCCD-EEFFGGHH", expectedError: true, bts: nil}, // invalid character + {id: "abc*ef", secret: "1234567890123456", expectedError: true, bts: nil}, // invalid character + {id: "abcdef", secret: "1234567890123456", expectedError: false, bts: &BootstrapTokenString{ID: "abcdef", Secret: "1234567890123456"}}, + {id: "123456", secret: "aabbccddeeffgghh", expectedError: false, bts: &BootstrapTokenString{ID: "123456", Secret: "aabbccddeeffgghh"}}, + {id: "abcdef", secret: "abcdef0123456789", expectedError: false, bts: &BootstrapTokenString{ID: "abcdef", Secret: "abcdef0123456789"}}, + {id: "123456", secret: "1234560123456789", expectedError: false, bts: &BootstrapTokenString{ID: "123456", Secret: "1234560123456789"}}, + } + for _, rt := range tests { + t.Run(rt.id, func(t *testing.T) { + actual, err := NewBootstrapTokenStringFromIDAndSecret(rt.id, rt.secret) + if (err != nil) != rt.expectedError { + t.Errorf( + "failed NewBootstrapTokenStringFromIDAndSecret for the token with id %q and secret %q\n\texpected error: %t\n\t actual error: %v", + rt.id, + rt.secret, + rt.expectedError, + err, + ) + } else if !reflect.DeepEqual(actual, rt.bts) { + t.Errorf( + "failed NewBootstrapTokenStringFromIDAndSecret for the token with id %q and secret %q\n\texpected: %v\n\t actual: %v", + rt.id, + rt.secret, + rt.bts, + actual, + ) + } + }) + } +} + +// This timestamp is used as the reference value when computing expiration dates based on TTLs in these unit tests +var refTime = time.Date(1970, time.January, 1, 1, 1, 1, 0, time.UTC) + +func TestBootstrapTokenToSecret(t *testing.T) { + var tests = []struct { + bt *BootstrapToken + secret *v1.Secret + }{ + { + &BootstrapToken{ // all together + Token: &BootstrapTokenString{ID: "abcdef", Secret: "abcdef0123456789"}, + Description: "foo", + Expires: &metav1.Time{ + Time: refTime, + }, + Usages: []string{"signing", "authentication"}, + Groups: []string{"system:bootstrappers", "system:bootstrappers:foo"}, + }, + &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "bootstrap-token-abcdef", + Namespace: "kube-system", + }, + Type: v1.SecretType("bootstrap.kubernetes.io/token"), + Data: map[string][]byte{ + "token-id": []byte("abcdef"), + "token-secret": []byte("abcdef0123456789"), + "description": []byte("foo"), + "expiration": []byte(refTime.Format(time.RFC3339)), + "usage-bootstrap-signing": []byte("true"), + "usage-bootstrap-authentication": []byte("true"), + "auth-extra-groups": []byte("system:bootstrappers,system:bootstrappers:foo"), + }, + }, + }, + } + for _, rt := range tests { + t.Run(rt.bt.Token.ID, func(t *testing.T) { + actual := BootstrapTokenToSecret(rt.bt) + if !reflect.DeepEqual(actual, rt.secret) { + t.Errorf( + "failed BootstrapTokenToSecret():\n\texpected: %v\n\t actual: %v", + rt.secret, + actual, + ) + } + }) + } +} + +func TestBootstrapTokenToSecretRoundtrip(t *testing.T) { + var tests = []struct { + bt *BootstrapToken + }{ + { + &BootstrapToken{ + Token: &BootstrapTokenString{ID: "abcdef", Secret: "abcdef0123456789"}, + Description: "foo", + Expires: &metav1.Time{ + Time: refTime, + }, + Usages: []string{"authentication", "signing"}, + Groups: []string{"system:bootstrappers", "system:bootstrappers:foo"}, + }, + }, + } + for _, rt := range tests { + t.Run(rt.bt.Token.ID, func(t *testing.T) { + actual, err := BootstrapTokenFromSecret(BootstrapTokenToSecret(rt.bt)) + if err != nil { + t.Errorf("failed BootstrapToken to Secret roundtrip with error: %v", err) + } + if !reflect.DeepEqual(actual, rt.bt) { + t.Errorf( + "failed BootstrapToken to Secret roundtrip:\n\texpected: %v\n\t actual: %v", + rt.bt, + actual, + ) + } + }) + } +} + +func TestEncodeTokenSecretData(t *testing.T) { + var tests = []struct { + name string + bt *BootstrapToken + data map[string][]byte + }{ + { + "the minimum amount of information needed to be specified", + &BootstrapToken{ + Token: &BootstrapTokenString{ID: "abcdef", Secret: "abcdef0123456789"}, + }, + map[string][]byte{ + "token-id": []byte("abcdef"), + "token-secret": []byte("abcdef0123456789"), + }, + }, + { + "adds description", + &BootstrapToken{ + Token: &BootstrapTokenString{ID: "abcdef", Secret: "abcdef0123456789"}, + Description: "foo", + }, + map[string][]byte{ + "token-id": []byte("abcdef"), + "token-secret": []byte("abcdef0123456789"), + "description": []byte("foo"), + }, + }, + { + "adds ttl", + &BootstrapToken{ + Token: &BootstrapTokenString{ID: "abcdef", Secret: "abcdef0123456789"}, + TTL: &metav1.Duration{ + Duration: mustParseDuration("2h", t), + }, + }, + map[string][]byte{ + "token-id": []byte("abcdef"), + "token-secret": []byte("abcdef0123456789"), + "expiration": []byte(refTime.Add(mustParseDuration("2h", t)).Format(time.RFC3339)), + }, + }, + { + "adds expiration", + &BootstrapToken{ + Token: &BootstrapTokenString{ID: "abcdef", Secret: "abcdef0123456789"}, + Expires: &metav1.Time{ + Time: refTime, + }, + }, + map[string][]byte{ + "token-id": []byte("abcdef"), + "token-secret": []byte("abcdef0123456789"), + "expiration": []byte(refTime.Format(time.RFC3339)), + }, + }, + { + "adds ttl and expiration, should favor expiration", + &BootstrapToken{ + Token: &BootstrapTokenString{ID: "abcdef", Secret: "abcdef0123456789"}, + TTL: &metav1.Duration{ + Duration: mustParseDuration("2h", t), + }, + Expires: &metav1.Time{ + Time: refTime, + }, + }, + map[string][]byte{ + "token-id": []byte("abcdef"), + "token-secret": []byte("abcdef0123456789"), + "expiration": []byte(refTime.Format(time.RFC3339)), + }, + }, + { + "adds usages", + &BootstrapToken{ + Token: &BootstrapTokenString{ID: "abcdef", Secret: "abcdef0123456789"}, + Usages: []string{"authentication", "signing"}, + }, + map[string][]byte{ + "token-id": []byte("abcdef"), + "token-secret": []byte("abcdef0123456789"), + "usage-bootstrap-signing": []byte("true"), + "usage-bootstrap-authentication": []byte("true"), + }, + }, + { + "adds groups", + &BootstrapToken{ + Token: &BootstrapTokenString{ID: "abcdef", Secret: "abcdef0123456789"}, + Groups: []string{"system:bootstrappers", "system:bootstrappers:foo"}, + }, + map[string][]byte{ + "token-id": []byte("abcdef"), + "token-secret": []byte("abcdef0123456789"), + "auth-extra-groups": []byte("system:bootstrappers,system:bootstrappers:foo"), + }, + }, + { + "all together", + &BootstrapToken{ + Token: &BootstrapTokenString{ID: "abcdef", Secret: "abcdef0123456789"}, + Description: "foo", + TTL: &metav1.Duration{ + Duration: mustParseDuration("2h", t), + }, + Expires: &metav1.Time{ + Time: refTime, + }, + Usages: []string{"authentication", "signing"}, + Groups: []string{"system:bootstrappers", "system:bootstrappers:foo"}, + }, + map[string][]byte{ + "token-id": []byte("abcdef"), + "token-secret": []byte("abcdef0123456789"), + "description": []byte("foo"), + "expiration": []byte(refTime.Format(time.RFC3339)), + "usage-bootstrap-signing": []byte("true"), + "usage-bootstrap-authentication": []byte("true"), + "auth-extra-groups": []byte("system:bootstrappers,system:bootstrappers:foo"), + }, + }, + } + for _, rt := range tests { + t.Run(rt.name, func(t *testing.T) { + actual := encodeTokenSecretData(rt.bt, refTime) + if !reflect.DeepEqual(actual, rt.data) { + t.Errorf( + "failed encodeTokenSecretData:\n\texpected: %v\n\t actual: %v", + rt.data, + actual, + ) + } + }) + } +} + +func mustParseDuration(durationStr string, t *testing.T) time.Duration { + d, err := time.ParseDuration(durationStr) + if err != nil { + t.Fatalf("couldn't parse duration %q: %v", durationStr, err) + } + return d +} + +func TestBootstrapTokenFromSecret(t *testing.T) { + var tests = []struct { + desc string + name string + data map[string][]byte + bt *BootstrapToken + expectedError bool + }{ + { + "minimum information", + "bootstrap-token-abcdef", + map[string][]byte{ + "token-id": []byte("abcdef"), + "token-secret": []byte("abcdef0123456789"), + }, + &BootstrapToken{ + Token: &BootstrapTokenString{ID: "abcdef", Secret: "abcdef0123456789"}, + }, + false, + }, + { + "invalid token id", + "bootstrap-token-abcdef", + map[string][]byte{ + "token-id": []byte("abcdeF"), + "token-secret": []byte("abcdef0123456789"), + }, + nil, + true, + }, + { + "invalid secret naming", + "foo", + map[string][]byte{ + "token-id": []byte("abcdef"), + "token-secret": []byte("abcdef0123456789"), + }, + nil, + true, + }, + { + "invalid token secret", + "bootstrap-token-abcdef", + map[string][]byte{ + "token-id": []byte("abcdef"), + "token-secret": []byte("ABCDEF0123456789"), + }, + nil, + true, + }, + { + "adds description", + "bootstrap-token-abcdef", + map[string][]byte{ + "token-id": []byte("abcdef"), + "token-secret": []byte("abcdef0123456789"), + "description": []byte("foo"), + }, + &BootstrapToken{ + Token: &BootstrapTokenString{ID: "abcdef", Secret: "abcdef0123456789"}, + Description: "foo", + }, + false, + }, + { + "adds expiration", + "bootstrap-token-abcdef", + map[string][]byte{ + "token-id": []byte("abcdef"), + "token-secret": []byte("abcdef0123456789"), + "expiration": []byte(refTime.Format(time.RFC3339)), + }, + &BootstrapToken{ + Token: &BootstrapTokenString{ID: "abcdef", Secret: "abcdef0123456789"}, + Expires: &metav1.Time{ + Time: refTime, + }, + }, + false, + }, + { + "invalid expiration", + "bootstrap-token-abcdef", + map[string][]byte{ + "token-id": []byte("abcdef"), + "token-secret": []byte("abcdef0123456789"), + "expiration": []byte("invalid date"), + }, + nil, + true, + }, + { + "adds usages", + "bootstrap-token-abcdef", + map[string][]byte{ + "token-id": []byte("abcdef"), + "token-secret": []byte("abcdef0123456789"), + "usage-bootstrap-signing": []byte("true"), + "usage-bootstrap-authentication": []byte("true"), + }, + &BootstrapToken{ + Token: &BootstrapTokenString{ID: "abcdef", Secret: "abcdef0123456789"}, + Usages: []string{"authentication", "signing"}, + }, + false, + }, + { + "should ignore usages that aren't set to true", + "bootstrap-token-abcdef", + map[string][]byte{ + "token-id": []byte("abcdef"), + "token-secret": []byte("abcdef0123456789"), + "usage-bootstrap-signing": []byte("true"), + "usage-bootstrap-authentication": []byte("true"), + "usage-bootstrap-foo": []byte("false"), + "usage-bootstrap-bar": []byte(""), + }, + &BootstrapToken{ + Token: &BootstrapTokenString{ID: "abcdef", Secret: "abcdef0123456789"}, + Usages: []string{"authentication", "signing"}, + }, + false, + }, + { + "adds groups", + "bootstrap-token-abcdef", + map[string][]byte{ + "token-id": []byte("abcdef"), + "token-secret": []byte("abcdef0123456789"), + "auth-extra-groups": []byte("system:bootstrappers,system:bootstrappers:foo"), + }, + &BootstrapToken{ + Token: &BootstrapTokenString{ID: "abcdef", Secret: "abcdef0123456789"}, + Groups: []string{"system:bootstrappers", "system:bootstrappers:foo"}, + }, + false, + }, + { + "all fields set", + "bootstrap-token-abcdef", + map[string][]byte{ + "token-id": []byte("abcdef"), + "token-secret": []byte("abcdef0123456789"), + "description": []byte("foo"), + "expiration": []byte(refTime.Format(time.RFC3339)), + "usage-bootstrap-signing": []byte("true"), + "usage-bootstrap-authentication": []byte("true"), + "auth-extra-groups": []byte("system:bootstrappers,system:bootstrappers:foo"), + }, + &BootstrapToken{ + Token: &BootstrapTokenString{ID: "abcdef", Secret: "abcdef0123456789"}, + Description: "foo", + Expires: &metav1.Time{ + Time: refTime, + }, + Usages: []string{"authentication", "signing"}, + Groups: []string{"system:bootstrappers", "system:bootstrappers:foo"}, + }, + false, + }, + } + for _, rt := range tests { + t.Run(rt.desc, func(t *testing.T) { + actual, err := BootstrapTokenFromSecret(&v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: rt.name, + Namespace: "kube-system", + }, + Type: v1.SecretType("bootstrap.kubernetes.io/token"), + Data: rt.data, + }) + if (err != nil) != rt.expectedError { + t.Errorf( + "failed BootstrapTokenFromSecret\n\texpected error: %t\n\t actual error: %v", + rt.expectedError, + err, + ) + } else { + if actual == nil && rt.bt == nil { + // if both pointers are nil, it's okay, just continue + return + } + // If one of the pointers is defined but the other isn't, throw error. If both pointers are defined but unequal, throw error + if (actual == nil && rt.bt != nil) || (actual != nil && rt.bt == nil) || !reflect.DeepEqual(*actual, *rt.bt) { + t.Errorf( + "failed BootstrapTokenFromSecret\n\texpected: %s\n\t actual: %s", + jsonMarshal(rt.bt), + jsonMarshal(actual), + ) + } + } + }) + } +} + +func jsonMarshal(bt *BootstrapToken) string { + b, _ := json.Marshal(*bt) + return string(b) +} diff --git a/pkg/utils/kubernetes/token/bootstraptoken/v1/zz_generated.deepcopy.go b/pkg/utils/kubernetes/token/bootstraptoken/v1/zz_generated.deepcopy.go new file mode 100644 index 000000000..1dcf76cb9 --- /dev/null +++ b/pkg/utils/kubernetes/token/bootstraptoken/v1/zz_generated.deepcopy.go @@ -0,0 +1,66 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by deepcopy-gen. DO NOT EDIT. + +package v1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BootstrapToken) DeepCopyInto(out *BootstrapToken) { + *out = *in + if in.Token != nil { + in, out := &in.Token, &out.Token + *out = new(BootstrapTokenString) + **out = **in + } + if in.TTL != nil { + in, out := &in.TTL, &out.TTL + *out = new(metav1.Duration) + **out = **in + } + if in.Expires != nil { + in, out := &in.Expires, &out.Expires + *out = (*in).DeepCopy() + } + if in.Usages != nil { + in, out := &in.Usages, &out.Usages + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Groups != nil { + in, out := &in.Groups, &out.Groups + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BootstrapToken. +func (in *BootstrapToken) DeepCopy() *BootstrapToken { + if in == nil { + return nil + } + out := new(BootstrapToken) + in.DeepCopyInto(out) + return out +} diff --git a/pkg/utils/kubernetes/token/pins.go b/pkg/utils/kubernetes/token/pins.go new file mode 100644 index 000000000..c4c7d64bf --- /dev/null +++ b/pkg/utils/kubernetes/token/pins.go @@ -0,0 +1,111 @@ +/* +Copyright 2022 cuisongliu@qq.com. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package token + +import ( + "crypto/rand" + "crypto/sha256" + "crypto/x509" + "encoding/hex" + "github.com/pkg/errors" + "k8s.io/client-go/tools/clientcmd" + clientcmdapi "k8s.io/client-go/tools/clientcmd/api" + "k8s.io/client-go/util/cert" + "strings" +) + +func discoveryTokenCaCertHash() ([]string, error) { + tlsBootstrapCfg, err := clientcmd.LoadFromFile(defaultAdminConf) + if err != nil { + return nil, err + } + // load the default cluster config + clusterConfig := GetClusterFromKubeConfig(tlsBootstrapCfg) + if clusterConfig == nil { + return nil, errors.New("failed to get default cluster config") + } + + // load CA certificates from the kubeconfig (either from PEM data or by file path) + var caCerts []*x509.Certificate + if clusterConfig.CertificateAuthorityData != nil { + caCerts, err = cert.ParseCertsPEM(clusterConfig.CertificateAuthorityData) + if err != nil { + return nil, errors.Wrap(err, "failed to parse CA certificate from kubeconfig") + } + } else if clusterConfig.CertificateAuthority != "" { + caCerts, err = cert.CertsFromFile(clusterConfig.CertificateAuthority) + if err != nil { + return nil, errors.Wrap(err, "failed to load CA certificate referenced by kubeconfig") + } + } else { + return nil, errors.New("no CA certificates found in kubeconfig") + } + + // hash all the CA certs and include their public key pins as trusted values + publicKeyPins := make([]string, 0, len(caCerts)) + for _, caCert := range caCerts { + publicKeyPins = append(publicKeyPins, Hash(caCert)) + } + return publicKeyPins, nil +} + +// CreateRandBytes returns a cryptographically secure slice of random bytes with a given size +func CreateRandBytes(size uint32) ([]byte, error) { + bytes := make([]byte, size) + if _, err := rand.Read(bytes); err != nil { + return nil, err + } + return bytes, nil +} + +const ( + CertificateKeySize = 32 +) + +//CreateCertificateKey returns a cryptographically secure random key +func CreateCertificateKey() (string, error) { + randBytes, err := CreateRandBytes(CertificateKeySize) + if err != nil { + return "", err + } + return hex.EncodeToString(randBytes), nil +} + +// GetClusterFromKubeConfig returns the default Infra of the specified KubeConfig +func GetClusterFromKubeConfig(config *clientcmdapi.Config) *clientcmdapi.Cluster { + // If there is an unnamed cluster object, use it + if config.Clusters[""] != nil { + return config.Clusters[""] + } + if config.Contexts[config.CurrentContext] != nil { + return config.Clusters[config.Contexts[config.CurrentContext].Cluster] + } + return nil +} + +const ( + // formatSHA256 is the prefix for pins that are full-length SHA-256 hashes encoded in base 16 (hex) + formatSHA256 = "sha256" +) + +// Hash calculates the SHA-256 hash of the Subject Public Key Information (SPKI) +// object in an x509 certificate (in DER encoding). It returns the full hash as a +// hex encoded string (suitable for passing to Set.Allow). +func Hash(certificate *x509.Certificate) string { + spkiHash := sha256.Sum256(certificate.RawSubjectPublicKeyInfo) + return formatSHA256 + ":" + strings.ToLower(hex.EncodeToString(spkiHash[:])) +} diff --git a/pkg/utils/kubernetes/token/token.go b/pkg/utils/kubernetes/token/token.go index 7d2b1ecfe..6969c5b90 100644 --- a/pkg/utils/kubernetes/token/token.go +++ b/pkg/utils/kubernetes/token/token.go @@ -1,5 +1,5 @@ /* -Copyright 2021 cuisongliu@qq.com. +Copyright 2022 cuisongliu@qq.com. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,41 +17,111 @@ limitations under the License. package token import ( - "context" - - "github.com/fanux/sealos/pkg/utils/kubernetes/apiclient" - "github.com/fanux/sealos/pkg/utils/kubernetes/apis/kubeadm" - - "github.com/pkg/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - clientset "k8s.io/client-go/kubernetes" - bootstraputil "k8s.io/cluster-bootstrap/token/util" + "encoding/json" + "fmt" + "github.com/fanux/sealos/pkg/utils/exec" + "github.com/fanux/sealos/pkg/utils/file" + "github.com/fanux/sealos/pkg/utils/kubernetes/token/bootstraptoken/v1" + "github.com/fanux/sealos/pkg/utils/yaml" + "k8s.io/apimachinery/pkg/util/sets" + "time" ) -// CreateNewTokens tries to create a token and fails if one with the same ID already exists -func CreateNewTokens(client clientset.Interface, token kubeadm.BootstrapToken) error { - return UpdateOrCreateTokens(client, true, token) +type Token struct { + JoinToken string `json:"token,omitempty"` + DiscoveryTokenCaCertHash []string `json:"discovery-token-ca-cert-hash,omitempty"` + CertificateKey string `json:"certificate-key,omitempty"` + Command string `json:"command,omitempty"` + Message string `json:"message,omitempty"` } -// UpdateOrCreateTokens attempts to update a token with the given ID, or create if it does not already exist. -func UpdateOrCreateTokens(client clientset.Interface, failIfExists bool, token kubeadm.BootstrapToken) error { - secretName := bootstraputil.BootstrapTokenSecretName(token.Token.ID) - secret, err := client.CoreV1().Secrets(metav1.NamespaceSystem).Get(context.TODO(), secretName, metav1.GetOptions{}) - if secret != nil && err == nil && failIfExists { - return errors.Errorf("a token with id %q already exists", token.Token.ID) - } +const defaultAdminConf = "/etc/kubernetes/admin.conf" - updatedOrNewSecret := token.ToSecret() - // Try to create or update the token with an exponential backoff - err = apiclient.TryRunCommand(func() error { - if err := apiclient.CreateOrUpdateSecret(client, updatedOrNewSecret); err != nil { - return errors.Wrapf(err, "failed to create or update bootstrap token with name %s", secretName) +func Master() *Token { + token := &Token{} + if _, ok := exec.CheckCmdIsExist("kubeadm"); ok && file.IsExist(defaultAdminConf) { + key, _ := CreateCertificateKey() + token.CertificateKey = key + const uploadCertTemplate = "kubeadm init phase upload-certs --upload-certs --certificate-key %s" + _, _ = exec.RunBashCmd(fmt.Sprintf(uploadCertTemplate, key)) + const tokenTemplate = "kubeadm token create --print-join-command --certificate-key %s" + tokens := ListToken() + _, _ = exec.RunBashCmd(fmt.Sprintf(tokenTemplate, key)) + afterTokens := ListToken() + diff := afterTokens.ToStrings().Difference(tokens.ToStrings()) + if diff.Len() == 1 { + token.JoinToken = diff.List()[0] + hashs, err := discoveryTokenCaCertHash() + if err != nil { + token.Message = err.Error() + return token + } + token.DiscoveryTokenCaCertHash = hashs + return token } - return nil - }, 5) - - if err != nil { - return err + token.Message = fmt.Sprintf("token list found more than one") + } else { + token.Message = fmt.Sprintf("kubeadm command not found or /etc/kubernetes/admin.conf not exist") } - return nil + + return token +} + +func Node() *Token { + token := &Token{} + if _, ok := exec.CheckCmdIsExist("kubeadm"); ok && file.IsExist(defaultAdminConf) { + tokens := ListToken() + if len(tokens) > 0 { + token.JoinToken = tokens[0].Token.String() + hashs, err := discoveryTokenCaCertHash() + if err != nil { + token.Message = err.Error() + return token + } + return &Token{JoinToken: tokens[0].Token.String(), DiscoveryTokenCaCertHash: hashs} + } + token.Message = fmt.Sprintf("token not found") + } + token.Message = fmt.Sprintf("kubeadm command not found or /etc/kubernetes/admin.conf not exist") + return token +} + +func ListToken() BootstrapTokens { + const tokenListShell = "kubeadm token list -o yaml" + data, _ := exec.RunBashCmd(tokenListShell) + return processTokenList(data) +} + +func processTokenList(data string) BootstrapTokens { + var slice []v1.BootstrapToken + if data != "" { + jsons := yaml.ToJSON([]byte(data)) + for _, j := range jsons { + var to v1.BootstrapToken + _ = json.Unmarshal([]byte(j), &to) + slice = append(slice, to) + } + } + var result []v1.BootstrapToken + for _, token := range slice { + if token.Expires != nil { + t := time.Now().Unix() + ex := token.Expires.Time.Unix() + if ex < t { + continue + } + } + result = append(result, token) + } + return result +} + +type BootstrapTokens []v1.BootstrapToken + +func (c BootstrapTokens) ToStrings() sets.String { + s := sets.NewString() + for _, token := range c { + s.Insert(token.Token.String()) + } + return s } diff --git a/pkg/utils/logger/log.go b/pkg/utils/logger/log.go index 6fa602a05..f69306f37 100755 --- a/pkg/utils/logger/log.go +++ b/pkg/utils/logger/log.go @@ -149,7 +149,7 @@ func init() { defaultLogger = NewLogger(3) } -func Cfg(debugMod bool, logDir string) { +func Cfg(debugMod bool, logDir,name string) { debugMode = debugMod logLev := 5 if debugMod { @@ -164,7 +164,7 @@ func Cfg(debugMod bool, logDir string) { "LogLevel": %d }, "File": { - "filename": "%s/%s.log", + "filename": "%s/%s-%s.log", "level": "TRAC", "daily": true, "maxlines": 1000000, @@ -174,7 +174,7 @@ func Cfg(debugMod bool, logDir string) { "permit": "0660", "LogLevel":0 }}`, - logLev, logDir, time.Now().Format("2006-01-02"), + logLev, logDir,name, time.Now().Format("2006-01-02"), )) SetLogPath(true) diff --git a/pkg/utils/assh/README.md b/pkg/utils/ssh/README.md similarity index 100% rename from pkg/utils/assh/README.md rename to pkg/utils/ssh/README.md diff --git a/pkg/utils/ssh/connect.go b/pkg/utils/ssh/connect.go index c8bf68d1b..994632222 100644 --- a/pkg/utils/ssh/connect.go +++ b/pkg/utils/ssh/connect.go @@ -1,4 +1,4 @@ -// Copyright © 2021 sealos. +// Copyright © 2021 Alibaba Group Holding Ltd. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -17,88 +17,94 @@ package ssh import ( "fmt" "io/ioutil" + "net" "os" + "path" + "path/filepath" + "strconv" "strings" "time" + "github.com/fanux/sealos/pkg/utils/iputils" + "github.com/fanux/sealos/pkg/utils/logger" "golang.org/x/crypto/ssh" ) /** -这里主要是做连接ssh操作的 + SSH connection operation */ -func (ss *SSH) connect(host string) (*ssh.Client, error) { - auth := ss.sshAuthMethod(ss.Password, ss.PkFile, ss.PkPassword) +func (s *SSH) connect(host string) (*ssh.Client, error) { + auth := s.sshAuthMethod(s.Password, s.PkFile, s.PkPassword) config := ssh.Config{ Ciphers: []string{"aes128-ctr", "aes192-ctr", "aes256-ctr", "aes128-gcm@openssh.com", "arcfour256", "arcfour128", "aes128-cbc", "3des-cbc", "aes192-cbc", "aes256-cbc"}, } - DefaultTimeout := time.Duration(1) * time.Minute - if ss.Timeout == nil { - ss.Timeout = &DefaultTimeout + DefaultTimeout := time.Duration(15) * time.Second + if s.Timeout == nil { + s.Timeout = &DefaultTimeout } clientConfig := &ssh.ClientConfig{ - User: ss.User, - Auth: auth, - Timeout: *ss.Timeout, - Config: config, - HostKeyCallback: ssh.InsecureIgnoreHostKey(), + User: s.User, + Auth: auth, + Timeout: *s.Timeout, + Config: config, + HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error { + return nil + }, } - - addr := ss.addrReformat(host) + ip, port := iputils.GetSSHHostIPAndPort(host) + addr := s.addrReformat(ip, port) return ssh.Dial("tcp", addr, clientConfig) } -func (ss *SSH) Connect(host string) (*ssh.Session, error) { - client, err := ss.connect(host) +func (s *SSH) Connect(host string) (*ssh.Client, *ssh.Session, error) { + client, err := s.connect(host) if err != nil { - return nil, err + return nil, nil, err } session, err := client.NewSession() if err != nil { - return nil, err + _ = client.Close() + return nil, nil, err } modes := ssh.TerminalModes{ - ssh.ECHO: 0, // disable echoing + ssh.ECHO: 0, //disable echoing ssh.TTY_OP_ISPEED: 14400, // input speed = 14.4kbaud ssh.TTY_OP_OSPEED: 14400, // output speed = 14.4kbaud } if err := session.RequestPty("xterm", 80, 40, modes); err != nil { - return nil, err + _ = session.Close() + _ = client.Close() + return nil, nil, err } - return session, nil + return client, session, nil } -func (ss *SSH) sshAuthMethod(passwd, pkFile, pkPasswd string) (auth []ssh.AuthMethod) { - // pkfile存在, 就进行密钥验证, 如果不存在,则跳过密钥验证。 +func (s *SSH) sshAuthMethod(password, pkFile, pkPasswd string) (auth []ssh.AuthMethod) { if fileExist(pkFile) { - am, err := ss.sshPrivateKeyMethod(pkFile, pkPasswd) - // 获取到密钥验证就添加, 没获取到就直接跳过。 + am, err := s.sshPrivateKeyMethod(pkFile, pkPasswd) if err == nil { auth = append(auth, am) } } - // 密码不为空, 则添加密码验证。 - if passwd != "" { - auth = append(auth, ss.sshPasswordMethod(passwd)) + if password != "" { + auth = append(auth, s.sshPasswordMethod(password)) } - return auth } -func fileExist(path string) bool { - _, err := os.Stat(path) - return err == nil || os.IsExist(err) -} +//Authentication with a private key,private key has password and no password to verify in this +func (s *SSH) sshPrivateKeyMethod(pkFile, pkPassword string) (am ssh.AuthMethod, err error) { + pkData, err := ioutil.ReadFile(filepath.Clean(pkFile)) + if err != nil { + return nil, err + } -// 使用 pk认证, pk路径为 "$HOME/.ssh/id_rsa", pk有密码和无密码在这里面验证 -func (ss *SSH) sshPrivateKeyMethod(pkFile, pkPassword string) (am ssh.AuthMethod, err error) { - pkData := ss.readFile(pkFile) var pk ssh.Signer if pkPassword == "" { pk, err = ssh.ParsePrivateKey(pkData) @@ -115,24 +121,48 @@ func (ss *SSH) sshPrivateKeyMethod(pkFile, pkPassword string) (am ssh.AuthMethod return ssh.PublicKeys(pk), nil } -func (ss *SSH) sshPasswordMethod(passwd string) ssh.AuthMethod { - return ssh.Password(passwd) +func fileExist(path string) bool { + _, err := os.Stat(path) + return err == nil || os.IsExist(err) +} +func (s *SSH) sshPasswordMethod(password string) ssh.AuthMethod { + return ssh.Password(password) } -// readFile 从文件读取privateKey, 并返回[]byte 无需返回string。 -// 直接返回[]byte, 避免重复 []byte -> string -> []byte -func (ss *SSH) readFile(name string) []byte { - content, err := ioutil.ReadFile(name) - if err != nil { - logger.Error("[globals] read %s file err is : %s", name, err) - os.Exit(1) - } - return content -} - -func (ss *SSH) addrReformat(host string) string { +func (s *SSH) addrReformat(host, port string) string { if !strings.Contains(host, ":") { - host = fmt.Sprintf("%s:22", host) + host = fmt.Sprintf("%s:%s", host, port) } return host } + +//RemoteFileExist is +func (s *SSH) IsFileExist(host, remoteFilePath string) bool { + // if remote file is + // ls -l | grep aa | wc -l + remoteFileName := path.Base(remoteFilePath) // aa + remoteFileDirName := path.Dir(remoteFilePath) + //it's bug: if file is aa.bak, `ls -l | grep aa | wc -l` is 1 ,should use `ll aa 2>/dev/null |wc -l` + //remoteFileCommand := fmt.Sprintf("ls -l %s| grep %s | grep -v grep |wc -l", remoteFileDirName, remoteFileName) + remoteFileCommand := fmt.Sprintf("ls -l %s/%s 2>/dev/null |wc -l", remoteFileDirName, remoteFileName) + + data, err := s.CmdToString(host, remoteFileCommand, " ") + defer func() { + if r := recover(); r != nil { + logger.Error("[ssh][%s]remoteFileCommand err:%s", host, err) + } + }() + if err != nil { + panic(1) + } + count, err := strconv.Atoi(strings.TrimSpace(data)) + defer func() { + if r := recover(); r != nil { + logger.Error("[ssh][%s]RemoteFileExist:%s", host, err) + } + }() + if err != nil { + panic(1) + } + return count != 0 +} diff --git a/pkg/utils/ssh/hostname.go b/pkg/utils/ssh/hostname.go deleted file mode 100644 index 5a2a39230..000000000 --- a/pkg/utils/ssh/hostname.go +++ /dev/null @@ -1,50 +0,0 @@ -/* -Copyright 2021 cuisongliu@qq.com. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package ssh - -import ( - "strings" - - "github.com/fanux/sealos/pkg/utils/logger" -) - -//HostName 判断当前host的hostname -func HostName(sshConfig SSH, master, host string) string { - hostString := sshConfig.CmdToString(master, "kubectl get nodes | grep -v NAME | awk '{print $1}'", ",") - hostName := sshConfig.CmdToString(host, "hostname", "") - logger.Debug("hosts %v", hostString) - hosts := strings.Split(hostString, ",") - var name string - for _, h := range hosts { - if strings.TrimSpace(h) == "" { - continue - } else { - hh := strings.ToLower(h) - fromH := strings.ToLower(hostName) - if hh == fromH { - name = h - break - } - } - } - return name -} - -func RemoteHostName(sshConfig SSH, hostIP string) string { - hostName := sshConfig.CmdToString(hostIP, "hostname", "") - return strings.ToLower(hostName) -} diff --git a/pkg/utils/ssh/scp.go b/pkg/utils/ssh/scp.go index caddca79b..b7f81be78 100644 --- a/pkg/utils/ssh/scp.go +++ b/pkg/utils/ssh/scp.go @@ -1,4 +1,4 @@ -// Copyright © 2021 sealos. +// Copyright © 2021 Alibaba Group Holding Ltd. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -15,444 +15,244 @@ package ssh import ( - "bytes" "fmt" "io" "io/ioutil" - "net" "os" "path" "path/filepath" "strings" - "sync" - "time" "github.com/fanux/sealos/pkg/utils/hash" - "github.com/fanux/sealos/pkg/utils/http" + "github.com/fanux/sealos/pkg/utils/file" + "github.com/fanux/sealos/pkg/utils/iputils" "github.com/fanux/sealos/pkg/utils/logger" + "github.com/fanux/sealos/pkg/utils/progress" + "github.com/schollz/progressbar/v3" "github.com/pkg/sftp" "golang.org/x/crypto/ssh" ) -//Copy is -func (ss *SSH) CopyForMD5(host, localFilePath, remoteFilePath, md5 string) bool { - //如果有md5则可以验证 - //如果没有md5则拿到本地数据后验证 - if md5 == "" { - md5 = hash.FileMD5(localFilePath) - } - logger.Debug("[ssh]source file md5 value is %s", md5) - ss.Copy(host, localFilePath, remoteFilePath) - remoteMD5 := ss.Md5Sum(host, remoteFilePath) - logger.Debug("[ssh]host: %s , remote md5: %s", host, remoteMD5) - remoteMD5 = strings.TrimSpace(remoteMD5) - md5 = strings.TrimSpace(md5) - if remoteMD5 == md5 { - logger.Info("[ssh]md5 validate true") - return true - } - logger.Error("[ssh]md5 validate false") - return false -} - -func (ss *SSH) Md5Sum(host, remoteFilePath string) string { +func (s *SSH) RemoteMd5Sum(host, remoteFilePath string) string { cmd := fmt.Sprintf("md5sum %s | cut -d\" \" -f1", remoteFilePath) - remoteMD5 := ss.CmdToString(host, cmd, "") + remoteMD5, err := s.CmdToString(host, cmd, "") + if err != nil { + logger.Error("count remote md5 failed %s %s %v", host, remoteFilePath, err) + } return remoteMD5 } -//Copy is -func (ss *SSH) Copy(host, localFilePath, remoteFilePath string) { - sftpClient, err := ss.sftpConnect(host) - defer func() { - if r := recover(); r != nil { - logger.Error("[ssh][%s]scpCopy: %s", host, err) - } - }() +//CmdToString is in host exec cmd and replace to spilt str +func (s *SSH) CmdToString(host, cmd, spilt string) (string, error) { + data, err := s.Cmd(host, cmd) + str := string(data) if err != nil { - panic(1) + return str, fmt.Errorf("exec remote command failed %s %s %v", host, cmd, err) } - defer sftpClient.Close() - srcFile, err := os.Open(localFilePath) - defer func() { - if r := recover(); r != nil { - logger.Error("[ssh][%s]scpCopy: %s", host, err) - } - }() - if err != nil { - panic(1) - } - defer srcFile.Close() - - dstFile, err := sftpClient.Create(remoteFilePath) - defer func() { - if r := recover(); r != nil { - logger.Error("[ssh][%s]scpCopy: %s", host, err) - } - }() - if err != nil { - panic(1) - } - defer dstFile.Close() - buf := make([]byte, 100*oneMBByte) //100mb - total := 0 - unit := "" - for { - n, _ := srcFile.Read(buf) - if n == 0 { - break - } - length, _ := dstFile.Write(buf[0:n]) - isKb := length/oneMBByte < 1 - speed := 0 - if isKb { - total += length - unit = "KB" - speed = length / oneKBByte - } else { - total += length - unit = "MB" - speed = length / oneMBByte - } - totalLength, totalUnit := toSizeFromInt(total) - logger.Info("[ssh][%s]transfer total size is: %.2f%s ;speed is %d%s", host, totalLength, totalUnit, speed, unit) - } -} - -//Copy is -func (ss *SSH) CopyConfigFile(host, remoteFilePath string, localFilePathOrBytes interface{}) { - var ( - data io.Reader - ) - sftpClient, err := ss.sftpConnect(host) - defer func() { - if r := recover(); r != nil { - logger.Error("[ssh][%s]scpCopy: %s", host, err) - } - }() - if err != nil { - panic(1) - } - defer sftpClient.Close() - - switch v := localFilePathOrBytes.(type) { - case string: - srcFile, err := os.Open(v) - defer func() { - if r := recover(); r != nil { - logger.Error("[ssh][%s]scpCopy: %s", host, err) - } - }() - if err != nil { - panic(1) - } - defer srcFile.Close() - data = srcFile - case []byte: - data = bytes.NewReader(v) - default: - panic("must use path or []bytes") - } - - dstFile, err := sftpClient.Create(remoteFilePath) - defer func() { - if r := recover(); r != nil { - logger.Error("[ssh][%s]scpCopy: %s", host, err) - } - }() - if err != nil { - panic(1) - } - defer dstFile.Close() - buf := make([]byte, 100*oneMBByte) //100mb - totalMB := 0 - for { - n, _ := data.Read(buf) - if n == 0 { - break - } - length, _ := dstFile.Write(buf[0:n]) - totalMB += length / oneMBByte - logger.Info("[ssh][%s]transfer total size is: %d%s", host, totalMB, "MB") + if data != nil { + str = strings.ReplaceAll(str, "\r\n", spilt) + str = strings.ReplaceAll(str, "\n", spilt) + return str, nil } + return str, fmt.Errorf("command %s %s return nil", host, cmd) } //SftpConnect is -func (ss *SSH) sftpConnect(host string) (*sftp.Client, error) { - var ( - auth []ssh.AuthMethod - addr string - clientConfig *ssh.ClientConfig - sshClient *ssh.Client - sftpClient *sftp.Client - err error - ) - // get auth method - auth = ss.sshAuthMethod(ss.Password, ss.PkFile, ss.PkPassword) - - clientConfig = &ssh.ClientConfig{ - User: ss.User, - Auth: auth, - Timeout: 30 * time.Second, - HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error { - return nil - }, - Config: ssh.Config{ - Ciphers: []string{"aes128-ctr", "aes192-ctr", "aes256-ctr", "aes128-gcm@openssh.com", "arcfour256", "arcfour128", "aes128-cbc", "3des-cbc", "aes192-cbc", "aes256-cbc"}, - }, - } - - // connet to ssh - addr = ss.addrReformat(host) - - if sshClient, err = ssh.Dial("tcp", addr, clientConfig); err != nil { - return nil, err +func (s *SSH) sftpConnect(host string) (*ssh.Client, *sftp.Client, error) { + sshClient, err := s.connect(host) + if err != nil { + return nil, nil, err } // create sftp client - if sftpClient, err = sftp.NewClient(sshClient); err != nil { - return nil, err - } - - return sftpClient, nil + sftpClient, err := sftp.NewClient(sshClient) + return sshClient, sftpClient, err } // CopyRemoteFileToLocal is scp remote file to local -func (ss *SSH) CopyRemoteFileToLocal(host, localFilePath, remoteFilePath string) { - sftpClient, err := ss.sftpConnect(host) - defer func() { - if r := recover(); r != nil { - logger.Error("[ssh][%s]scpCopy: %s", host, err) +func (s *SSH) Fetch(host, localFilePath, remoteFilePath string) error { + if iputils.IsLocalIP(host, s.LocalAddress) { + if remoteFilePath != localFilePath { + logger.Debug("local copy files src %s to dst %s", remoteFilePath, localFilePath) + return file.RecursionCopy(remoteFilePath, localFilePath) } - }() - if err != nil { - panic(1) + return nil } - defer sftpClient.Close() + sshClient, sftpClient, err := s.sftpConnect(host) + if err != nil { + return fmt.Errorf("new sftp client failed %v", err) + } + defer func() { + _ = sftpClient.Close() + _ = sshClient.Close() + }() // open remote source file srcFile, err := sftpClient.Open(remoteFilePath) - defer func() { - if r := recover(); r != nil { - logger.Error("[ssh][%s]scpCopy: %s", host, err) - } - }() if err != nil { - panic(1) + return fmt.Errorf("open remote file failed %v, remote path: %s", err, remoteFilePath) } defer srcFile.Close() + err = file.MkFileFullPathDir(localFilePath) + if err != nil { + return err + } // open local Destination file dstFile, err := os.Create(localFilePath) - defer func() { - if r := recover(); r != nil { - logger.Error("[ssh][%s]scpCopy: %s", host, err) - } - }() if err != nil { - panic(1) + return fmt.Errorf("create local file failed %v", err) } defer dstFile.Close() // copy to local file - _, _ = srcFile.WriteTo(dstFile) + _, err = srcFile.WriteTo(dstFile) + return err } // CopyLocalToRemote is copy file or dir to remotePath, add md5 validate -func (ss *SSH) CopyLocalToRemote(host, localPath, remotePath string) { - sftpClient, err := ss.sftpConnect(host) - defer func() { - if r := recover(); r != nil { - logger.Error("[ssh][%s]sshConnect err: %s", host, err) - } - }() - if err != nil { - panic(1) +func (s *SSH) Copy(host, localPath, remotePath string) error { + if iputils.IsLocalIP(host, s.LocalAddress) { + logger.Debug("local copy files src %s to dst %s", localPath, remotePath) + return file.RecursionCopy(localPath, remotePath) } - sshClient, err := ss.connect(host) - defer func() { - if r := recover(); r != nil { - logger.Error("[ssh][%s]scpConnect err: %s", host, err) - } - }() + logger.Debug("remote copy files src %s to dst %s", localPath, remotePath) + sshClient, sftpClient, err := s.sftpConnect(host) if err != nil { - panic(1) + return fmt.Errorf("new sftp client failed %s", err) } - defer sftpClient.Close() - defer sshClient.Close() - s, _ := os.Stat(localPath) - if s.IsDir() { - ss.copyLocalDirToRemote(host, sshClient, sftpClient, localPath, remotePath) + defer func() { + _ = sftpClient.Close() + _ = sshClient.Close() + }() + + f, err := os.Stat(localPath) + if err != nil { + return fmt.Errorf("get file stat failed %s", err) + } + + baseRemoteFilePath := filepath.Dir(remotePath) + _, err = sftpClient.ReadDir(baseRemoteFilePath) + if err != nil { + if err = sftpClient.MkdirAll(baseRemoteFilePath); err != nil { + return err + } + } + number := 1 + if f.IsDir() { + number = file.CountDirFiles(localPath) + } + // no file in dir, do need to send + if number == 0 { + return nil + } + var ( + bar = progress.Simple("copying files to "+host, number) + ) + defer func() { + _ = bar.Close() + }() + + if f.IsDir() { + s.copyLocalDirToRemote(host, sftpClient, localPath, remotePath, bar) } else { - baseRemoteFilePath := filepath.Dir(remotePath) - mkDstDir := fmt.Sprintf("mkdir -p %s || true", baseRemoteFilePath) - _ = ss.CmdAsync(host, mkDstDir) - ss.copyLocalFileToRemote(host, sshClient, sftpClient, localPath, remotePath) + err = s.copyLocalFileToRemote(host, sftpClient, localPath, remotePath) + if err != nil { + errMsg := fmt.Sprintf("copy local file to remote failed %v %s %s %s", err, host, localPath, remotePath) + logger.Error(errMsg) + } + _ = bar.Add(1) } + return nil } -// ssh session is a problem, 复用ssh链接 -func (ss *SSH) copyLocalDirToRemote(host string, sshClient *ssh.Client, sftpClient *sftp.Client, localPath, remotePath string) { +func (s *SSH) copyLocalDirToRemote(host string, sftpClient *sftp.Client, localPath, remotePath string, epu *progressbar.ProgressBar) { localFiles, err := ioutil.ReadDir(localPath) - defer func() { - if r := recover(); r != nil { - logger.Error("readDir err : %s", err) - } - }() if err != nil { - panic(1) + logger.Error("read local path dir failed %s %s", host, localPath) + return + } + if err = sftpClient.MkdirAll(remotePath); err != nil { + logger.Error("failed to create remote path %s:%v", remotePath, err) + return } - _ = sftpClient.Mkdir(remotePath) for _, file := range localFiles { lfp := path.Join(localPath, file.Name()) rfp := path.Join(remotePath, file.Name()) if file.IsDir() { - _ = sftpClient.Mkdir(rfp) - ss.copyLocalDirToRemote(host, sshClient, sftpClient, lfp, rfp) + if err = sftpClient.MkdirAll(rfp); err != nil { + logger.Error("failed to create remote path %s:%v", rfp, err) + return + } + s.copyLocalDirToRemote(host, sftpClient, lfp, rfp, epu) } else { - ss.copyLocalFileToRemote(host, sshClient, sftpClient, lfp, rfp) + err := s.copyLocalFileToRemote(host, sftpClient, lfp, rfp) + if err != nil { + errMsg := fmt.Sprintf("copy local file to remote failed %v %s %s %s", err, host, lfp, rfp) + logger.Error(errMsg) + return + } + _ = epu.Add(1) } } } -// solve the session -func (ss *SSH) copyLocalFileToRemote(host string, sshClient *ssh.Client, sftpClient *sftp.Client, localPath, remotePath string) { - srcFile, err := os.Open(localPath) - defer func() { - if r := recover(); r != nil { - logger.Error("open file [%s] err : %s", localPath, err) +// check the remote file existence before copying +// solve the sesion +func (s *SSH) copyLocalFileToRemote(host string, sftpClient *sftp.Client, localPath, remotePath string) error { + var ( + srcMd5, dstMd5 string + ) + srcMd5 = hash.FileMD5(localPath) + if s.IsFileExist(host, remotePath) { + dstMd5 = s.RemoteMd5Sum(host, remotePath) + if srcMd5 == dstMd5 { + logger.Debug("remote dst %s already exists and is the latest version , skip copying process", remotePath) + return nil } - }() + } + srcFile, err := os.Open(filepath.Clean(localPath)) if err != nil { - panic(1) + return err } defer srcFile.Close() dstFile, err := sftpClient.Create(remotePath) if err != nil { - logger.Error("err:", err) + return err + } + fileStat, err := srcFile.Stat() + if err != nil { + return fmt.Errorf("get file stat failed %v", err) + } + // TODO seems not work + if err := dstFile.Chmod(fileStat.Mode()); err != nil { + return fmt.Errorf("chmod remote file failed %v", err) } defer dstFile.Close() - buf := make([]byte, 100*oneMBByte) //100mb - total := 0 - unit := "" - for { - n, _ := srcFile.Read(buf) - if n == 0 { - break - } - length, _ := dstFile.Write(buf[0:n]) - isKb := length/oneMBByte < 1 - speed := 0 - if isKb { - total += length - unit = "KB" - speed = length / oneKBByte - } else { - total += length - unit = "MB" - speed = length / oneMBByte - } - totalLength, totalUnit := toSizeFromInt(total) - logger.Debug("[ssh][%s]transfer local [%s] to Dst [%s] total size is: %.2f%s ;speed is %d%s", host, localPath, remotePath, totalLength, totalUnit, speed, unit) + _, err = io.Copy(dstFile, srcFile) + if err != nil { + return err } - if !ss.isCopyMd5Success(sshClient, localPath, remotePath) { - // logger.Debug("[ssh][%s] copy local file: %s to remote file: %s validate md5sum success", host, localPath, remotePath) - //} else { - logger.Error("[ssh][%s] copy local file: %s to remote file: %s validate md5sum failed", host, localPath, remotePath) + dstMd5 = s.RemoteMd5Sum(host, remotePath) + if srcMd5 != dstMd5 { + return fmt.Errorf("[ssh][%s] validate md5sum failed %s != %s", host, srcMd5, dstMd5) } + return nil } -func (ss *SSH) isCopyMd5Success(sshClient *ssh.Client, localFile, remoteFile string) bool { - cmd := fmt.Sprintf("md5sum %s | cut -d\" \" -f1", remoteFile) - localMd5 := hash.FileMD5(localFile) - sshSession, err := sshClient.NewSession() +//if remote file not exist return false and nil +func (s *SSH) RemoteDirExist(host, remoteDirpath string) (bool, error) { + sshClient, sftpClient, err := s.sftpConnect(host) if err != nil { - return false + return false, err } - modes := ssh.TerminalModes{ - ssh.ECHO: 0, // disable echoing - ssh.TTY_OP_ISPEED: 14400, // input speed = 14.4kbaud - ssh.TTY_OP_OSPEED: 14400, // output speed = 14.4kbaud - } - - if err := sshSession.RequestPty("xterm", 80, 40, modes); err != nil { - return false - } - - if sshSession.Stdout != nil { - sshSession.Stdout = nil - sshSession.Stderr = nil - } - b, err := sshSession.CombinedOutput(cmd) defer func() { - if r := recover(); r != nil { - logger.Error("[ssh]Error exec command failed: %s", err) - } + _ = sftpClient.Close() + _ = sshClient.Close() }() - if err != nil { - panic(1) + if _, err := sftpClient.ReadDir(remoteDirpath); err != nil { + return false, err } - var remoteMd5 string - if b != nil { - remoteMd5 = string(b) - remoteMd5 = strings.ReplaceAll(remoteMd5, "\r\n", "") - } - return localMd5 == remoteMd5 -} - -func (ss *SSH) ValidateMd5sumLocalWithRemote(host, localFile, remoteFile string) bool { - localMd5 := hash.FileMD5(localFile) - return localMd5 == ss.Md5Sum(host, remoteFile) -} - -//location : url -//md5 -//dst: /root -//hook: cd /root && rm -rf kube && tar zxvf %s && cd /root/kube/shell && sh init.sh -func CopyFiles(sshConfig SSH, location string, hosts []string, dst string, before, after *string) string { - var md5 string - location, md5 = http.DownloadFile(location) - pkg := path.Base(location) - fullPath := fmt.Sprintf("%s/%s", dst, pkg) - mkDstDir := fmt.Sprintf("mkdir -p %s || true", dst) - var wm sync.WaitGroup - for _, host := range hosts { - wm.Add(1) - go func(host string) { - defer wm.Done() - _ = sshConfig.CmdAsync(host, mkDstDir) - logger.Debug("[%s]please wait for mkDstDir", host) - if before != nil { - logger.Debug("[%s]please wait for before hook", host) - _ = sshConfig.CmdAsync(host, *before) - } - if sshConfig.IsFileExist(host, fullPath) { - if sshConfig.ValidateMd5sumLocalWithRemote(host, location, fullPath) { - logger.Info("[%s]CopyFiles: %s file is exist and ValidateMd5 success", host, fullPath) - } else { - rm := fmt.Sprintf("rm -f %s", fullPath) - _ = sshConfig.Cmd(host, rm) - // del then copy - if ok := sshConfig.CopyForMD5(host, location, fullPath, md5); ok { - logger.Info("[%s]copy file md5 validate success", host) - } else { - logger.Error("[%s]copy file md5 validate failed", host) - } - } - } else { - if ok := sshConfig.CopyForMD5(host, location, fullPath, md5); ok { - logger.Info("[%s]copy file md5 validate success", host) - } else { - logger.Error("[%s]copy file md5 validate failed", host) - } - } - if after != nil { - logger.Debug("[%s]please wait for after hook", host) - _ = sshConfig.CmdAsync(host, *after) - } - }(host) - } - wm.Wait() - return location + return true, nil } diff --git a/pkg/utils/ssh/scp_test.go b/pkg/utils/ssh/scp_test.go deleted file mode 100644 index b829a5164..000000000 --- a/pkg/utils/ssh/scp_test.go +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright © 2021 sealos. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package ssh - -import ( - "testing" - - "github.com/fanux/sealos/pkg/utils/logger" -) - -func TestSSHCopyLocalToRemote(t *testing.T) { - type args struct { - host string - localPath string - remotePath string - } - var ( - host = "192.168.160.243" - ssh = SSH{ - User: "root", - Password: "centos", - PkFile: "", - PkPassword: "", - Timeout: nil, - } - ) - tests := []struct { - name string - fields SSH - args args - }{ - {"test for copy file to remote server", ssh, args{ - host, - "/home/louis/temp/01", - "/data/temp/01", - }}, - {"test copy for file when local file is not exist", ssh, args{ - host, - // local file 001 is not exist. - "/home/louis/temp/001", - "/data/temp/01", - }}, - {"test copy dir to remote server", ssh, args{ - host, - "/home/louis/temp", - "/data/temp01", - }}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ss := &SSH{ - User: tt.fields.User, - Password: tt.fields.Password, - PkFile: tt.fields.PkFile, - PkPassword: tt.fields.PkPassword, - Timeout: tt.fields.Timeout, - } - - if !fileExist(tt.args.localPath) { - logger.Error("local filepath is not exit") - return - } - if ss.IsFileExist(host, tt.args.remotePath) { - logger.Error("remote filepath is exit") - return - } - // test copy dir - ss.CopyLocalToRemote(tt.args.host, tt.args.localPath, tt.args.remotePath) - - // test the copy result - ss.Cmd(tt.args.host, "ls -lh "+tt.args.remotePath) - - // rm remote file - ss.Cmd(tt.args.host, "rm -rf "+tt.args.remotePath) - }) - } -} diff --git a/pkg/utils/ssh/ssh.go b/pkg/utils/ssh/ssh.go index b89ce7829..b088c0fbe 100644 --- a/pkg/utils/ssh/ssh.go +++ b/pkg/utils/ssh/ssh.go @@ -1,4 +1,4 @@ -// Copyright © 2021 sealos. +// Copyright © 2021 Alibaba Group Holding Ltd. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -15,102 +15,142 @@ package ssh import ( - "bufio" - "io" - "strings" - - "github.com/fanux/sealos/pkg/utils/logger" + "fmt" + "net" + "sync" + "time" ) -//Cmd is in host exec cmd -func (ss *SSH) Cmd(host string, cmd string) []byte { - logger.Info("[ssh][%s] %s", host, cmd) - session, err := ss.Connect(host) - defer func() { - if r := recover(); r != nil { - logger.Error("[ssh][%s]Error create ssh session failed,%s", host, err) - } - }() - if err != nil { - panic(1) - } - defer session.Close() - b, err := session.CombinedOutput(cmd) - logger.Debug("[ssh][%s]command result is: %s", host, string(b)) - defer func() { - if r := recover(); r != nil { - logger.Error("[ssh][%s]Error exec command failed: %s", host, err) - } - }() - if err != nil { - panic(1) - } - return b +type Interface interface { + // Copy is copy local files to remote host + // scp -r /tmp root@192.168.0.2:/root/tmp => Copy("192.168.0.2","tmp","/root/tmp") + // need check md5sum + Copy(host, srcFilePath, dstFilePath string) error + // Fetch is copy remote host files to localhost + Fetch(host, srcFilePath, dstFilePath string) error + // CmdAsync is exec command on remote host, and asynchronous return logs + CmdAsync(host string, cmd ...string) error + // Cmd is exec command on remote host, and return combined standard output and standard error + Cmd(host, cmd string) ([]byte, error) + // IsFileExist is check remote file exist or not + IsFileExist(host, remoteFilePath string) bool + //RemoteDirExist is remote file existence returns true, nil + RemoteDirExist(host, remoteDirpath string) (bool, error) + //CmdToString is exec command on remote host, and return spilt standard output and standard error + CmdToString(host, cmd, spilt string) (string, error) + Ping(host string) error } -func readPipe(host string, pipe io.Reader, isErr bool) { - r := bufio.NewReader(pipe) - for { - line, _, err := r.ReadLine() - if line == nil { - return - } else if err != nil { - logger.Info("[%s] %s", host, line) - logger.Error("[ssh] [%s] %s", host, err) - return - } else { - if isErr { - logger.Error("[%s] %s", host, line) - } else { - logger.Info("[%s] %s", host, line) +type SSH struct { + User string + Password string + PkFile string + PkPassword string + Timeout *time.Duration + LocalAddress *[]net.Addr +} + +//func NewSSHByCluster(cluster *v1.Cluster) Interface { +// if cluster.Spec.SSH.User == "" { +// cluster.Spec.SSH.User = common.ROOT +// } +// address, err := utils.IsLocalHostAddrs() +// if err != nil { +// logger.Warn("failed to get local address, %v", err) +// } +// return &SSH{ +// User: cluster.Spec.SSH.User, +// Password: cluster.Spec.SSH.Passwd, +// PkFile: cluster.Spec.SSH.Pk, +// PkPassword: cluster.Spec.SSH.PkPasswd, +// LocalAddress: address, +// } +//} +// +//func NewSSHClient(ssh *v1.SSH) Interface { +// if ssh.User == "" { +// ssh.User = common.ROOT +// } +// address, err := utils.IsLocalHostAddrs() +// if err != nil { +// logger.Warn("failed to get local address, %v", err) +// } +// return &SSH{ +// User: ssh.User, +// Password: ssh.Passwd, +// PkFile: ssh.Pk, +// PkPassword: ssh.PkPasswd, +// LocalAddress: address, +// } +//} +// +//func GetHostSSHClient(hostIP string, cluster *v2.Cluster) (Interface, error) { +// for _, host := range cluster.Spec.Hosts { +// for _, ip := range host.IPS { +// if hostIP == ip { +// if err := mergo.Merge(&host.SSH, &cluster.Spec.SSH); err != nil { +// return nil, err +// } +// +// return NewSSHClient(&host.SSH), nil +// } +// } +// } +// return nil, fmt.Errorf("get host ssh client failed, host ip %s not in hosts ip list", hostIP) +//} +// + +type Client struct { + SSH Interface + Host string +} +// +//func NewSSHClientWithCluster(cluster *v1.Cluster) (*Client, error) { +// var ( +// ipList []string +// host string +// ) +// sshClient := NewSSHByCluster(cluster) +// if cluster.Spec.Provider == common.AliCloud { +// host = cluster.GetAnnotationsByKey(common.Eip) +// if host == "" { +// return nil, fmt.Errorf("get cluster EIP failed") +// } +// ipList = append(ipList, host) +// } else { +// host = cluster.Spec.Masters.IPList[0] +// ipList = append(ipList, append(cluster.Spec.Masters.IPList, cluster.Spec.Nodes.IPList...)...) +// } +// err := WaitSSHReady(sshClient, 6, ipList...) +// if err != nil { +// return nil, err +// } +// if sshClient == nil { +// return nil, fmt.Errorf("cloud build init ssh client failed") +// } +// return &Client{ +// SSH: sshClient, +// Host: host, +// }, nil +//} + +func WaitSSHReady(ssh Interface, tryTimes int, hosts ...string) error { + var err error + var wg sync.WaitGroup + for _, h := range hosts { + wg.Add(1) + go func(host string) { + defer wg.Done() + for i := 0; i < tryTimes; i++ { + err = ssh.Ping(host) + if err == nil { + return + } + time.Sleep(time.Duration(i) * time.Second) } - } + err = fmt.Errorf("wait for [%s] ssh ready timeout: %v, ensure that the IP address or password is correct", host, err) + }(h) } -} - -func (ss *SSH) CmdAsync(host string, cmd string) error { - logger.Debug("[%s] %s", host, cmd) - session, err := ss.Connect(host) - if err != nil { - logger.Error("[ssh][%s]Error create ssh session failed,%s", host, err) - return err - } - defer session.Close() - stdout, err := session.StdoutPipe() - if err != nil { - logger.Error("[ssh][%s]Unable to request StdoutPipe(): %s", host, err) - return err - } - stderr, err := session.StderrPipe() - if err != nil { - logger.Error("[ssh][%s]Unable to request StderrPipe(): %s", host, err) - return err - } - if err := session.Start(cmd); err != nil { - logger.Error("[ssh][%s]Unable to execute command: %s", host, err) - return err - } - doneout := make(chan bool, 1) - doneerr := make(chan bool, 1) - go func() { - readPipe(host, stderr, true) - doneerr <- true - }() - go func() { - readPipe(host, stdout, false) - doneout <- true - }() - <-doneerr - <-doneout - return session.Wait() -} - -//CmdToString is in host exec cmd and replace to spilt str -func (ss *SSH) CmdToString(host, cmd, spilt string) string { - if data := ss.Cmd(host, cmd); data != nil { - str := string(data) - str = strings.ReplaceAll(str, "\r\n", spilt) - return str - } - return "" + wg.Wait() + return err } diff --git a/pkg/utils/assh/sshcmd.go b/pkg/utils/ssh/sshcmd.go similarity index 100% rename from pkg/utils/assh/sshcmd.go rename to pkg/utils/ssh/sshcmd.go diff --git a/pkg/utils/assh/sshcmd_test.go b/pkg/utils/ssh/sshcmd_test.go similarity index 92% rename from pkg/utils/assh/sshcmd_test.go rename to pkg/utils/ssh/sshcmd_test.go index 51bc1ed69..2ebbaee7d 100644 --- a/pkg/utils/assh/sshcmd_test.go +++ b/pkg/utils/ssh/sshcmd_test.go @@ -189,8 +189,24 @@ func TestSSH_CmdAsync(t *testing.T) { }, wantErr: true, //Process exited with status 1 }, + { + name: "exist 1", + args: args{ + ssh: SSH{ + "root", + "", + "/Users/cuisongliu/.ssh/id_rsa", + "", + nil, + &[]net.Addr{}, + }, + host: "192.168.64.15", + cmd: "cd kube/scripts && bash check-node.sh", + }, + wantErr: true, //Process exited with status 1 + }, } - + DebugMode = true for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if err := tt.args.ssh.CmdAsync(tt.args.host, tt.args.cmd); (err != nil) != tt.wantErr { diff --git a/pkg/utils/ssh/watch.go b/pkg/utils/ssh/watch.go deleted file mode 100644 index c0d40e6c0..000000000 --- a/pkg/utils/ssh/watch.go +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright © 2021 sealos. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package ssh - -import ( - "fmt" - "path" - "strconv" - "strings" - "time" - - "github.com/fanux/sealos/pkg/utils/logger" -) - -const oneKBByte = 1024 -const oneMBByte = 1024 * 1024 - -//WatchFileSize is -func (ss *SSH) LoggerFileSize(host, filename string, size int) { - t := time.NewTicker(3 * time.Second) //every 3s check file - defer t.Stop() - for { - if <-t.C; true { - length := ss.CmdToString(host, "ls -l "+filename+" | awk '{print $5}'", "") - length = strings.Replace(length, "\n", "", -1) - length = strings.Replace(length, "\r", "", -1) - lengthByte, _ := strconv.Atoi(length) - if lengthByte == size { - t.Stop() - } - lengthFloat := float64(lengthByte) - value, _ := strconv.ParseFloat(fmt.Sprintf("%.2f", lengthFloat/oneMBByte), 64) - logger.Info("[ssh][%s]transfer total size is: %.2f%s", host, value, "MB") - } - } -} - -//RemoteFileExist is -func (ss *SSH) IsFileExist(host, remoteFilePath string) bool { - // if remote file is - // ls -l | grep aa | wc -l - remoteFileName := path.Base(remoteFilePath) // aa - remoteFileDirName := path.Dir(remoteFilePath) - //it's bug: if file is aa.bak, `ls -l | grep aa | wc -l` is 1 ,should use `ll aa 2>/dev/null |wc -l` - //remoteFileCommand := fmt.Sprintf("ls -l %s| grep %s | grep -v grep |wc -l", remoteFileDirName, remoteFileName) - remoteFileCommand := fmt.Sprintf("ls -l %s/%s 2>/dev/null |wc -l", remoteFileDirName, remoteFileName) - - data := ss.CmdToString(host, remoteFileCommand, " ") - count, err := strconv.Atoi(strings.TrimSpace(data)) - defer func() { - if r := recover(); r != nil { - logger.Error("[ssh][%s]RemoteFileExist:%s", host, err) - } - }() - if err != nil { - panic(1) - } - return count != 0 -} - -func toSizeFromInt(length int) (float64, string) { - value, _ := strconv.ParseFloat(fmt.Sprintf("%.2f", float64(length)/oneMBByte), 64) - if isMb := length/oneMBByte > 1; isMb { - return value, "MB" - } - value, _ = strconv.ParseFloat(fmt.Sprintf("%.2f", float64(length)/oneKBByte), 64) - return value, "KB" -} diff --git a/pkg/utils/yaml/yaml.go b/pkg/utils/yaml/yaml.go new file mode 100644 index 000000000..f129047f9 --- /dev/null +++ b/pkg/utils/yaml/yaml.go @@ -0,0 +1,40 @@ +/* +Copyright 2022 cuisongliu@qq.com. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package yaml + +import ( + "bytes" + "io" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/yaml" +) + +func ToJSON(bs []byte) (jsons []string) { + reader := bytes.NewReader(bs) + ext := runtime.RawExtension{} + d := yaml.NewYAMLOrJSONDecoder(reader, 4096) + for { + if err := d.Decode(&ext); err != nil { + if err == io.EOF { + break + } + break + } + jsons = append(jsons, string(ext.Raw)) + } + return +}