pkg/agent/agent.go
/******************************************************************
* 版权声明(开源协议)
* 本文件遵循 Mulan PSL v2 协议
* → 说明这是一个开源项目代码,可以自由使用(遵守协议)
******************************************************************/
package agent
// 定义当前代码属于 agent 模块
// 👉 这个模块职责:与 Kubernetes 中的 Command CRD 交互
import (
"context" // Go 标准上下文(用于 API 调用控制生命周期)
"fmt" // 格式化输出
"os" // 文件操作
"strconv" // 类型转换
"strings" // 字符串处理
"text/tabwriter" // 用于格式化终端表格输出
"time" // 时间处理
agentv1beta1 "gopkg.openfuyao.cn/cluster-api-provider-bke/api/bkeagent/v1beta1"
// 👉 定义 CRD(Command)的 Go 结构体
configinit "gopkg.openfuyao.cn/cluster-api-provider-bke/common/cluster/initialize"
// 👉 提供默认 NTP 服务
"gopkg.openfuyao.cn/cluster-api-provider-bke/common/ntp"
// 👉 时间同步工具(调用 ntp server)
corev1 "k8s.io/api/core/v1"
// 👉 Kubernetes 核心 API(ConfigMap)
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
// 👉 K8s 元数据(ObjectMeta)
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
// 👉 动态资源(CRD 不需要强类型)
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
// 👉 GVK / GVR 定义
yaml2 "sigs.k8s.io/yaml"
// 👉 YAML 序列化
"gopkg.openfuyao.cn/bkeadm/pkg/global"
// 👉 Kubernetes client 封装
"gopkg.openfuyao.cn/bkeadm/pkg/root"
// 👉 CLI 基础结构
"gopkg.openfuyao.cn/bkeadm/utils"
"gopkg.openfuyao.cn/bkeadm/utils/log"
)
const (
// namespace/name 至少要分成 2 段
// 👉 用于校验 CLI 输入格式
nsNamePartsCount = 2
)
var (
// annotation:标记这个资源是 bke 创建的
// 👉 controller 可以识别来源
annotationKey = "bkeagent.bocloud.com/create"
annotationValue = "bke"
)
// Options:CLI 输入参数封装
// 👉 所有命令行参数最终都转换为这个结构体
type Options struct {
root.Options // 继承基础 CLI 参数
Args []string // 原始参数(如 namespace/name)
Name string // Command 资源名
Command string // shell 命令
File string // 脚本文件路径
Nodes string // 节点列表(逗号分隔)
}
// GVR:Kubernetes 资源定位
// 👉 指向 CRD:commands
var gvr = schema.GroupVersionResource{
Group: agentv1beta1.GroupVersion.Group,
Version: agentv1beta1.GroupVersion.Version,
Resource: "commands",
}
// 👉 等价 API:/apis/<group>/<version>/commands
// ======================== 核心入口 ========================
func (op *Options) Exec() {
// 1️⃣ 构建 Command CR(只是一个结构体)
cmd := op.buildCommand()
// 2️⃣ 应用到 Kubernetes(真正创建资源)
if err := op.applyCommand(&cmd); err != nil {
log.Error(err.Error())
return
}
// 3️⃣ 提示用户查看结果
// 👉 注意:这里是异步执行(不是立即返回结果)
log.BKEFormat(log.NIL,
fmt.Sprintf("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))
}
// ======================== 构建 CRD ========================
func (op *Options) buildCommand() agentv1beta1.Command {
// 创建 Command 对象(CRD 实例)
cmd := agentv1beta1.Command{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
annotationKey: annotationValue, // 标记来源
},
},
Spec: agentv1beta1.CommandSpec{
Suspend: false, // 是否暂停执行
Commands: []agentv1beta1.ExecCommand{},
},
}
// 设置 GVK(告诉 K8s 这是哪种资源)
cmd.SetGroupVersionKind(schema.GroupVersionKind{
Group: agentv1beta1.GroupVersion.Group,
Version: agentv1beta1.GroupVersion.Version,
Kind: "Command",
})
// 设置资源名称
cmd.SetName(op.Name)
// 设置 namespace(默认 default)
cmd.SetNamespace(metav1.NamespaceDefault)
// 设置节点选择器(控制在哪些节点执行)
cmd.Spec.NodeSelector = op.buildNodeSelector()
return cmd
}
// ======================== 节点选择 ========================
func (op *Options) buildNodeSelector() *metav1.LabelSelector {
nodeMap := map[string]string{}
// 输入:node1,node2
for _, value := range strings.Split(op.Nodes, ",") {
nodeMap[value] = value
}
// 转成 K8s LabelSelector
return &metav1.LabelSelector{
MatchLabels: nodeMap,
}
}
// 👉 controller 会根据 label 把任务分发到对应节点
// ======================== 构建执行内容 ========================
func (op *Options) applyCommand(cmd *agentv1beta1.Command) error {
// ====== 情况1:直接执行命令 ======
if op.Command != "" {
cmd.Spec.Commands = append(cmd.Spec.Commands,
agentv1beta1.ExecCommand{
ID: "command", // 标识
Command: []string{op.Command}, // shell 命令
Type: agentv1beta1.CommandShell, // 类型:shell
BackoffIgnore: false, // 是否忽略失败重试
BackoffDelay: 0,
})
}
// ====== 情况2:执行脚本文件 ======
if op.File != "" {
// 文件会被转成 ConfigMap
if err := op.createConfigMapFromFile(cmd); err != nil {
return err
}
}
// 最终安装(提交到 K8s)
return op.installCommand(cmd)
}
// ======================== 文件转 ConfigMap ========================
func (op *Options) createConfigMapFromFile(cmd *agentv1beta1.Command) error {
// 读取本地文件
b1, err := os.ReadFile(op.File)
if err != nil {
return err
}
// 创建 ConfigMap
cm := corev1.ConfigMap{
TypeMeta: metav1.TypeMeta{
APIVersion: "v1",
Kind: "ConfigMap",
},
ObjectMeta: metav1.ObjectMeta{
Name: op.Name,
},
Data: map[string]string{
"value": string(b1), // 存储脚本内容
},
}
// 调用 Kubernetes API 创建 ConfigMap
_, err = global.K8s.GetClient().
CoreV1().
ConfigMaps(metav1.NamespaceDefault).
Create(context.Background(), &cm, metav1.CreateOptions{})
if err != nil {
return err
}
// 在 Command 中引用 ConfigMap
cmd.Spec.Commands = append(cmd.Spec.Commands,
agentv1beta1.ExecCommand{
ID: "file",
// 关键格式:
// configmap:namespace/name:rx:shell
Command: []string{
fmt.Sprintf("configmap:%s/%s:rx:shell",
metav1.NamespaceDefault, op.Name),
},
Type: agentv1beta1.CommandKubernetes,
})
return nil
}
// ======================== 提交到 Kubernetes ========================
func (op *Options) installCommand(cmd *agentv1beta1.Command) error {
// 序列化为 YAML
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 %s", err.Error())
}
// 调用封装方法安装(类似 kubectl apply)
return global.K8s.InstallYaml(cmdName, map[string]string{}, "")
}
// ======================== 查询列表 ========================
func (op *Options) List() {
commandList := &agentv1beta1.CommandList{}
// 获取所有 Command CR
err := global.ListK8sResources(gvr, commandList)
if err != nil {
return
}
// 创建表格输出
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 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),
)
}
}
w.Flush()
}
// ======================== 查询详情 ========================
func (op *Options) Info() {
// 解析 namespace/name
ns := strings.Split(op.Args[0], "/")
if len(ns) < 2 {
log.Error("invalid argument format")
return
}
dynamicClient := global.K8s.GetDynamicClient()
// 获取 CR(动态方式)
workloadUnstructured, err :=
dynamicClient.Resource(gvr).
Namespace(ns[0]).
Get(context.TODO(), ns[1], metav1.GetOptions{})
if err != nil {
log.Error(err.Error())
return
}
// 转换为结构体
commands := &agentv1beta1.Command{}
runtime.DefaultUnstructuredConverter.
FromUnstructured(workloadUnstructured.UnstructuredContent(), commands)
// 输出每个节点执行结果
for node, v := range commands.Status {
for _, c := range v.Conditions {
fmt.Printf("node=%s id=%s status=%s\n",
node, c.ID, c.Status)
// 输出 stdout / stderr
if len(c.StdOut) > 0 {
fmt.Println(strings.Join(c.StdOut, ""))
}
if len(c.StdErr) > 0 {
fmt.Println(strings.Join(c.StdErr, ""))
}
}
}
}
// ======================== 删除 ========================
func (op *Options) Remove() {
ns := strings.Split(op.Args[0], "/")
dynamicClient := global.K8s.GetDynamicClient()
// 删除 CR
dynamicClient.Resource(gvr).
Namespace(ns[0]).
Delete(context.TODO(), ns[1], metav1.DeleteOptions{})
}
// ======================== 时间同步 ========================
func (op *Options) SyncTime() {
// 默认 NTP
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
}
}
fmt.Println("Failed to connect to ntp server")
}
}
pkg/agent/agent_test.go
/*
* 单元测试文件(_test.go)
* 👉 用于验证 agent 相关逻辑是否正确
* 👉 使用 Go testing + testify + gomonkey
*/
package agent
import (
"fmt"
"io"
"os"
"strings"
"testing"
"github.com/agiledragon/gomonkey/v2"
// 👉 gomonkey:运行时打桩(mock函数)
// 👉 可以替换任意函数/方法实现 :contentReference[oaicite:0]{index=0}
"github.com/stretchr/testify/assert"
// 👉 assert:断言库
agentv1beta1 "..." // CRD 类型
"gopkg.openfuyao.cn/cluster-api-provider-bke/common/ntp"
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"
dynamicFake "k8s.io/client-go/dynamic/fake"
// 👉 fake dynamic client(模拟 K8s API)
fake "k8s.io/client-go/kubernetes/fake"
// 👉 fake clientset(模拟 K8s)
"gopkg.openfuyao.cn/bkeadm/pkg/executor/k8s"
"gopkg.openfuyao.cn/bkeadm/pkg/global"
)
// ======================== 常量 ========================
const (
testZeroValue = 0
testOneValue = 1
testTwoValue = 2
testDefaultPortNumber = 123
)
// 👉 测试中使用的常量,避免 magic number
// ======================== 测试1:NodeSelector ========================
func TestBuildNodeSelector(t *testing.T) {
// 表驱动测试(推荐方式) :contentReference[oaicite:1]{index=1}
tests := []struct {
name string
nodes string
expected map[string]string
}{
{
name: "single node",
nodes: "node1",
expected: map[string]string{"node1": "node1"},
},
{
name: "multiple nodes",
nodes: "node1,node2,node3",
expected: map[string]string{
"node1": "node1",
"node2": "node2",
"node3": "node3",
},
},
{
name: "empty nodes",
nodes: "",
expected: map[string]string{"": ""},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// 创建 Options
op := &Options{Nodes: tt.nodes}
// 执行被测函数
result := op.buildNodeSelector()
// 验证输出
assert.Equal(t, tt.expected, result.MatchLabels)
})
}
}
/*
测试目的:
✔ 验证 nodes 字符串是否正确转为 label map
核心逻辑:
"node1,node2" → {"node1":"node1", "node2":"node2"}
*/
// ======================== 测试2:buildCommand ========================
func TestBuildCommand(t *testing.T) {
op := &Options{
Name: "test-command",
Nodes: "node1,node2",
}
cmd := op.buildCommand()
// 验证基本属性
assert.Equal(t, "test-command", cmd.GetName())
assert.Equal(t, metav1.NamespaceDefault, cmd.GetNamespace())
// 验证 GVK
assert.Equal(t, "Command", cmd.GetObjectKind().GroupVersionKind().Kind)
assert.Equal(t, agentv1beta1.GroupVersion.Group, cmd.GroupVersionKind().Group)
// 验证 annotation
expectedAnnotations := map[string]string{annotationKey: annotationValue}
assert.Equal(t, expectedAnnotations, cmd.GetAnnotations())
// 验证 NodeSelector
expectedNodeSelector := &metav1.LabelSelector{
MatchLabels: map[string]string{
"node1": "node1",
"node2": "node2",
},
}
assert.Equal(t, expectedNodeSelector, cmd.Spec.NodeSelector)
// 验证默认值
assert.False(t, cmd.Spec.Suspend)
assert.Empty(t, cmd.Spec.Commands)
}
/*
测试目的:
✔ buildCommand 是否正确构建 CRD 对象
验证点:
-
name / namespace
-
GVK
-
annotation
-
nodeSelector
-
默认字段
*/
// ======================== 测试3:applyCommand ========================
func TestApplyCommand(t *testing.T) {
op := &Options{
Name: "test-cmd",
Command: "echo hello",
}
cmd := op.buildCommand()
// mock createConfigMapFromFile(避免真实执行)
patches := gomonkey.ApplyFunc((*Options).createConfigMapFromFile,
func(o *Options, c *agentv1beta1.Command) error {
return nil
})
defer patches.Reset()
// mock installCommand(避免真正调用 kubernetes)
patches = gomonkey.ApplyFunc((*Options).installCommand,
func(o *Options, c *agentv1beta1.Command) error {
return nil
})
defer patches.Reset()
err := op.applyCommand(&cmd)
assert.NoError(t, err)
// 验证 command 被加入
assert.Len(t, cmd.Spec.Commands, 1)
assert.Equal(t, "command", cmd.Spec.Commands[0].ID)
assert.Equal(t, []string{"echo hello"}, cmd.Spec.Commands[0].Command)
}
/*
测试目的:
✔ applyCommand 是否正确构建 ExecCommand
关键点:
-
使用 gomonkey 替换真实函数
-
避免副作用(K8s调用)
说明:
👉 gomonkey 可以在运行时替换函数实现 :contentReference[oaicite:2]{index=2}
*/
// ======================== 测试4:installCommand ========================
func TestInstallCommand(t *testing.T) {
op := &Options{}
cmd := &agentv1beta1.Command{
ObjectMeta: metav1.ObjectMeta{
Name: "test-install",
},
}
// mock InstallYaml
patches := gomonkey.ApplyFunc((*k8s.Client).InstallYaml,
func(c *k8s.Client, filename string, data map[string]string, template string) error {
// 读取生成的 YAML
content, _ := os.ReadFile(filename)
var parsedCmd agentv1beta1.Command
yaml.Unmarshal(content, &parsedCmd)
assert.Equal(t, cmd.Name, parsedCmd.Name)
return nil
})
defer patches.Reset()
// mock 写文件
patches = gomonkey.ApplyFunc(os.WriteFile,
func(name string, data []byte, perm os.FileMode) error {
if !strings.HasSuffix(name, ".yaml") {
return fmt.Errorf("filename should end with .yaml")
}
return nil
})
defer patches.Reset()
if global.K8s == nil {
global.K8s = &k8s.Client{}
}
err := op.installCommand(cmd)
assert.Error(t, err)
}
/*
测试目的:
✔ YAML 是否正确生成
✔ 文件名是否合法
关键点:
-
mock os.WriteFile
-
mock InstallYaml
*/
// ======================== 测试5:Exec ========================
func TestExec(t *testing.T) {
// mock buildCommand
patches := gomonkey.ApplyFunc((*Options).buildCommand,
func(o *Options) agentv1beta1.Command {
return agentv1beta1.Command{
ObjectMeta: metav1.ObjectMeta{Name: "test-exec"},
}
})
defer patches.Reset()
applyCalled := false
// mock applyCommand
patches = gomonkey.ApplyFunc((*Options).applyCommand,
func(o *Options, cmd *agentv1beta1.Command) error {
applyCalled = true
return nil
})
defer patches.Reset()
op := &Options{Name: "test-exec"}
op.Exec()
assert.True(t, applyCalled)
}
/*
测试目的:
✔ Exec 是否调用 applyCommand
核心:
只验证流程,不验证逻辑
*/
// ======================== 测试6:List ========================
func TestList(t *testing.T) {
patches := gomonkey.NewPatches()
// mock Kubernetes 查询
patches.ApplyFunc(global.ListK8sResources,
func(gvr schema.GroupVersionResource, list interface{}) error {
commandList := list.(*agentv1beta1.CommandList)
now := metav1.Now()
commandList.Items = []agentv1beta1.Command{
{
ObjectMeta: metav1.ObjectMeta{
Name: "test-command",
},
Status: map[string]*agentv1beta1.CommandStatus{
"node1": {
LastStartTime: &now,
CompletionTime: &now,
Phase: "Completed",
Status: "Success",
},
},
},
}
return nil
})
// mock 输出函数(避免打印)
patches.ApplyFunc(fmt.Fprintln, func(w io.Writer, a ...interface{}) (int, error) { return 0, nil })
patches.ApplyFunc(fmt.Fprintf, func(w io.Writer, format string, a ...interface{}) (int, error) { return 0, nil })
defer patches.Reset()
op := &Options{}
op.List()
}
/*
测试目的:
✔ List 是否正确遍历数据
关键:
-
mock K8s 返回数据
-
mock 输出
*/
// ======================== 测试7:Info ========================
func TestInfo(t *testing.T) {
// 测试两种情况:合法/非法参数
tests := []struct {
name string
args []string
expectError bool
}{
{
name: "valid",
args: []string{"default/test-command"},
},
{
name: "invalid",
args: []string{"invalid-format"},
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
op := &Options{Args: tt.args}
// 使用 fake dynamic client
scheme := runtime.NewScheme()
client := dynamicFake.NewSimpleDynamicClient(scheme)
global.K8s = &k8s.MockK8sClient{}
op.Info()
assert.True(t, true)
})
}
}
/*
测试目的:
✔ 参数解析是否正确
✔ dynamic client 是否调用
说明:
使用 fake client 模拟 Kubernetes API
*/
// ======================== 测试8:Remove ========================
func TestRemove(t *testing.T) {
op := &Options{
Args: []string{"default/test-command"},
}
op.Remove()
}
/*
测试目的:
✔ 删除逻辑不 panic
*/
// ======================== 测试9:SyncTime ========================
func TestSyncTime(t *testing.T) {
op := &Options{
Args: []string{"ntp.server:123"},
}
// mock ntp.Date
patches := gomonkey.ApplyFunc(ntp.Date,
func(server string) error {
return nil
})
defer patches.Reset()
op.SyncTime()
assert.True(t, true)
}
/*
测试目的:
✔ NTP 调用是否执行
*/
// ======================== 测试10:ConfigMap ========================
func TestCreateConfigMapFromFile(t *testing.T) {
patches := gomonkey.NewPatches()
defer patches.Reset()
// mock 文件读取
patches.ApplyFunc(os.ReadFile,
func(name string) ([]byte, error) {
return []byte("test content"), nil
})
// mock k8s client
clientset := fake.NewSimpleClientset()
patches.ApplyFunc((*k8s.MockK8sClient).GetClient,
func(_ *k8s.MockK8sClient) kubernetes.Interface {
return clientset
})
op := &Options{
Name: "test-configmap",
File: "/tmp/test",
}
cmd := &agentv1beta1.Command{}
err := op.createConfigMapFromFile(cmd)
assert.NoError(t, err)
assert.Len(t, cmd.Spec.Commands, 1)
}
/*
测试目的:
✔ 文件 → ConfigMap → Command 引用 是否正确
关键:
-
mock 文件系统
-
mock k8s client
*/
pkg/build/build.go
/******************************************************************
* Build 模块:用于构建 bke 离线安装包(完整流程控制器)
* 核心流程:
* 1. 校验运行环境(必须 Docker)
* 2. 读取并校验配置文件(YAML → struct)
* 3. 初始化工作目录
* 4. 并发收集依赖(rpm / binary / 镜像)
* 5. 打包生成最终 tar.gz
******************************************************************/
package build
// Options:封装 CLI 输入参数(构建上下文)
type Options struct {
root.Options // 嵌入全局参数(如 kubeconfig 等)
File string // 构建配置文件路径(yaml)
Target string // 输出包路径(可为空自动生成)
Strategy string // 构建策略(预留字段)
Arch string // 架构(amd64 / arm)
}
// Build:整个构建流程的入口函数(pipeline 调度器)
func (o *Options) Build() {
// 步骤1:必须在 Docker 环境中运行(保证构建环境一致性)
if !infrastructure.IsDocker() {
log.BKEFormat(log.ERROR, "This build instruction only supports running in docker environment.")
return
}
// 步骤2:加载并校验配置文件
cfg, err := loadAndVerifyBuildConfig(o.File)
if err != nil {
log.BKEFormat(log.ERROR, fmt.Sprintf("Failed to load and verify build config: %s", err.Error()))
return
}
// 步骤3:准备工作目录(tmp / packages / bke 等)
if err := prepareBuildWorkspace(); err != nil {
log.BKEFormat(log.ERROR, fmt.Sprintf("Failed to prepare build workspace: %s", err.Error()))
return
}
// 步骤4:并发收集依赖(核心逻辑)
version, err := o.collectDependenciesAndImages(cfg)
if err != nil {
log.BKEFormat(log.ERROR, fmt.Sprintf("Failed to collect dependencies and images: %s", err.Error()))
return
}
// 步骤5:打包最终产物
if err := o.createFinalPackage(cfg, version); err != nil {
log.BKEFormat(log.ERROR, fmt.Sprintf("Failed to create final package: %s", err.Error()))
return
}
// 步骤8:完成
log.BKEFormat("step.8", fmt.Sprintf("Packaging complete %s", o.Target))
}
// loadAndVerifyBuildConfig:读取 YAML 并反序列化 + 校验配置合法性
func loadAndVerifyBuildConfig(file string) (*BuildConfig, error) {
log.BKEFormat("step.1", "Configuration file check")
cfg := &BuildConfig{}
// 读取文件内容
yamlFile, err := os.ReadFile(file)
if err != nil {
log.BKEFormat(log.ERROR, fmt.Sprintf("Failed to read the file, %s", err.Error()))
return nil, err
}
// YAML → Go struct
if err = yaml.Unmarshal(yamlFile, cfg); err != nil {
log.BKEFormat(log.ERROR, fmt.Sprintf("Unable to serialize file, %s", err.Error()))
return nil, err
}
// 校验配置内容(字段合法性)
err = verifyConfigContent(cfg)
if err != nil {
log.BKEFormat(log.ERROR, fmt.Sprintf("Configuration verification fails %s", err.Error()))
return nil, err
}
return cfg, nil
}
// prepareBuildWorkspace:创建构建工作目录
func prepareBuildWorkspace() error {
log.BKEFormat("step.2", "Creates a workspace in the current directory")
// 实际创建目录结构(tmp / packages / bke)
if err := prepare(); err != nil {
log.BKEFormat(log.ERROR, fmt.Sprintf("Failed to prepare workspace: %s", err.Error()))
return err
}
return nil
}
// collectDependenciesAndImages:并发收集所有构建资源(核心函数)
func (o *Options) collectDependenciesAndImages(cfg *BuildConfig) (string, error) {
var version string // bke 版本号
var errNumber uint64 // 错误计数器
stopChan := make(chan struct{}) // 用于中断其他 goroutine(失败传播)
defer closeChanStruct(stopChan)
wg := sync.WaitGroup{} // 并发控制器(等待所有任务完成)
// goroutine #1:收集 rpm + binary
wg.Add(1)
go func() {
defer wg.Done()
var err error
version, err = collectRpmsAndBinary(cfg, stopChan, &errNumber)
if err != nil {
// 如果失败,通知其他 goroutine 停止
closeChanStruct(stopChan)
}
}()
// goroutine #2:收集镜像
wg.Add(1)
go func() {
defer wg.Done()
if err := collectRegistryImages(cfg, stopChan, &errNumber); err != nil {
closeChanStruct(stopChan)
}
}()
// 阻塞等待所有 goroutine 完成(WaitGroup 核心作用:同步并发执行):contentReference[oaicite:0]{index=0}
wg.Wait()
// 如果存在错误 → 构建失败
if errNumber > 0 {
log.BKEFormat(log.ERROR, fmt.Sprintf("Build failures %d", errNumber))
return "", fmt.Errorf("build failures: %d", errNumber)
}
// 必须拿到版本号
if len(version) == 0 {
log.BKEFormat(log.ERROR, "Failed to get bke version number, please check")
return "", fmt.Errorf("failed to get bke version")
}
return version, nil
}
// collectRpmsAndBinary:收集系统依赖 + 编译 bke
func collectRpmsAndBinary(cfg *BuildConfig, stopChan chan struct{}, errNumber *uint64) (string, error) {
log.BKEFormat("step.3", "Collect host dependency packages and package files")
// 构建 rpm 依赖包
if err := buildRpms(cfg, stopChan); err != nil {
log.BKEFormat(log.ERROR, fmt.Sprintf("Build failures %s when collecting host dependency packages and package files", err.Error()))
// ⚠️ 这里存在写法问题(应为 (*errNumber)++)
*errNumber++
return "", err
}
log.BKEFormat("step.4", "Collect the bke binary file")
// 编译 bke 二进制
version, err := buildBkeBinary()
if err != nil || len(version) == 0 {
log.BKEFormat(log.ERROR, "Collect the bke binary file failed, get bke version number failed")
if err != nil {
log.BKEFormat(log.ERROR, err.Error())
}
*errNumber++
return "", err
}
return version, nil
}
// collectRegistryImages:收集镜像 + 同步仓库
func collectRegistryImages(cfg *BuildConfig, stopChan chan struct{}, errNumber *uint64) error {
log.BKEFormat("step.5", "Collect the required image files")
// 拉取镜像
if err := buildRegistry(cfg.Registry.ImageAddress, cfg.Registry.Architecture); err != nil {
log.BKEFormat(log.ERROR, fmt.Sprintf("Build failures %s when collecting image files", err.Error()))
*errNumber++
return err
}
log.BKEFormat("step.6", "Collect images from the source repository to the target repository")
// 同步镜像仓库
if err := syncRepo(cfg, stopChan); err != nil {
log.BKEFormat(log.ERROR, fmt.Sprintf("Build failures %s when collecting images from the source repository to the target repository", err.Error()))
*errNumber++
return err
}
return nil
}
// createFinalPackage:生成最终 tar.gz 包
func (o *Options) createFinalPackage(cfg *BuildConfig, version string) error {
log.BKEFormat("step.7", "Build the bke package, please wait for the larger package...")
// 如果未指定输出路径,则自动生成
if len(o.Target) == 0 {
fileInfo, err := os.Stat(o.File)
if err != nil {
log.BKEFormat(log.ERROR, err.Error())
return err
}
// 自动命名:bke-版本-配置-架构-时间.tar.gz
o.Target = path.Join(pwd,
fmt.Sprintf("bke-%s-%s-%s-%s.tar.gz",
version,
strings.TrimSuffix(fileInfo.Name(), ".yaml"),
strings.Join(cfg.Registry.Architecture, "-"),
time.Now().Format("20060102150405")))
}
// 执行压缩
if err := compressedPackage(cfg, o.Target); err != nil {
log.BKEFormat(log.ERROR, fmt.Sprintf("Failed to compress the package error is%s", err.Error()))
return err
}
return nil
}
// compressedPackage:写 manifests + 压缩
func compressedPackage(cfg *BuildConfig, target string) error {
// 写入 manifests.yaml(记录构建信息)
err := writeManifestsFile(cfg, path.Join(bke, "manifests.yaml"))
if err != nil {
return err
}
// 最终压缩
return finalizeAndCompress(target)
}
// writeManifestsFile:将配置写入 YAML 文件
func writeManifestsFile(cfg *BuildConfig, manifestPath string) error {
// struct → yaml
b, err := yaml.Marshal(cfg)
if err != nil {
log.BKEFormat(log.ERROR, fmt.Sprintf("Description Failed to parse the configuration file %v", err))
return err
}
// 写入文件
err = os.WriteFile(manifestPath, b, utils.DefaultFilePermission)
if err != nil {
log.BKEFormat(log.ERROR, fmt.Sprintf("Failed to write the configuration file. Procedure %v", err))
return err
}
return nil
}
// finalizeAndCompress:清理 + 打包
func finalizeAndCompress(target string) error {
// 删除临时目录
if err := os.RemoveAll(tmp); err != nil {
log.BKEFormat(log.ERROR, fmt.Sprintf("Unable to delete temporary file %s", err.Error()))
return err
}
// 如果是相对路径,拼接当前目录
if !strings.Contains(target, "/") {
target = filepath.Join(pwd, target)
}
// 打包 packages → tar.gz
if err := global.TaeGZWithoutChangeFile(packages, target); err != nil {
log.BKEFormat(log.ERROR, fmt.Sprintf("Failed to compress the package to %s", target))
return err
}
return nil
}