* 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 cluster
import (
"context"
"fmt"
"os"
"path"
"strings"
"text/tabwriter"
configv1beta1 "gopkg.openfuyao.cn/cluster-api-provider-bke/api/bkecommon/v1beta1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
yaml2 "sigs.k8s.io/yaml"
"gopkg.openfuyao.cn/bkeadm/pkg/executor/k8s"
"gopkg.openfuyao.cn/bkeadm/pkg/global"
"gopkg.openfuyao.cn/bkeadm/pkg/root"
"gopkg.openfuyao.cn/bkeadm/utils"
"gopkg.openfuyao.cn/bkeadm/utils/log"
)
type Options struct {
root.Options
Args []string `json:"args"`
File string `json:"file"`
NodesFile string `json:"nodesFile"`
Conf string `json:"Conf"`
NtpServer string `json:"ntpServer"`
}
const (
nsNamePartsCount = 2
)
func marshalAndWriteClusterYAML(bkeCluster *configv1beta1.BKECluster) (string, error) {
if !strings.HasPrefix(bkeCluster.Namespace, "bke") {
bkeCluster.Namespace = "bke-" + bkeCluster.Namespace
}
by, err := yaml2.Marshal(bkeCluster)
if err != nil {
log.Errorf("Marshal cluster %s/%s failed: %v", bkeCluster.Namespace, bkeCluster.Name, err)
return "", err
}
if err := os.MkdirAll(path.Join(global.Workspace, "cluster"), utils.DefaultDirPermission); err != nil {
log.Warnf("Create cluster directory %s failed: %v", path.Join(global.Workspace, "cluster"), err)
}
bkeFile := path.Join(global.Workspace, "cluster", fmt.Sprintf("%s-%s.yaml", bkeCluster.Namespace, bkeCluster.Name))
if err := os.WriteFile(bkeFile, by, utils.DefaultFilePermission); err != nil {
log.Errorf("Generate cluster file %s failed: %v", bkeFile, err)
return "", err
}
return bkeFile, nil
}
var gvr = schema.GroupVersionResource{
Group: configv1beta1.GVK.Group,
Version: configv1beta1.GVK.Version,
Resource: "bkeclusters",
}
func (op *Options) List() {
bclusterlist := &configv1beta1.BKEClusterList{}
err := global.ListK8sResources(gvr, bclusterlist)
if err != nil {
return
}
headers := []string{"namespace", "name", "endpoint", "master", "worker", "pause", "dryRun", "reset"}
var rows [][]string
for _, bc := range bclusterlist.Items {
master := 0
worker := 0
line := []string{
bc.Namespace,
bc.Name,
fmt.Sprintf("%s:%d", bc.Spec.ControlPlaneEndpoint.Host, bc.Spec.ControlPlaneEndpoint.Port),
fmt.Sprintf("%d", master),
fmt.Sprintf("%d", worker),
fmt.Sprintf("%t", bc.Spec.Pause),
fmt.Sprintf("%t", bc.Spec.DryRun),
fmt.Sprintf("%t", bc.Spec.Reset),
}
rows = append(rows, line)
}
const tabPadding = 2
w := tabwriter.NewWriter(os.Stdout, 0, 0, tabPadding, ' ', 0)
fmt.Fprintln(w, strings.Join(headers, "\t"))
for _, row := range rows {
fmt.Fprintln(w, strings.Join(row, "\t"))
}
err = w.Flush()
if err != nil {
log.Errorf("Flush cluster tablewriter failed: %v", err)
}
}
func (op *Options) Remove() {
var workloadUnstructured *unstructured.Unstructured
var err error
ns := strings.Split(op.Args[0], "/")
if len(ns) < nsNamePartsCount {
log.Error("invalid argument format, expected namespace/name")
return
}
dynamicClient := global.K8s.GetDynamicClient()
workloadUnstructured, err = dynamicClient.Resource(gvr).Namespace(ns[0]).Get(context.TODO(), ns[1], metav1.GetOptions{})
if err != nil {
log.Errorf("Get cluster %s/%s failed: %v", ns[0], ns[1], err)
return
}
bcluster := &configv1beta1.BKECluster{}
err = runtime.DefaultUnstructuredConverter.FromUnstructured(workloadUnstructured.UnstructuredContent(), bcluster)
if err != nil {
log.Errorf("Convert cluster %s/%s failed: %v", ns[0], ns[1], err)
return
}
bcluster.Spec.Reset = true
t1, err := runtime.DefaultUnstructuredConverter.ToUnstructured(bcluster)
if err != nil {
log.Errorf("Convert cluster %s/%s to unstructured failed: %v", bcluster.Namespace, bcluster.Name, err)
return
}
m1 := &unstructured.Unstructured{
Object: t1,
}
_, err = dynamicClient.Resource(gvr).Namespace(bcluster.Namespace).Update(context.TODO(), m1, metav1.UpdateOptions{})
if err != nil {
log.Errorf("Update cluster %s/%s failed: %v", bcluster.Namespace, bcluster.Name, err)
return
}
}
func (op *Options) Scale() {
resources, err := NewClusterResourcesFromFiles(op.File, op.NodesFile)
if err != nil {
log.Errorf("Failed to load the configuration file. %v", err)
return
}
bkeCluster := resources.BKECluster
bkeNodes := resources.BKENodes
bkeFile, err := marshalAndWriteClusterYAML(bkeCluster)
if err != nil {
return
}
nodeFiles, err := marshalAndWriteNodeYAMLs(bkeCluster.Namespace, bkeNodes)
if err != nil {
log.Errorf("Failed to write BKENode files: %v", err)
return
}
log.Info("Patch cluster-api yaml to the cluster")
err = global.K8s.PatchYaml(bkeFile, map[string]string{})
if err != nil {
log.Errorf("Failed to patch bke-cluster, %v", err)
return
}
for _, nodeFile := range nodeFiles {
err = global.K8s.PatchYaml(nodeFile, map[string]string{})
if err != nil {
log.Errorf("Failed to patch bke-node, %v", err)
return
}
}
log.Infof("Patched configuration (1 BKECluster + %d BKENodes)", len(bkeNodes))
log.Info("Waiting for the cluster to start...")
global.K8s.WatchEventByAnnotation(bkeCluster.Namespace)
}
func (op *Options) Log() {
ns := strings.Split(op.Args[0], "/")
global.K8s.WatchEventByAnnotation(ns[0])
}
func loadClusterConfig(file string) (*configv1beta1.BKECluster, error) {
conf := &configv1beta1.BKECluster{}
yamlFile, err := os.ReadFile(file)
if err != nil {
return nil, err
}
if err = yaml2.Unmarshal(yamlFile, conf); err != nil {
return nil, err
}
if conf.Spec.ClusterConfig == nil {
return nil, fmt.Errorf("the cluster configuration cannot be empty")
}
return conf, nil
}
func createKubeconfigSecret(namespace, name, confPath string) error {
kubeConfig, err := os.ReadFile(confPath)
if err != nil {
return err
}
secret := corev1.Secret{
TypeMeta: metav1.TypeMeta{
APIVersion: "v1",
Kind: "Secret",
},
ObjectMeta: metav1.ObjectMeta{
Namespace: namespace,
Name: name + "-kubeconfig",
},
Data: map[string][]byte{
"value": kubeConfig,
},
}
return global.K8s.CreateSecret(&secret)
}
func (op *Options) ExistsCluster() {
conf, err := loadClusterConfig(op.File)
if err != nil {
log.Errorf("Load cluster config %s failed: %v", op.File, err)
return
}
if err = k8s.CreateNamespace(global.K8s, conf.Namespace); err != nil {
log.Errorf("Create namespace %s failed: %v", conf.Namespace, err)
return
}
if err = createKubeconfigSecret(conf.Namespace, conf.Name, op.Conf); err != nil {
log.Errorf("Create kubeconfig secret for cluster %s/%s failed: %v", conf.Namespace, conf.Name, err)
return
}
if err = global.K8s.InstallYaml(op.File, map[string]string{}, ""); err != nil {
log.Errorf("Install cluster yaml %s failed: %v", op.File, err)
return
}
}