* 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 agent
import (
"context"
"fmt"
"os"
"strconv"
"strings"
"text/tabwriter"
"time"
agentv1beta1 "gopkg.openfuyao.cn/cluster-api-provider-bke/api/bkeagent/v1beta1"
configinit "gopkg.openfuyao.cn/cluster-api-provider-bke/common/cluster/initialize"
"gopkg.openfuyao.cn/cluster-api-provider-bke/common/ntp"
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/global"
"gopkg.openfuyao.cn/bkeadm/pkg/root"
"gopkg.openfuyao.cn/bkeadm/utils"
"gopkg.openfuyao.cn/bkeadm/utils/log"
)
const (
nsNamePartsCount = 2
)
var (
annotationKey = "bkeagent.bocloud.com/create"
annotationValue = "bke"
)
type Options struct {
root.Options
Args []string `json:"args"`
Name string `json:"name"`
Command string `json:"command"`
File string `json:"file"`
Nodes string `json:"nodes"`
}
var gvr = schema.GroupVersionResource{
Group: agentv1beta1.GroupVersion.Group,
Version: agentv1beta1.GroupVersion.Version,
Resource: "commands",
}
func (op *Options) Exec() {
cmd := op.buildCommand()
if err := op.applyCommand(&cmd); err != nil {
log.Errorf("Apply command %s failed: %v", op.Name, err)
return
}
log.Infof("The execution command has been sent to the cluster, Please run the `bke command info "+
"%s/%s` to optain the execution result", metav1.NamespaceDefault, op.Name)
}
func (op *Options) buildCommand() agentv1beta1.Command {
cmd := agentv1beta1.Command{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
annotationKey: annotationValue,
},
},
Spec: agentv1beta1.CommandSpec{
Suspend: false,
Commands: []agentv1beta1.ExecCommand{},
},
}
cmd.SetGroupVersionKind(schema.GroupVersionKind{
Group: agentv1beta1.GroupVersion.Group,
Version: agentv1beta1.GroupVersion.Version,
Kind: "Command",
})
cmd.SetName(op.Name)
cmd.SetNamespace(metav1.NamespaceDefault)
cmd.Spec.NodeSelector = op.buildNodeSelector()
return cmd
}
func (op *Options) buildNodeSelector() *metav1.LabelSelector {
nodeMap := map[string]string{}
for _, value := range strings.Split(op.Nodes, ",") {
nodeMap[value] = value
}
return &metav1.LabelSelector{MatchLabels: nodeMap}
}
func (op *Options) applyCommand(cmd *agentv1beta1.Command) error {
if op.Command != "" {
cmd.Spec.Commands = append(cmd.Spec.Commands, agentv1beta1.ExecCommand{
ID: "command",
Command: []string{op.Command},
Type: agentv1beta1.CommandShell,
BackoffIgnore: false,
BackoffDelay: 0,
})
}
if op.File != "" {
if err := op.createConfigMapFromFile(cmd); err != nil {
return err
}
}
return op.installCommand(cmd)
}
func (op *Options) createConfigMapFromFile(cmd *agentv1beta1.Command) error {
b1, err := os.ReadFile(op.File)
if err != nil {
return err
}
cm := corev1.ConfigMap{
TypeMeta: metav1.TypeMeta{
APIVersion: "v1",
Kind: "ConfigMap",
},
ObjectMeta: metav1.ObjectMeta{
Name: op.Name,
},
Data: map[string]string{
"value": string(b1),
},
}
_, err = global.K8s.GetClient().CoreV1().ConfigMaps(
metav1.NamespaceDefault).Create(context.Background(), &cm, metav1.CreateOptions{})
if err != nil {
return err
}
cmd.Spec.Commands = append(cmd.Spec.Commands, agentv1beta1.ExecCommand{
ID: "file",
Command: []string{fmt.Sprintf("configmap:%s/%s:rx:shell", metav1.NamespaceDefault, op.Name)},
Type: agentv1beta1.CommandKubernetes,
BackoffIgnore: false,
BackoffDelay: 0,
})
return nil
}
func (op *Options) installCommand(cmd *agentv1beta1.Command) error {
by, err := yaml2.Marshal(cmd)
if err != nil {
return err
}
cmdName := fmt.Sprintf("%s.yaml", cmd.Name)
err = os.WriteFile(cmdName, by, utils.DefaultFilePermission)
if err != nil {
return fmt.Errorf("file generation failure %v", err)
}
return global.K8s.InstallYaml(cmdName, map[string]string{}, "")
}
func (op *Options) List() {
commandList := &agentv1beta1.CommandList{}
err := global.ListK8sResources(gvr, commandList)
if err != nil {
return
}
const tabPadding = 2
w := tabwriter.NewWriter(os.Stdout, 0, 0, tabPadding, ' ', 0)
fmt.Fprintln(w, "name\tsuspend\tnode\tLastStartTime\tCompletionTime\tPhase\tStatus")
for _, bc := range commandList.Items {
for node, v := range bc.Status {
completionTime := ""
if v.CompletionTime != nil {
completionTime = v.CompletionTime.String()
}
fmt.Fprintf(w, "%s/%s\t%s\t%s\t%s\t%s\t%s\t%s\n",
bc.Namespace, bc.Name,
strconv.FormatBool(bc.Spec.Suspend),
node,
v.LastStartTime.String(),
completionTime,
string(v.Phase),
string(v.Status),
)
}
}
err = w.Flush()
if err != nil {
log.Errorf("Flush command tablewriter failed: %v", err)
}
}
func (op *Options) Info() {
ns := strings.Split(op.Args[0], "/")
if len(ns) < nsNamePartsCount {
log.Error("invalid argument format, expected namespace/name")
return
}
var workloadUnstructured *unstructured.Unstructured
var err error
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 command %s/%s failed: %v", ns[0], ns[1], err)
return
}
commands := &agentv1beta1.Command{}
err = runtime.DefaultUnstructuredConverter.FromUnstructured(workloadUnstructured.UnstructuredContent(), commands)
if err != nil {
log.Errorf("Convert command %s/%s failed: %v", ns[0], ns[1], err)
return
}
const tabPadding = 2
for node, v := range commands.Status {
for _, c := range v.Conditions {
w := tabwriter.NewWriter(os.Stdout, 0, 0, tabPadding, ' ', 0)
fmt.Fprintln(w, "name\tsuspend\tnode\tID\tSTATUS")
fmt.Fprintf(w, "%s/%s\t%s\t%s\t%s\t%s\n",
commands.Namespace, commands.Name,
strconv.FormatBool(commands.Spec.Suspend),
node,
c.ID,
string(c.Status),
)
err = w.Flush()
if err != nil {
log.Errorf("Flush command info tablewriter failed: %v", err)
}
output := ""
if len(c.StdOut) > 0 {
output = strings.Join(c.StdOut, "")
}
if len(c.StdErr) > 0 {
output = strings.Join(c.StdErr, "")
}
fmt.Println(output)
}
}
}
func (op *Options) Remove() {
ns := strings.Split(op.Args[0], "/")
if len(ns) < nsNamePartsCount {
log.Error("invalid argument format, expected namespace/name")
return
}
var err error
dynamicClient := global.K8s.GetDynamicClient()
err = dynamicClient.Resource(gvr).Namespace(ns[0]).Delete(context.TODO(), ns[1], metav1.DeleteOptions{})
if err != nil {
log.Errorf("Delete command %s/%s failed: %v", ns[0], ns[1], err)
return
}
}
func (op *Options) SyncTime() {
ntpServer := configinit.DefaultNTPServer
if len(op.Args) >= 1 {
ntpServer = op.Args[0]
}
err := ntp.Date(ntpServer)
if err != nil {
for i := 0; i < utils.MinManifestsImageArgs; i++ {
time.Sleep(utils.ContainerWaitSeconds * time.Second)
err = ntp.Date(ntpServer)
if err == nil {
return
}
}
log.Errorf("Failed to connect to the ntp server %s", ntpServer)
}
}