* Copyright (c) 2025 Bocloud Technologies Co., Ltd.
* installer is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain n copy of Mulan PSL v2 at:
* http://license.coscl.org.cn/MulanPSL2
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
******************************************************************/
package initialize
import (
"context"
"errors"
"fmt"
"net"
"os"
"path"
"path/filepath"
"regexp"
"runtime"
"sort"
"strings"
"github.com/shirou/gopsutil/v3/cpu"
"github.com/shirou/gopsutil/v3/host"
"github.com/shirou/gopsutil/v3/mem"
"github.com/spf13/afero"
"gopkg.in/yaml.v3"
"gopkg.openfuyao.cn/cluster-api-provider-bke/api/bkecommon/v1beta1"
bkecommon "gopkg.openfuyao.cn/cluster-api-provider-bke/common"
configinit "gopkg.openfuyao.cn/cluster-api-provider-bke/common/cluster/initialize"
configsource "gopkg.openfuyao.cn/cluster-api-provider-bke/common/source"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"gopkg.openfuyao.cn/bkeadm/pkg/build"
"gopkg.openfuyao.cn/bkeadm/pkg/cluster"
"gopkg.openfuyao.cn/bkeadm/pkg/common/types"
"gopkg.openfuyao.cn/bkeadm/pkg/config"
econd "gopkg.openfuyao.cn/bkeadm/pkg/executor/containerd"
"gopkg.openfuyao.cn/bkeadm/pkg/executor/k8s"
"gopkg.openfuyao.cn/bkeadm/pkg/global"
"gopkg.openfuyao.cn/bkeadm/pkg/infrastructure"
"gopkg.openfuyao.cn/bkeadm/pkg/infrastructure/containerd"
"gopkg.openfuyao.cn/bkeadm/pkg/infrastructure/k3s"
"gopkg.openfuyao.cn/bkeadm/pkg/infrastructure/kubelet"
"gopkg.openfuyao.cn/bkeadm/pkg/initialize/bkeagent"
"gopkg.openfuyao.cn/bkeadm/pkg/initialize/bkeconfig"
"gopkg.openfuyao.cn/bkeadm/pkg/initialize/bkeconsole"
"gopkg.openfuyao.cn/bkeadm/pkg/initialize/clusterapi"
"gopkg.openfuyao.cn/bkeadm/pkg/initialize/repository"
"gopkg.openfuyao.cn/bkeadm/pkg/initialize/syscompat"
"gopkg.openfuyao.cn/bkeadm/pkg/initialize/timezone"
"gopkg.openfuyao.cn/bkeadm/pkg/root"
"gopkg.openfuyao.cn/bkeadm/utils"
"gopkg.openfuyao.cn/bkeadm/utils/log"
)
type Options struct {
root.Options
File string `json:"file"`
Args []string `json:"args"`
HostIP string `json:"hostIP"`
Domain string `json:"domain"`
KubernetesPort string `json:"kubernetesPort"`
ImageRepoPort string `json:"imageRepoPort"`
YumRepoPort string `json:"yumRepoPort"`
ChartRepoPort string `json:"chartRepoPort"`
ClusterAPI string `json:"clusterAPI"`
OFVersion string `json:"OFVersion"`
VersionUrl string `json:"versionUrl"`
NtpServer string `json:"ntpServer"`
Runtime string `json:"runtime"`
RuntimeStorage string `json:"runtimeStorage"`
OnlineImage string `json:"onlineImage"`
OtherRepo string `json:"otherRepo"`
OtherSource string `json:"otherSource"`
OtherChart string `json:"otherChart"`
InstallConsole bool `json:"installConsole"`
EnableNTP bool `json:"enableNTP"`
ImageRepoCAFile string `json:"imageRepoCAFile"`
ImageRepoUsername string `json:"imageRepoUsername"`
ImageRepoPassword string `json:"imageRepoPassword"`
ImageRepoTLSVerify bool `json:"imageRepoTLSVerify"`
ImageFilePath string `json:"imageFilePath"`
AgentHealthPort string `json:"agentHealthPort"`
FS afero.Fs
DownloadFunc func(url, dest string) error
SetPatchConfigFn func(version, path, key string) error
K8sClient k8s.KubernetesClient
}
var oc repository.OtherRepo
func (op *Options) Initialize() {
log.Info("BKE initialize ...")
op.nodeInfo()
err := op.Validate()
if err != nil {
log.Errorf("Validation failure, %v", err)
return
}
err = op.setTimezone()
if err != nil {
log.Errorf("Timezone failure, %v", err)
return
}
err = op.prepareEnvironment()
if err != nil {
log.Errorf("Failed to prepare environment, %v", err)
return
}
err = op.ensureContainerServer()
if err != nil {
log.Errorf("Failed to start the container service, %v", err)
return
}
err = op.ensureRepository()
if err != nil {
log.Errorf("Failed to start warehouse, %v", err)
return
}
err = op.ensureClusterAPI()
if err != nil {
log.Errorf("Failed to start cluster API, %v", err)
return
}
if op.InstallConsole {
err = op.ensureConsoleAll()
if err != nil {
log.Errorf("Failed to start Console, %v", err)
return
}
} else {
log.Info("Skipping bkeconsole installation as requested")
}
op.generateClusterConfig()
op.modifyPermission()
log.Info("BKE initialization is complete")
op.deployCluster()
}
func (op *Options) nodeInfo() {
h, _ := host.Info()
c, _ := cpu.Counts(false)
v, _ := mem.VirtualMemory()
log.Infof("HOSTNAME: %s", h.Hostname)
log.Infof("PLATFORM: %s", h.Platform)
log.Infof("Version: %s", h.PlatformVersion)
log.Infof("KERNEL: %s", h.KernelVersion)
log.Infof("GOOS: %s", runtime.GOOS)
log.Infof("ARCH: %s", runtime.GOARCH)
log.Infof("CPU: %d", c)
log.Infof("MEMORY: %dG", v.Total/1024/1024/1024+1)
if op.InstallConsole {
log.Info("BKE Console: ENABLED")
} else {
log.Info("BKE Console: DISABLED")
}
}
func (op *Options) Validate() error {
log.Info("BKE initialize environment check...")
var err error
oc, err = repository.ParseOnlineConfig(op.Domain, op.OnlineImage, op.OtherRepo, op.OtherSource, op.OtherChart)
if err != nil {
return errors.New(fmt.Sprintf("Configuration parsing failure %v", err))
}
op.logAuthMode()
if err = op.validateDiskSpace(); err != nil {
return err
}
if err = op.validatePorts(); err != nil {
return err
}
op.setGlobalCustomExtra()
return nil
}
func (op *Options) logAuthMode() {
if op.ImageRepoTLSVerify {
if op.ImageRepoUsername != "" && op.ImageRepoPassword != "" {
log.Info("Password authentication will be used")
} else if op.ImageRepoCAFile != "" {
log.Info("CA certificate authentication will be used")
} else {
log.Warn("Client authentication enabled but no credentials provided")
}
}
}
func (op *Options) validateDiskSpace() error {
_, free := utils.DiskUsage(global.Workspace)
if utils.Exists(path.Join(global.Workspace, utils.ImageDataDirectory)) {
if free/1024/1024/1024 < utils.MinDiskSpaceExisting {
return errors.New(fmt.Sprintf("The available space of the working directory %s is less than %d GB",
global.Workspace, utils.MinDiskSpaceExisting))
}
} else {
if free/1024/1024/1024 < utils.MinDiskSpace {
return errors.New(fmt.Sprintf("The available space of the working directory %s is less than %d GB",
global.Workspace, utils.MinDiskSpace))
}
}
return nil
}
func (op *Options) validatePorts() error {
ports := []string{op.KubernetesPort, op.ImageRepoPort, op.ChartRepoPort, op.YumRepoPort}
ports = op.filterExistingContainerPorts(ports)
err := utils.CheckPorts(ports)
if err != nil {
return errors.New(fmt.Sprintf("The port is already in use %v", err))
}
return nil
}
func (op *Options) filterExistingContainerPorts(ports []string) []string {
if infrastructure.IsDocker() {
ports = op.filterDockerContainerPorts(ports)
}
if infrastructure.IsContainerd() {
ports = op.filterContainerdPorts(ports)
}
return ports
}
func (op *Options) filterDockerContainerPorts(ports []string) []string {
if _, ok := global.Docker.ContainerExists(utils.LocalKubernetesName); ok {
ports = utils.RemoveStringObject(ports, op.KubernetesPort)
}
if _, ok := global.Docker.ContainerExists(utils.LocalImageRegistryName); ok {
ports = utils.RemoveStringObject(ports, op.ImageRepoPort)
}
if _, ok := global.Docker.ContainerExists(utils.LocalChartRegistryName); ok {
ports = utils.RemoveStringObject(ports, op.ChartRepoPort)
}
if _, ok := global.Docker.ContainerExists(utils.LocalNFSRegistryName); ok {
ports = utils.RemoveStringObject(ports, "2049")
}
if _, ok := global.Docker.ContainerExists(utils.LocalYumRegistryName); ok {
ports = utils.RemoveStringObject(ports, op.YumRepoPort)
}
return ports
}
func (op *Options) filterContainerdPorts(ports []string) []string {
if _, ok := econd.ContainerExists(utils.LocalKubernetesName); ok {
ports = utils.RemoveStringObject(ports, op.KubernetesPort)
}
if _, ok := econd.ContainerExists(utils.LocalImageRegistryName); ok {
ports = utils.RemoveStringObject(ports, op.ImageRepoPort)
}
if _, ok := econd.ContainerExists(utils.LocalChartRegistryName); ok {
ports = utils.RemoveStringObject(ports, op.ChartRepoPort)
}
if _, ok := econd.ContainerExists(utils.LocalNFSRegistryName); ok {
ports = utils.RemoveStringObject(ports, "2049")
}
if _, ok := econd.ContainerExists(utils.LocalYumRegistryName); ok {
ports = utils.RemoveStringObject(ports, op.YumRepoPort)
}
return ports
}
func (op *Options) setGlobalCustomExtra() {
global.CustomExtra["domain"] = op.Domain
global.CustomExtra["host"] = op.HostIP
global.CustomExtra["imageRepoPort"] = op.ImageRepoPort
global.CustomExtra["yumRepoPort"] = op.YumRepoPort
global.CustomExtra["chartRepoPort"] = op.ChartRepoPort
global.CustomExtra["clusterapi"] = op.ClusterAPI
global.CustomExtra["nfsserverpath"] = "/"
global.CustomExtra["onlineImage"] = op.OnlineImage
global.CustomExtra["otherRepo"] = oc.Repo
global.CustomExtra["otherRepoIp"] = oc.RepoIP
global.CustomExtra["otherSource"] = oc.Source
global.CustomExtra["otherChart"] = oc.ChartRepo
global.CustomExtra["otherChartIp"] = oc.ChartRepoIP
}
func (op *Options) setTimezone() error {
log.Info("set up the host machine zone")
err := timezone.SetTimeZone()
if err != nil {
return err
}
if !op.EnableNTP {
log.Info("NTP service is disabled, skipping NTP server setup")
op.NtpServer = ""
return nil
}
log.Info("set ntp server")
newNTPServer, err := timezone.NTPServer(op.NtpServer, op.HostIP, len(oc.Repo) > 0)
if err != nil {
return err
}
op.NtpServer = newNTPServer
return nil
}
func (op *Options) prepareEnvironment() error {
log.Info("config local source")
op.configLocalSource()
hostIP, domain := op.resolveHostIPAndDomain()
if err := syscompat.SetHosts(hostIP, domain); err != nil {
log.Warnf("Failed to set hosts %v", err)
}
clientAuthConfig := op.buildClientAuthConfig()
if err := op.configurePrivateRegistry(clientAuthConfig); err != nil {
return err
}
return op.initRepositories(clientAuthConfig)
}
func (op *Options) configLocalSource() {
if op.OtherRepo == "" && op.OnlineImage == "" {
baseurl := "file://" + path.Join(global.Workspace, utils.SourceDataDirectory)
if len(oc.Source) > 0 {
baseurl = oc.Source
}
err := configsource.SetSource(baseurl)
if err != nil {
log.Warnf("Failed to set download source %v", err)
}
}
}
func (op *Options) resolveHostIPAndDomain() (string, string) {
hostIP, domain := op.HostIP, op.Domain
if op.OtherRepo != "" || op.OnlineImage != "" {
registryHost, _ := repository.ParseRegistryHostPort(op.OtherRepo)
if net.ParseIP(registryHost) != nil {
hostIP = registryHost
log.Infof("Online mode: domain=%s bound to otherRepo IP=%s", domain, hostIP)
} else {
log.Infof("Online mode: domain=%s bound to default IP=%s", domain, hostIP)
}
} else {
log.Infof("Offline mode: domain=%s bound to bootstrap node IP=%s", domain, hostIP)
}
if strings.Contains(oc.Repo, configinit.DefaultImageRepo) {
hostIP = oc.RepoIP
domain = strings.Split(strings.Split(oc.Repo, "/")[0], ":")[0]
}
return hostIP, domain
}
func (op *Options) buildClientAuthConfig() *repository.CertificateConfig {
return &repository.CertificateConfig{
TLSVerify: op.ImageRepoTLSVerify,
Username: op.ImageRepoUsername,
Password: op.ImageRepoPassword,
CAFile: op.ImageRepoCAFile,
}
}
func (op *Options) configurePrivateRegistry(cfg *repository.CertificateConfig) error {
registryHost, registryPort := repository.ParseRegistryHostPort(oc.Repo)
if registryHost != "" && registryPort != "" {
cfg.RegistryHost = registryHost
cfg.RegistryPort = registryPort
if cfg.TLSVerify && cfg.CAFile != "" {
if err := repository.SetupCACertificate(cfg); err != nil {
return fmt.Errorf("配置私有仓库CA证书失败:%v", err)
}
}
} else {
log.Warn("Cannot resolve private registry address, skipping CA certificate configuration")
}
return nil
}
func (op *Options) initRepositories(clientAuthConfig *repository.CertificateConfig) error {
if err := repository.RepoInit(oc, clientAuthConfig); err != nil {
return err
}
if err := repository.DecompressionSystemSourceFile(); err != nil {
return err
}
if op.ImageFilePath == "" {
if err := repository.SourceInit(oc); err != nil {
return err
}
}
if err := syscompat.RepoUpdate(); err != nil {
log.Warnf("Failed to update repo %v", err)
}
if err := syscompat.Compat(); err != nil {
return errors.New(fmt.Sprintf("The system is not compatible %v", err))
}
syscompat.SetSysctl()
return nil
}
func (op *Options) ensureContainerServer() error {
err := repository.PrepareRepositoryDependOn(op.ImageFilePath)
if err != nil {
return err
}
op.modifyPermission()
result, err := repository.VerifyContainerdFile(op.ImageFilePath)
if err != nil {
return err
}
containerdFile := result.ContainerdList[0]
cniPluginFile := result.CniPluginList[0]
for _, cond := range result.ContainerdList {
if strings.Contains(cond, runtime.GOARCH) {
containerdFile = cond
continue
}
}
for _, cni := range result.CniPluginList {
if strings.Contains(cni, runtime.GOARCH) {
cniPluginFile = cni
continue
}
}
err = infrastructure.RuntimeInstall(infrastructure.RuntimeConfig{
Runtime: op.Runtime,
RuntimeStorage: op.RuntimeStorage,
Domain: op.Domain,
ImageRepoPort: op.ImageRepoPort,
ContainerdFile: result.FilePath + "/" + containerdFile,
CniPluginFile: result.FilePath + "/" + cniPluginFile,
DockerdFile: result.FilePath + "/" + strings.Replace(utils.KylinDocker, "{.arch}", runtime.GOARCH, -1),
HostIP: op.HostIP,
CAFile: op.ImageRepoCAFile,
})
if err != nil {
return err
}
return nil
}
func (op *Options) ensureRepository() error {
log.Info("Start the base dependency service")
var err error
if op.ImageFilePath != "" {
err = repository.LoadLocalImage()
if err != nil {
return err
}
}
err = repository.LoadLocalRepository()
if err != nil {
return err
}
err = repository.ContainerServer(op.ImageFilePath, op.ImageRepoPort, oc.Repo, oc.Image)
if err != nil {
return err
}
if op.ImageFilePath != "" {
err = repository.SyncLocalImage(op.ImageRepoPort)
if err != nil {
return err
}
}
err = repository.YumServer(op.ImageFilePath, op.ImageRepoPort, op.YumRepoPort, oc.Repo, oc.Image)
if err != nil {
return err
}
err = repository.ChartServer(op.ImageFilePath, op.ImageRepoPort, op.ChartRepoPort, oc.Repo, oc.Image)
if err != nil {
return err
}
log.Info("Skip starting NFS service during init")
return nil
}
func extractVersionFromFilename(filename string) string {
base := strings.TrimSuffix(filename, filepath.Ext(filename))
versionRegex := regexp.MustCompile(`^(?:.*-)?(latest|v\d+\.\d+(?:[-.]\w+)*)$`)
matches := versionRegex.FindStringSubmatch(base)
if len(matches) >= utils.MatchFields {
return matches[1]
}
return ""
}
func (op *Options) offlineGenerateDeployCM(patchesDir string) error {
fs := op.FS
if fs == nil {
fs = afero.NewOsFs()
}
setPatchFn := op.SetPatchConfigFn
if setPatchFn == nil {
setPatchFn = bkeconfig.SetPatchConfig
}
if _, err := fs.Stat(patchesDir); os.IsNotExist(err) {
log.Warnf("patchesDir %s not exist, use default", patchesDir)
return err
}
entries, err := afero.ReadDir(fs, patchesDir)
if err != nil {
log.Warnf("read %s fail %s, use default", patchesDir, err)
return err
}
for _, entry := range entries {
version := extractVersionFromFilename(entry.Name())
if op.OFVersion == version {
log.Infof("version %s file, generate cm", op.OFVersion)
fullPath := filepath.Join(patchesDir, entry.Name())
cmKey := fmt.Sprintf("%s%s", utils.PatchValuePrefix, version)
if err = setPatchFn(version, fullPath, cmKey); err != nil {
log.Warnf("generate cm fail %s, use default", err)
return err
}
return nil
}
}
return fmt.Errorf("offline patch %s not exist, use default", op.OFVersion)
}
func parseYAMLBytesToSliceMap(data []byte) ([]map[string]string, error) {
var rawList []map[string]string
if err := yaml.Unmarshal(data, &rawList); err != nil {
return nil, fmt.Errorf("failed to unmarshal YAML: %w", err)
}
result := make([]map[string]string, 0, len(rawList))
for i, item := range rawList {
if _, exists := item["openFuyaoVersion"]; !exists {
return nil, fmt.Errorf("item at index %d is missing required field 'openFuyaoVersion'", i)
}
if _, exists := item["filePath"]; !exists {
return nil, fmt.Errorf("item at index %d is missing required field 'filePath'", i)
}
item["filePath"], _ = strings.CutPrefix(item["filePath"], "./")
result = append(result, item)
}
return result, nil
}
func (op *Options) onlineGenerateDeployCM() error {
fs := op.FS
if fs == nil {
fs = afero.NewOsFs()
}
downloadFn := op.DownloadFunc
if downloadFn == nil {
downloadFn = utils.DownloadFile
}
setPatchFn := op.SetPatchConfigFn
if setPatchFn == nil {
setPatchFn = bkeconfig.SetPatchConfig
}
patchesDir := filepath.Join(global.Workspace, utils.PatchDataDirectory)
if err := fs.MkdirAll(patchesDir, utils.DefaultDirPermission); err != nil {
log.Warnf("mkdir dir %s err %v, use default", patchesDir, err)
return err
}
url := op.VersionUrl
if strings.HasSuffix(url, "/") {
url = strings.TrimSuffix(url, "/")
}
indexURL := fmt.Sprintf("%s/index.yaml", url)
indexFile := filepath.Join(patchesDir, "index.yaml")
if err := downloadFn(indexURL, indexFile); err != nil {
log.Warnf("download file %s err %v, use default", indexURL, err)
return err
}
defer func() {
_ = fs.Remove(indexFile)
}()
data, err := afero.ReadFile(fs, indexFile)
if err != nil {
log.Warnf("read index.yaml failed: %v", err)
return err
}
return op.processIndexYAML(downloadFn, setPatchFn, data, url, patchesDir)
}
func (op *Options) processIndexYAML(
downloadFn func(string, string) error,
setPatchFn func(string, string, string) error,
data []byte,
baseURL, patchesDir string,
) error {
patchRes, err := parseYAMLBytesToSliceMap(data)
if err != nil {
log.Warnf("parseYAMLFileToSliceMap err %v, use default", err)
return err
}
for _, value := range patchRes {
if value["openFuyaoVersion"] == op.OFVersion {
filePath := value["filePath"]
downloadURL := fmt.Sprintf("%s/%s", baseURL, filePath)
downloadFile := filepath.Join(patchesDir, filePath)
if err = downloadFn(downloadURL, downloadFile); err != nil {
log.Warnf("download file %s err %v, use default", downloadURL, err)
return err
}
cmKey := fmt.Sprintf("%s%s", utils.PatchValuePrefix, op.OFVersion)
if err = setPatchFn(op.OFVersion, downloadFile, cmKey); err != nil {
log.Warnf("generate cm fail %s, use default", err)
return err
}
return nil
}
}
return fmt.Errorf("online patch %s not exist, use default", op.OFVersion)
}
func (op *Options) generateDeployCM() error {
if op.ImageFilePath != "" {
patchesDir := filepath.Join(global.Workspace, utils.LocalPatchDirectory)
return op.offlineGenerateDeployCM(patchesDir)
}
if oc.Repo == "" && oc.Image == "" {
patchesDir := filepath.Join(global.Workspace, utils.PatchDataDirectory)
return op.offlineGenerateDeployCM(patchesDir)
} else {
return op.onlineGenerateDeployCM()
}
}
func (op *Options) getClusterAPIVersion(openFuyaoVersion, defaultVersion string) (string, string) {
var client k8s.KubernetesClient
if op.K8sClient != nil {
client = op.K8sClient
} else if global.K8s != nil {
client = global.K8s
} else {
var err error
client, err = k8s.NewKubernetesClient("")
if err != nil {
log.Warnf("failed to init k8s client: %v", err)
return defaultVersion, defaultVersion
}
global.K8s = client
}
patchCmKey := fmt.Sprintf("cm.%s", openFuyaoVersion)
k8sClient := client.GetClient()
patchConfigMap, err := k8sClient.CoreV1().ConfigMaps("openfuyao-patch").Get(context.TODO(), patchCmKey, metav1.GetOptions{})
if err != nil {
log.Warnf("failed to get patch cm, err: %v", err)
return defaultVersion, defaultVersion
}
data, ok := patchConfigMap.Data[openFuyaoVersion]
if !ok {
log.Warnf("cm data not contain %s key", openFuyaoVersion)
return defaultVersion, defaultVersion
}
cfg := &build.BuildConfig{}
if err = yaml.Unmarshal([]byte(data), cfg); err != nil {
log.Warnf("Unable to serialize err %s", err)
return defaultVersion, defaultVersion
}
return extractVersionsFromConfig(cfg, defaultVersion)
}
func flattenImages(cfg *build.BuildConfig) []build.Image {
if cfg == nil {
return nil
}
var images []build.Image
for _, repo := range cfg.Repos {
for _, sub := range repo.SubImages {
images = append(images, sub.Images...)
}
}
return images
}
func findImageTag(cfg *build.BuildConfig, imageName, defaultVersion string) string {
for _, img := range flattenImages(cfg) {
if img.Name == imageName && len(img.Tag) > 0 {
return img.Tag[0]
}
}
return defaultVersion
}
func extractVersionsFromConfig(cfg *build.BuildConfig, defaultVersion string) (string, string) {
manifestsVersion := findImageTag(cfg, "bke-manifests", defaultVersion)
providerVersion := findImageTag(cfg, "cluster-api-provider-bke", defaultVersion)
return manifestsVersion, providerVersion
}
func (op *Options) ensureClusterAPI() error {
err := infrastructure.StartLocalKubernetes(k3s.Config{
OnlineImage: oc.Image,
OtherRepo: oc.Repo,
OtherRepoIP: oc.RepoIP,
HostIP: op.HostIP,
ImageRepo: op.Domain,
ImageRepoPort: op.ImageRepoPort,
KubernetesPort: op.KubernetesPort,
}, op.ImageFilePath)
if err != nil {
log.Errorf("Failed to start kubernetes %v", err)
return err
}
if op.OFVersion != "" {
if err = op.generateDeployCM(); err != nil {
log.Errorf("Deploy version %s not in released version list", op.OFVersion)
return fmt.Errorf("version %s not in released version list", op.OFVersion)
}
}
err = containerd.ApplyContainerdCfg(fmt.Sprintf("%s:%s", op.Domain, op.ImageRepoPort))
if err != nil {
log.Errorf("Failed to install containerd config %v", err)
return err
}
err = kubelet.ApplyKubeletCfg()
if err != nil {
log.Errorf("Failed to install kubelet config %v", err)
return err
}
err = bkeagent.InstallBKEAgentCRD()
if err != nil {
log.Errorf("Failed to install bkeagent command CRD: %v", err)
return err
}
var repo string
localRepoPath := fmt.Sprintf("%s:%s/%s/", op.Domain, "443", bkecommon.ImageRegistryKubernetes)
if op.ImageFilePath != "" {
repo = localRepoPath
} else if oc.Repo != "" {
repo = oc.Repo
} else if oc.Image == "" {
repo = localRepoPath
}
manifestsVersion, providerVersion := op.getClusterAPIVersion(op.OFVersion, op.ClusterAPI)
err = clusterapi.DeployClusterAPI(repo, manifestsVersion, providerVersion)
if err != nil {
log.Errorf("Failed to deploy cluster-api %v", err)
return err
}
log.Info("The cluster-api deployment is complete")
return nil
}
func (op *Options) ensureConsoleAll() error {
if !op.InstallConsole {
log.Info("BKE Console installation is disabled")
return nil
}
log.Info("Starting BKE Console installation...")
var repo string
localRepoPath := fmt.Sprintf("%s:%s/%s/", op.Domain, "443", bkecommon.ImageRegistryKubernetes)
if oc.Repo != "" {
repo = oc.Repo
} else if oc.Image == "" {
repo = localRepoPath
}
var sRestartConfig types.K3sRestartConfig
sRestartConfig = types.K3sRestartConfig{
OnlineImage: oc.Image,
OtherRepo: oc.Repo,
OtherRepoIp: oc.RepoIP,
HostIP: op.HostIP,
ImageRepo: op.Domain,
ImageRepoPort: op.ImageRepoPort,
KubernetesPort: op.KubernetesPort,
}
err := bkeconsole.DeployConsoleAll(sRestartConfig, repo, op.OFVersion)
if err != nil {
log.Errorf("Failed to deloy console %v", err)
return err
}
log.Info("The bke console deployment is complete")
return nil
}
func (op *Options) generateClusterConfig() {
log.Info("Generate the cluster configuration file")
data, repo, err := op.prepareClusterConfigData()
if err != nil {
log.Errorf("generateClusterConfig func is error %s", err)
return
}
op.createClusterConfigFile(data, repo[0], repo[1], repo[2])
}
func (op *Options) prepareClusterConfigData() (map[string]string, []v1beta1.Repo, error) {
k8sVersion, err := op.getStartKubernetesVersion()
if err != nil {
return nil, []v1beta1.Repo{}, err
}
data := map[string]string{
"chartRepoPort": fmt.Sprintf("%s", op.ChartRepoPort),
"clusterapi": op.ClusterAPI,
"domain": op.Domain,
"host": op.HostIP,
"httpDomain": configinit.DefaultYumRepo,
"httpIp": "",
"httpRepo": oc.Source,
"imageRepoPort": fmt.Sprintf("%s", op.ImageRepoPort),
"ntpServer": op.NtpServer,
"otherRepo": oc.Repo,
"otherRepoIp": oc.RepoIP,
"runtime": op.Runtime,
"yumRepoPort": fmt.Sprintf("%s", op.YumRepoPort),
"kubernetesVersion": k8sVersion,
"agentHealthPort": op.AgentHealthPort,
}
patchesDir := filepath.Join(global.Workspace, utils.PatchDataDirectory)
if op.ImageFilePath != "" {
patchesDir = filepath.Join(global.Workspace, utils.LocalPatchDirectory)
}
if patchMap := op.ProcessPatchFiles(patchesDir); patchMap != nil {
for k, v := range patchMap {
data[k] = v
}
}
imageRepo := op.prepareImageRepoConfig()
yumRepo := op.prepareHTTPRepoConfig()
chartRepo := op.prepareChartRepoConfig()
for k, v := range global.CustomExtra {
data[k] = v
}
return data, []v1beta1.Repo{imageRepo, yumRepo, chartRepo}, nil
}
func (op *Options) prepareImageRepoConfig() v1beta1.Repo {
imageRepo := v1beta1.Repo{
Domain: op.Domain,
Ip: op.HostIP,
Port: op.ImageRepoPort,
Prefix: bkecommon.ImageRegistryKubernetes,
}
if oc.Repo != "" {
img := strings.Split(oc.Repo, "/")
img1 := strings.Split(img[0], ":")
port := "443"
if len(img1) == utils.HttpUrlFields {
port = img1[1]
}
imageRepo = v1beta1.Repo{
Domain: img1[0],
Ip: oc.RepoIP,
Port: port,
Prefix: strings.TrimRight(strings.Join(img[1:], "/"), "/"),
}
} else if oc.Image != "" {
imageRepo.Prefix = ""
imageRepo.Domain = "default"
}
return imageRepo
}
func (op *Options) prepareChartRepoConfig() v1beta1.Repo {
ChartRepoIP, err := utils.LoopIP(configinit.DefaultChartRepo)
if err == nil {
chartRepo := v1beta1.Repo{
Domain: configinit.DefaultChartRepo,
Ip: ChartRepoIP[0],
Port: "443",
Prefix: "charts",
}
return chartRepo
} else {
chartRepo := v1beta1.Repo{
Domain: "",
Ip: op.HostIP,
Port: op.ChartRepoPort,
Prefix: "",
}
return chartRepo
}
}
func (op *Options) prepareHTTPRepoConfig() v1beta1.Repo {
yumRepo := v1beta1.Repo{
Domain: configinit.DefaultYumRepo,
Ip: op.HostIP,
Port: op.YumRepoPort,
}
if oc.Source != "" {
httpRepo := strings.TrimLeft(oc.Source, "http://")
httpRepoArray := strings.Split(httpRepo, ":")
port := "80"
if len(httpRepoArray) == utils.HttpUrlFields {
port = httpRepoArray[1]
}
yumRepo = v1beta1.Repo{
Domain: configinit.DefaultYumRepo,
Port: port,
}
if net.ParseIP(httpRepoArray[0]) == nil {
global.CustomExtra["httpIp"] = httpRepoArray[0]
yumRepo.Ip = httpRepoArray[0]
} else {
yumRepo.Domain = httpRepoArray[0]
}
}
return yumRepo
}
func (op *Options) createClusterConfigFile(data map[string]string, imageRepo, yumRepo, chartRepo v1beta1.Repo) {
err := bkeconfig.SetKubernetesConfig(data, bkecommon.BKEClusterConfigFileName, "cluster-system")
if err != nil {
log.Errorf("Failed to generate the cluster configuration file %v", err)
return
}
if op.File != "" {
return
}
c := config.Options{
Directory: fmt.Sprintf("%s/cluster", global.Workspace),
Product: "fuyao-allinone",
Domain: op.Domain,
ImageRepoPort: op.ImageRepoPort,
NtpServer: op.NtpServer,
AgentHealthPort: op.AgentHealthPort,
}
c.Config(global.CustomExtra, imageRepo, yumRepo, chartRepo, op.NtpServer)
log.Tracef("Run `bke cluster create -f %s/cluster/1master-cluster.yaml -n %s/cluster/1master-node.yaml`"+
"command to deploy the cluster", global.Workspace, global.Workspace)
}
func versionLess(v1, v2 string) bool {
num1 := strings.TrimPrefix(v1, "v")
num2 := strings.TrimPrefix(v2, "v")
parts1 := strings.Split(num1, ".")
parts2 := strings.Split(num2, ".")
for i := 0; i < len(parts1) && i < len(parts2); i++ {
if parts1[i] != parts2[i] {
return parts1[i] < parts2[i]
}
}
return len(parts1) < len(parts2)
}
func (op *Options) getStartKubernetesVersion() (string, error) {
sourceRegistry := fmt.Sprintf("%s/mount/source_registry/files", global.Workspace)
if !utils.Exists(sourceRegistry) {
return configinit.DefaultKubernetesVersion, nil
}
entries, err := os.ReadDir(sourceRegistry)
if err != nil {
return "", err
}
arches := []string{"-arm64", "-amd64", "-x86_64", "-ppc64le", "-s390x"}
re := regexp.MustCompile(`^kubectl-(v\d+\.\d+\.\d+(?:[-.][a-zA-Z0-9]+)*)$`)
var versions []string
for _, entry := range entries {
base := entry.Name()
for _, arch := range arches {
if strings.HasSuffix(base, arch) {
base = strings.TrimSuffix(base, arch)
break
}
}
matches := re.FindStringSubmatch(base)
if len(matches) > 1 {
versions = append(versions, matches[1])
}
}
if len(versions) == 0 {
return "", errors.New("no kubernetes version found")
}
sort.Slice(versions, func(i, j int) bool {
return versionLess(versions[i], versions[j])
})
return versions[0], nil
}
func (op *Options) ProcessPatchFiles(patchesDir string) map[string]string {
if _, err := os.Stat(patchesDir); os.IsNotExist(err) {
return nil
}
entries, err := os.ReadDir(patchesDir)
if err != nil {
return nil
}
patchFiles := make(map[string]string)
for _, entry := range entries {
info, err := entry.Info()
if err != nil {
log.Warnf("failed to get file info for %s: %v", entry.Name(), err)
continue
}
if info.Size() == 0 {
log.Warnf("skipping empty file: %s", entry.Name())
continue
}
version := extractVersionFromFilename(entry.Name())
if version == "" {
continue
}
fullPath := filepath.Join(patchesDir, entry.Name())
bkeConfigMapKey := fmt.Sprintf("%s%s", utils.PatchKeyPrefix, version)
patchConfigMapName := fmt.Sprintf("%s%s", utils.PatchValuePrefix, version)
if err = bkeconfig.SetPatchConfig(version, fullPath, patchConfigMapName); err != nil {
continue
}
patchFiles[bkeConfigMapKey] = patchConfigMapName
}
return patchFiles
}
func (op *Options) modifyPermission() {
var workDir string
if utils.IsFile("/opt/BKE_WORKSPACE") {
f, err := os.ReadFile("/opt/BKE_WORKSPACE")
if err == nil {
workDir = string(f)
workDir = strings.TrimSpace(workDir)
workDir = strings.TrimRight(workDir, "\n")
workDir = strings.TrimRight(workDir, "\r")
workDir = strings.TrimRight(workDir, "\t")
}
}
if os.Getenv("BKE_WORKSPACE") != "" {
workDir = os.Getenv("BKE_WORKSPACE")
}
if workDir == "" {
workDir = "/bke"
}
err := filepath.Walk(workDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
log.Infof("Walk %s err %v", workDir, err)
return err
}
if info.IsDir() {
err = os.Chmod(path, utils.DefaultDirPermission)
if err != nil {
log.Infof("path %s mod dir permission err %v", path, err)
}
} else {
err = os.Chmod(path, utils.DefaultFilePermission)
if err != nil {
log.Infof("path %s mod file permission err %v", path, err)
}
}
return nil
})
if err != nil {
log.Infof("workDir %s mod permission err %v", workDir, err)
} else {
log.Infof("workDir %s mod permission success", workDir)
}
}
func (op *Options) deployCluster() {
if op.File == "" {
return
}
log.Info("Starting to deploy the cluster...")
c := cluster.Options{
File: op.File,
NtpServer: op.NtpServer,
}
c.Cluster()
}