对openfuyao-bkeadm的内容分析

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"

"k8s.io/client-go/dynamic"

dynamicFake "k8s.io/client-go/dynamic/fake"

// 👉 fake dynamic client(模拟 K8s API)

"k8s.io/client-go/kubernetes"

fake "k8s.io/client-go/kubernetes/fake"

// 👉 fake clientset(模拟 K8s)

"sigs.k8s.io/yaml"

"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
}
相关推荐
海鸥812 小时前
k8s中实现进程环境的自动更新
云原生·容器·kubernetes
程序员爱酸奶2 小时前
Git + 云原生:构建坚如磐石的 Kubernetes 配置版本管理
git·云原生·kubernetes
@土豆6 小时前
K8s 单机二进制部署步骤(复制粘贴即可)
云原生·容器·kubernetes
丈剑走天涯16 小时前
kubernetes java app 部署使用harbor私服 问题集合
java·容器·kubernetes
Jinkxs18 小时前
Java 部署:滚动更新(K8s RollingUpdate 策略)
java·开发语言·kubernetes
lpfasd12319 小时前
Kubernetes (K8s) 底层早已不再直接使用 Docker 引擎了
java·docker·kubernetes
不吃香菜kkk、20 小时前
通过夜莺n9e监控Kubernetes集群
安全·云原生·容器·kubernetes
淡泊if21 小时前
K8s 网络排障:从抓包开始,一步步定位诡异“502”
网络·kubernetes·k8s
小李的便利店21 小时前
k8s集群部署Prometheus和Grafana
kubernetes·grafana·prometheus·监控