Files
Olares/cli/pkg/common/kube_runtime.go

566 lines
18 KiB
Go
Executable File

/*
Copyright 2021 The KubeSphere 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 common
import (
"encoding/json"
"fmt"
"net"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/joho/godotenv"
"github.com/pkg/errors"
"github.com/spf13/pflag"
kubekeyapiv1alpha2 "github.com/beclab/Olares/cli/apis/kubekey/v1alpha2"
"github.com/beclab/Olares/cli/pkg/core/common"
"github.com/beclab/Olares/cli/pkg/core/connector"
"github.com/beclab/Olares/cli/pkg/core/logger"
"github.com/beclab/Olares/cli/pkg/core/util"
kresource "k8s.io/apimachinery/pkg/api/resource"
)
type KubeRuntime struct {
connector.BaseRuntime
ClusterName string
Cluster *kubekeyapiv1alpha2.ClusterSpec
Kubeconfig string
Arg *Argument
}
type Argument struct {
NodeName string `json:"node_name"`
FilePath string `json:"file_path"`
KubernetesVersion string `json:"kubernetes_version"`
KsEnable bool `json:"ks_enable"`
KsVersion string `json:"ks_version"`
OlaresVersion string `json:"olares_version"`
Debug bool `json:"debug"`
IgnoreErr bool `json:"ignore_err"`
SkipPullImages bool `json:"skip_pull_images"`
SKipPushImages bool `json:"skip_push_images"`
SecurityEnhancement bool `json:"security_enhancement"`
DeployLocalStorage *bool `json:"deploy_local_storage"`
// DownloadCommand func(path, url string) string
SkipConfirmCheck bool `json:"skip_confirm_check"`
InCluster bool `json:"in_cluster"`
ContainerManager string `json:"container_manager"`
FromCluster bool `json:"from_cluster"`
KubeConfig string `json:"kube_config"`
Artifact string `json:"artifact"`
InstallPackages bool `json:"install_packages"`
ImagesDir string `json:"images_dir"`
Namespace string `json:"namespace"`
DeleteCRI bool `json:"delete_cri"`
Role string `json:"role"`
Type string `json:"type"`
Kubetype string `json:"kube_type"`
SystemInfo connector.Systems
// Extra args
ExtraAddon string `json:"extra_addon"` // addon yaml config
RegistryMirrors string `json:"registry_mirrors"`
OlaresCDNService string `json:"olares_cdn_service"`
// Swap config
*SwapConfig
// master node ssh config
*MasterHostConfig
LocalSSHPort int `json:"-"`
SkipMasterPullImages bool `json:"skip_master_pull_images"`
// User
User *User `json:"user"`
// if juicefs is opted off, the local storage is used directly
// only used in prepare phase
// the existence of juicefs should be checked in other phases
// to avoid wrong information given by user
WithJuiceFS bool `json:"with_juicefs"`
// the object storage service used as backend for JuiceFS
Storage *Storage `json:"storage"`
NetworkSettings *NetworkSettings `json:"network_settings"`
GPU *GPU `json:"gpu"`
Request any `json:"-"`
IsCloudInstance bool `json:"is_cloud_instance"`
MinikubeProfile string `json:"minikube_profile"`
WSLDistribution string `json:"wsl_distribution"`
Environment []string `json:"environment"`
BaseDir string `json:"base_dir"`
Manifest string `json:"manifest"`
ConsoleLogFileName string `json:"console_log_file_name"`
ConsoleLogTruncate bool `json:"console_log_truncate"`
HostIP string `json:"host_ip"`
CudaVersion string `json:"cuda_version"`
IsOlaresInContainer bool `json:"is_olares_in_container"`
}
type SwapConfig struct {
EnablePodSwap bool `json:"enable_pod_swap"`
Swappiness int `json:"swappiness"`
EnableZRAM bool `json:"enable_zram"`
ZRAMSize string `json:"zram_size"`
ZRAMSwapPriority int `json:"zram_swap_priority"`
}
func (cfg *SwapConfig) AddFlags(fs *pflag.FlagSet) {
fs.BoolVar(&cfg.EnablePodSwap, "enable-pod-swap", false, "Enable pods on Kubernetes cluster to use swap, setting --enable-zram, --zram-size or --zram-swap-priority implicitly enables this option, regardless of the command line args, note that only pods of the BestEffort QOS group can use swap due to K8s design")
fs.IntVar(&cfg.Swappiness, "swappiness", 0, "Configure the Linux swappiness value, if not set, the current configuration is remained")
fs.BoolVar(&cfg.EnableZRAM, "enable-zram", false, "Set up a ZRAM device to be used for swap, setting --zram-size or --zram-swap-priority implicitly enables this option, regardless of the command line args")
fs.StringVar(&cfg.ZRAMSize, "zram-size", "", "Set the size of the ZRAM device, takes a format of https://kubernetes.io/docs/reference/kubernetes-api/common-definitions/quantity, defaults to half of the total RAM")
fs.IntVar(&cfg.ZRAMSwapPriority, "zram-swap-priority", 0, "Set the swap priority of the ZRAM device, between -1 and 32767, defaults to 100")
}
func (cfg *SwapConfig) Validate() error {
if cfg.ZRAMSize == "" {
return nil
}
processedZRAMSize := cfg.ZRAMSize
if strings.HasSuffix(processedZRAMSize, "b") || strings.HasSuffix(processedZRAMSize, "B") {
processedZRAMSize = strings.TrimSuffix(cfg.ZRAMSize, "b")
processedZRAMSize = strings.TrimSuffix(cfg.ZRAMSize, "B")
}
processedZRAMSize = strings.ReplaceAll(processedZRAMSize, "g", "G")
processedZRAMSize = strings.ReplaceAll(processedZRAMSize, "k", "K")
processedZRAMSize = strings.ReplaceAll(processedZRAMSize, "m", "M")
q, err := kresource.ParseQuantity(processedZRAMSize)
if err != nil {
return fmt.Errorf("invalid zram size %s: %w", cfg.ZRAMSize, err)
}
cfg.ZRAMSize = q.String() + "B"
return nil
}
type MasterHostConfig struct {
MasterHost string `json:"master_host"`
MasterNodeName string `json:"master_node_name"`
MasterSSHUser string `json:"master_ssh_user"`
MasterSSHPassword string `json:"master_ssh_password"`
MasterSSHPrivateKeyPath string `json:"master_ssh_private_key_path"`
MasterSSHPort int `json:"master_ssh_port"`
}
func (cfg *MasterHostConfig) AddFlags(fs *pflag.FlagSet) {
fs.StringVar(&cfg.MasterHost, "master-host", "", "IP address of the master node")
fs.StringVar(&cfg.MasterNodeName, "master-node-name", "", "Name of the master node")
fs.StringVar(&cfg.MasterSSHUser, "master-ssh-user", "", "Username of the master node, defaults to root")
fs.StringVar(&cfg.MasterSSHPassword, "master-ssh-password", "", "Password of the master node")
fs.StringVar(&cfg.MasterSSHPrivateKeyPath, "master-ssh-private-key-path", "", "Path to the SSH key to access the master node, defaults to ~/.ssh/id_rsa")
fs.IntVar(&cfg.MasterSSHPort, "master-ssh-port", 0, "SSH Port of the master node, defaults to 22")
}
func (cfg *MasterHostConfig) Validate() error {
if cfg.MasterHost == "" {
return errors.New("--master-host is not provided")
}
if cfg.MasterSSHUser != "" && cfg.MasterSSHUser != "root" && cfg.MasterSSHPassword == "" {
return errors.New("--master-ssh-password must be provided for non-root user in order to execute sudo command")
}
return nil
}
type NetworkSettings struct {
// OSPublicIPs contains a list of public ip(s)
// by looking at local network interfaces
// if any
OSPublicIPs []net.IP `json:"os_public_ips"`
// CloudProviderPublicIP contains the info retrieved from the cloud provider instance metadata service
// if any
CloudProviderPublicIP net.IP `json:"cloud_provider_public_ip"`
// ExternalPublicIP is the IP address seen by others on the internet
// it may not be an IP address
// that's directly bound to a local network interface, e.g. on an AWS EC2 instance
// or may not be an IP address
// that can be used to access the machine at all, e.g. a machine behind multiple NAT gateways
// this is used as a fallback method to determine the machine's public IP address
// if none can be found from the OS or AWS IMDS service
// but the user explicitly specifies that the machine is publicly accessible
ExternalPublicIP net.IP `json:"external_public_ip"`
EnableReverseProxy *bool `json:"enable_reverse_proxy"`
}
type User struct {
UserName string `json:"user_name"`
Password string `json:"user_password"`
EncryptedPassword string `json:"-"`
Email string `json:"user_email"`
DomainName string `json:"user_domain_name"`
}
type Storage struct {
StorageVendor string `json:"storage_vendor"`
StorageType string `json:"storage_type"`
StorageBucket string `json:"storage_bucket"`
StoragePrefix string `json:"storage_prefix"`
StorageAccessKey string `json:"storage_access_key"`
StorageSecretKey string `json:"storage_secret_key"`
StorageToken string `json:"storage_token"` // juicefs --> from env
StorageClusterId string `json:"storage_cluster_id"` // use only on the Terminus cloud, juicefs --> from env
StorageSyncSecret string `json:"storage_sync_secret"` // use only on the Terminus cloud --> from env
BackupClusterBucket string `json:"backup_cluster_bucket"`
}
type GPU struct {
Enable bool `json:"gpu_enable"`
}
func NewArgument() *Argument {
si := connector.GetSystemInfo()
arg := &Argument{
KsEnable: true,
KsVersion: DefaultKubeSphereVersion,
InstallPackages: false,
SKipPushImages: false,
ContainerManager: Containerd,
SystemInfo: si,
Storage: &Storage{
StorageType: ManagedMinIO,
},
GPU: &GPU{
Enable: !strings.EqualFold(os.Getenv(ENV_LOCAL_GPU_ENABLE), "0"), // default enable GPU, not set or 1 means enable
},
User: &User{},
NetworkSettings: &NetworkSettings{},
RegistryMirrors: os.Getenv(ENV_REGISTRY_MIRRORS),
OlaresCDNService: os.Getenv(ENV_OLARES_CDN_SERVICE),
HostIP: os.Getenv(ENV_HOST_IP),
Environment: os.Environ(),
MasterHostConfig: &MasterHostConfig{},
SwapConfig: &SwapConfig{},
}
arg.IsCloudInstance, _ = strconv.ParseBool(os.Getenv(ENV_TERMINUS_IS_CLOUD_VERSION))
arg.IsOlaresInContainer = os.Getenv("CONTAINER_MODE") == "oic"
si.IsOIC = arg.IsOlaresInContainer
if err := arg.LoadReleaseInfo(); err != nil {
fmt.Printf("error loading release info: %v", err)
os.Exit(1)
}
return arg
}
// LoadReleaseInfo loads base directory and version settings
// from /etc/olares/release and environment variables,
// with the latter takes precedence.
// Note that the command line options --base-dir and --version
// still have the highest priority and will override any values loaded here
func (a *Argument) LoadReleaseInfo() error {
// load envs from the release file
// already existing envs are not overridden so
err := godotenv.Load(OlaresReleaseFile)
// silently ignore the non-existence of a release file
// otherwise, return the error
if err != nil && !os.IsNotExist(err) {
return err
}
a.BaseDir = os.Getenv(ENV_OLARES_BASE_DIR)
a.OlaresVersion = os.Getenv(ENV_OLARES_VERSION)
return nil
}
func (a *Argument) SaveReleaseInfo(withoutName bool) error {
if a.BaseDir == "" {
return errors.New("invalid: empty base directory")
}
if a.OlaresVersion == "" {
return errors.New("invalid: empty olares version")
}
releaseInfoMap := map[string]string{
ENV_OLARES_BASE_DIR: a.BaseDir,
ENV_OLARES_VERSION: a.OlaresVersion,
}
if !withoutName {
if a.User != nil && a.User.UserName != "" && a.User.DomainName != "" {
releaseInfoMap["OLARES_NAME"] = fmt.Sprintf("%s@%s", a.User.UserName, a.User.DomainName)
} else {
if util.IsExist(OlaresReleaseFile) {
// if the user is not set, try to load the user name from the release file
envs, err := godotenv.Read(OlaresReleaseFile)
if err == nil {
if userName, ok := envs["OLARES_NAME"]; ok {
releaseInfoMap["OLARES_NAME"] = userName
}
}
}
}
}
if !util.IsExist(filepath.Dir(OlaresReleaseFile)) {
if err := os.MkdirAll(filepath.Dir(OlaresReleaseFile), 0755); err != nil {
return fmt.Errorf("failed to create directory %s: %v", filepath.Dir(OlaresReleaseFile), err)
}
}
return godotenv.Write(releaseInfoMap, OlaresReleaseFile)
}
func (a *Argument) GetWslUserPath() string {
if a.Environment == nil || len(a.Environment) == 0 {
return ""
}
var res string
var wslSuffix = "/AppData/Local/Microsoft/WindowsApps"
for _, v := range a.Environment {
if strings.HasPrefix(v, "PATH=") {
p := strings.ReplaceAll(v, "PATH=", "")
s := strings.Split(p, ":")
for _, s1 := range s {
if strings.Contains(s1, wslSuffix) {
res = strings.ReplaceAll(s1, wslSuffix, "")
break
}
}
}
}
return res
}
func (a *Argument) SetOlaresCDNService(url string) {
u := strings.TrimSuffix(url, "/")
if u == "" {
u = common.DefaultOlaresCDNService
}
a.OlaresCDNService = u
}
func (a *Argument) SetGPU(enable bool) {
if a.GPU == nil {
a.GPU = new(GPU)
}
a.GPU.Enable = enable
}
func (a *Argument) SetOlaresVersion(version string) {
if version == "" || len(version) <= 2 {
return
}
if version[0] == 'v' {
version = version[1:]
}
a.OlaresVersion = version
}
func (a *Argument) SetRegistryMirrors(registryMirrors string) {
a.RegistryMirrors = registryMirrors
}
func (a *Argument) SetDeleteCRI(deleteCRI bool) {
a.DeleteCRI = deleteCRI
}
func (a *Argument) SetStorage(storage *Storage) {
a.Storage = storage
}
func (a *Argument) SetMinikubeProfile(profile string) {
a.MinikubeProfile = profile
if profile == "" && a.SystemInfo.IsDarwin() {
fmt.Printf("\nNote: Minikube profile is not set, will try to use the default profile: \"%s\"\n", MinikubeDefaultProfile)
fmt.Println("if this is not expected, please specify it explicitly by setting the --profile/-p option\n")
a.MinikubeProfile = MinikubeDefaultProfile
}
}
func (a *Argument) SetWSLDistribution(distribution string) {
a.WSLDistribution = distribution
if distribution == "" && a.SystemInfo.IsWindows() {
fmt.Printf("\nNote: WSL distribution is not set, will try to use the default distribution: \"%s\"\n", WSLDefaultDistribution)
fmt.Println("if this is not expected, please specify it explicitly by setting the --distribution/-d option\n")
a.WSLDistribution = WSLDefaultDistribution
}
}
func (a *Argument) SetKubeVersion(kubeType string) {
var kubeVersion = DefaultK3sVersion
if kubeType == K8s {
kubeVersion = DefaultK8sVersion
}
a.KubernetesVersion = kubeVersion
a.Kubetype = kubeType
}
func (a *Argument) SetKubernetesVersion(kubeType string, kubeVersion string) {
a.KubernetesVersion = kubeVersion
a.Kubetype = kubeType
}
func (a *Argument) SetBaseDir(dir string) {
if dir != "" {
a.BaseDir = dir
}
if a.BaseDir == "" {
a.BaseDir = filepath.Join(a.SystemInfo.GetHomeDir(), common.DefaultBaseDir)
}
if !filepath.IsAbs(a.BaseDir) {
var err error
var absBaseDir string
absBaseDir, err = filepath.Abs(a.BaseDir)
if err != nil {
panic(fmt.Errorf("failed to get absolute path for base directory %s: %v", a.BaseDir, err))
}
a.BaseDir = absBaseDir
}
}
func (a *Argument) SetCudaVersion(cudaVersion string) {
a.CudaVersion = CurrentVerifiedCudaVersion
if cudaVersion != "" {
a.CudaVersion = cudaVersion
}
}
func (a *Argument) SetManifest(manifest string) {
a.Manifest = manifest
}
func (a *Argument) SetConsoleLog(fileName string, truncate bool) {
a.ConsoleLogFileName = fileName
a.ConsoleLogTruncate = truncate
}
func (a *Argument) SetSwapConfig(config SwapConfig) {
a.SwapConfig = &SwapConfig{}
if config.ZRAMSize != "" || config.ZRAMSwapPriority != 0 {
a.EnableZRAM = true
} else {
a.EnableZRAM = config.EnableZRAM
}
if a.EnableZRAM {
a.ZRAMSize = config.ZRAMSize
a.ZRAMSwapPriority = config.ZRAMSwapPriority
a.EnablePodSwap = true
} else {
a.EnablePodSwap = config.EnablePodSwap
}
a.Swappiness = config.Swappiness
}
func (a *Argument) SetMasterHostOverride(config MasterHostConfig) {
if config.MasterHost != "" {
a.MasterHost = config.MasterHost
}
if config.MasterNodeName != "" {
a.MasterNodeName = config.MasterNodeName
}
// set a dummy name to bypass validity checks
// as it will be overridden later when the node name is fetched
if a.MasterNodeName == "" {
a.MasterNodeName = "master"
}
if config.MasterSSHPassword != "" {
a.MasterSSHPassword = config.MasterSSHPassword
}
if config.MasterSSHUser != "" {
a.MasterSSHUser = config.MasterSSHUser
}
if config.MasterSSHPort != 0 {
a.MasterSSHPort = config.MasterSSHPort
}
if config.MasterSSHPrivateKeyPath != "" {
a.MasterSSHPrivateKeyPath = config.MasterSSHPrivateKeyPath
}
}
func (a *Argument) LoadMasterHostConfigIfAny() error {
if a.BaseDir == "" {
return errors.New("basedir unset")
}
content, err := os.ReadFile(filepath.Join(a.BaseDir, MasterHostConfigFile))
if os.IsNotExist(err) {
return nil
}
if err != nil {
return err
}
return json.Unmarshal(content, a.MasterHostConfig)
}
func NewKubeRuntime(flag string, arg Argument) (*KubeRuntime, error) {
loader := NewLoader(flag, arg)
cluster, err := loader.Load()
if err != nil {
return nil, err
}
if err = loadExtraAddons(cluster, arg.ExtraAddon); err != nil {
return nil, err
}
base := connector.NewBaseRuntime(cluster.Name, connector.NewDialer(),
arg.Debug, arg.IgnoreErr, arg.BaseDir, arg.OlaresVersion, arg.ConsoleLogFileName, arg.ConsoleLogTruncate, arg.SystemInfo)
clusterSpec := &cluster.Spec
defaultCluster, roleGroups := clusterSpec.SetDefaultClusterSpec(arg.InCluster, arg.SystemInfo.IsDarwin())
hostSet := make(map[string]struct{})
for _, role := range roleGroups {
for _, host := range role {
if host.IsRole(Master) || host.IsRole(Worker) {
host.SetRole(K8s)
}
if host.IsRole(Master) && arg.SkipMasterPullImages {
host.GetCache().Set(SkipMasterNodePullImages, true)
}
if _, ok := hostSet[host.GetName()]; !ok {
hostSet[host.GetName()] = struct{}{}
base.AppendHost(host)
base.AppendRoleMap(host)
}
host.SetOs(arg.SystemInfo.GetOsType())
host.SetMinikubeProfile(arg.MinikubeProfile)
}
}
args, _ := json.Marshal(arg)
logger.Debugf("[runtime] arg: %s", string(args))
arg.KsEnable = defaultCluster.KubeSphere.Enabled
arg.KsVersion = defaultCluster.KubeSphere.Version
r := &KubeRuntime{
Cluster: defaultCluster,
ClusterName: cluster.Name,
Arg: &arg,
}
r.BaseRuntime = base
return r, nil
}
// Copy is used to create a copy for Runtime.
func (k *KubeRuntime) Copy() connector.Runtime {
runtime := *k
return &runtime
}