k8s源码解析(2)--构建方式

构建方式

1. kubernetes 构建方式

Kubernetes构建方式可以分为2种:

  1. 本地环境构建(make, make all)

  2. 容器环境构建(make release, make quick-release)

在 kubernetes 的根目录下,有两个 Makefile 文件,分别是:

  • Makefile:顶层 Makefile 文件,描述了整个项目所有代码文件的编译顺序、编译规则及编译后的二进制输出等。

  • Makefile.generated_files:描述了代码生成的逻辑。

注意到,根目录下的这两个文件是一个软连接,连接到:

  • build/root/Makefile

  • build/root/Makefile.generated_files

1.1 构建 Makefile 过程

  • make all

    • 执行hack/make-rules/build.sh $(WHAT)

  • 依赖generated_files

  • generated_files

    • 执行$(MAKE) -f Makefile.generated_files

  • generated_files依赖gen_prerelease_lifecycle gen_deepcopy gen_defaulter gen_conversion gen_openapi

  • 执行相应的代码生成指令

1.2 本地构建

可以使用make或者make all命令。

  • 可以在 make 命令后面加 -n 参数,输出但不执行所有执行命令,这样可以展示更详细的构建过程。

  • 如果需要单独构建某一个组件,则需要指定WHAT参数,例如:make WHAT=cmd/kubectl

执行 make 或 make all 命令,会编译 Kubernetes 的所有组件,组件二进制文件输出的相对路径是 _output/bin/

使用 make clean 会清理构建环境。

[root@master kubernetes]# yum -y install rsync
[root@master kubernetes]# chmod +x _output/bin/prerelease-lifecycle-gen
[root@master kubernetes]# make all

1.2.1 make all分析

# 配置 shell 的路径
SHELL := /usr/bin/env bash

# PRINT_HELP 变量 表示是否打印帮助信息
# 条件赋值 ( ?= ) 如果变量未定义,则使用符号中的值定义变量。如果该变量已经赋值,则该赋值语句无效。
PRINT_HELP ?=

# make 时的flag标志,使用 --no-builtin-rules
MAKEFLAGS += --no-builtin-rules
# 空的 target
.SUFFIXES:

# 下面时一些常量的初始化
.EXPORT_ALL_VARIABLES:
# OUT_DIR 表示最终生成的 target 文件的目录
OUT_DIR ?= _output
# BIN_DIR 表示生成 target 可执行文件的目录
BIN_DIR := $(OUT_DIR)/bin
# kubernetes 的项目地址
PRJ_SRC_PATH := k8s.io/kubernetes
# 所有自动生成的文件前缀
GENERATED_FILE_PREFIX := zz_generated.

# make 在编译过程中生成的元数据 metadata 的存储目录
META_DIR := .make

# 如果定义了KUBE_GOFLAGS 提示已经废弃,使用 GOFLAGS
ifdef KUBE_GOFLAGS
# info 函数只是打印到标准输出
$(info KUBE_GOFLAGS is now deprecated. Please use GOFLAGS instead.)
# GOFLAGS 变量表示:在执行 make all 时,可以手动传入的编译时选项。比如go install GOFLAGS=-v
# -v是go的build flags 最终运行就是go install -v -v表示打印构建包的名称
ifndef GOFLAGS
GOFLAGS := $(KUBE_GOFLAGS)
unexport KUBE_GOFLAGS
else
$(error Both KUBE_GOFLAGS and GOFLAGS are set. Please use just GOFLAGS)
endif
endif

# 定义 ALL_HELP_INFO 变量,其内容是 help 帮助信息
# 当使用 make help 时,target 为 all 的版主信息就是 ALL_HELP_INFO 变量的内容
define ALL_HELP_INFO
# Build code.
#
# Args:
#   WHAT: Directory names to build.  If any of these directories has a 'main'
#     package, the build will produce executable files under $(OUT_DIR)/bin.
#     If not specified, "everything" will be built.
#   GOFLAGS: Extra flags to pass to 'go' when building.
#   GOLDFLAGS: Extra linking flags passed to 'go' when building.
#   GOGCFLAGS: Additional go compile flags passed to 'go' when building.
#   DBG: If set to "1", build with optimizations disabled for easier
#     debugging.  Any other value is ignored.
#
# Example:
#   make
#   make all
#   make all WHAT=cmd/kubelet GOFLAGS=-v
#   make all DBG=1
#     Note: Specify DBG=1 for building unstripped binaries, which allows you to use code debugging
#           tools like delve. When DBG is unspecified, it defaults to "-s -w" which strips debug
#           information.
endef

# 把 all 声明成 伪目标,也就是不会生成 target 文件
.PHONY: all
# PRINT_HELP 表示 如果是y 打印帮助信息 make all PRINT_HELP=y
ifeq ($(PRINT_HELP),y)
# 打印帮助信息
all:
 @echo "$$ALL_HELP_INFO"
else
# 开始编译,依赖 generated_files
# target generated_files 执行完后,执行 build.sh 脚本
all: generated_files
#build.sh 就是编译的过程,并且传参就是需要编译的软件包 比如make all WHAT=cmd/kubectl
 hack/make-rules/build.sh $(WHAT)
endif

1.2.2 make generated_files

define GENERATED_FILES_HELP_INFO
# Produce auto-generated files needed for the build.
#
# Example:
#   make generated_files
endef
.PHONY: generated_files
ifeq ($(PRINT_HELP),y)
generated_files:
 @echo "$$GENERATED_FILES_HELP_INFO"
else
# target generated_files 执行的编译命令为:make -f Makefile.generated_files generated_files CALLED_FROM_MAIN_MAKEFILE=1
# $@ 表示规则的目标文件名。也就是 target 的名称,这里就是 generated_files
generated_files gen_openapi:
# -f 指定了 make 使用的哪个 makefile,这里使用了另一个名字叫 Makefile.generated_files 的文件
# 指定了一个变量 CALLED_FROM_MAIN_MAKEFILE,这个变量表示,target generated_files 是从 main makefile 中调用的
 $(MAKE) -f Makefile.generated_files $@ CALLED_FROM_MAIN_MAKEFILE=1 SHELL="$(SHELL)"
endif

Makefile.generated_files 文件是通过 Main Makefile 中的 target : generated_files 调用的,总体的依赖顺序为:

  1. make all(Makefile) ->

  2. make generated_files(Makefile) ->

  3. make generated_files(Makefile.generated_files) ->

  4. make {gen_prerelease_lifecycle gen_deepcopy gen_defaulter gen_conversion gen_openapi }(Makefile.generated_files)

# 判断变量 CALLED_FROM_MAIN_MAKEFILE 是否存在,如果不存在,报错
# 这个变量表示 generated_files 是从 Main Makefile 中调用的,也就是上面讲的,make generated_files 通过make指令传进来的,值为1
ifeq ($(CALLED_FROM_MAIN_MAKEFILE),)
    $(error Please use the main Makefile, e.g. `make generated_files`)
endif

# 这里表示 这个文件不是面向用户的,用户不能使用这个文件 make all
# Don't allow an implicit 'all' rule.  This is not a user-facing file.
ifeq ($(MAKECMDGOALS),)
    $(error This Makefile requires an explicit rule to be specified)
endif

# 是否开启了 debug
ifeq ($(DBG_MAKEFILE),1)
    ifeq ($(MAKE_RESTARTS),)
        $(warning ***** starting Makefile.generated_files for goal(s) "$(MAKECMDGOALS)")
    else
        $(warning ***** restarting Makefile.generated_files for goal(s) "$(MAKECMDGOALS)")
    endif
    $(warning ***** $(shell date))
endif

# 把 generated_files 设置成 伪目标,不会生成 target file
.PHONY: generated_files
# target generated_files 一共需要依赖5个target
generated_files: gen_prerelease_lifecycle gen_deepcopy gen_defaulter gen_conversion gen_openapi

# 设置了 kubernetes 整个项目中所用到的依赖文件,将所有依赖的库文件名及路径保存在文件 go-pkgdeps.mk 中
GO_PKGDEPS_FILE = go-pkgdeps.mk

# META_DIR 在 Main Makefile 中已经定义了,META_DIR := .make
# include 函数表示导入另外的文件
# 这条命令也就是说,导入 .make/go-pkgdeps.mk 文件
include $(META_DIR)/$(GO_PKGDEPS_FILE)
ifeq ($(MAKE_RESTARTS),)
# 这里使用了 FORCE 关键字,来保证下面这一段每次都能够执行
# target 为 .make/go-pkgdeps.mk 文件,依赖 FORCE,FORCE 每次执行会更新,这里每次执行 make 时,都会重新生成 这个文件
$(META_DIR)/$(GO_PKGDEPS_FILE): FORCE
    # 如果 设置了 DBG_CODEGEN 则 打印一条信息
 if [[ "$(DBG_CODEGEN)" == 1 ]]; then          \
     echo "DBG: calculating Go dependencies";  \
 fi
 # 执行命令 安装 go2make 软件包
 KUBE_BUILD_PLATFORMS="" \
     hack/make-rules/build.sh hack/make-rules/helpers/go2make
 # 执行 go2make 命令,会生成一个  .make/go-pkgdeps.mk.tmp 文件
 # go2make工具会计算一组 Go 软件包的所有依赖关系并打印,将打印结果重定向到 .make/go-pkgdeps.mk.tmp 文件
 hack/run-in-gopath.sh go2make                       \
     k8s.io/kubernetes/...                           \
     --prune  k8s.io/kubernetes/staging              \
     --prune  k8s.io/kubernetes/vendor               \
     k8s.io/kubernetes/vendor/k8s.io/...             \
     > $@.tmp
 if [[ -s $@.tmp ]]; then                             \
     # 比较 .make/go-pkgdeps.mk.tmp 文件 和 .make/go-pkgdeps.mk 文件是否有区别
 # 如果 有差别。则将 .make/go-pkgdeps.mk.tmp 的内容拷贝到 .make/go-pkgdeps.mk 中
     if ! cmp -s $@.tmp $@; then                      \
         if [[ "$(DBG_CODEGEN)" == 1 ]]; then         \
             echo "DBG: $(GO_PKGDEPS_FILE) changed";  \
         fi;                                          \
         cat $@.tmp > $@;                             \
     fi                                               \
 else                                                 \
     kube::log::error "go2make produced no results";  \
     rm -f $@;                                        \
     false;                                           \
 fi
 # 如果没有差别,就删除 .make/go-pkgdeps.mk.tmp
 rm -f $@.tmp
endif # MAKE_RESTARTS
# 设置 FORCE 为伪目标,不生成 target 文件
.PHONY: FORCE
# 依赖为空、执行命令也为空的TARGET,则需要每次都重新生成
# 将 FORCE 置为空,不执行任何命令,但是每次编译时,所有依赖 FORCE 的 target 都会执行,因为 FORCE 更新了
FORCE:
  • 这里用到了 go2make 这个软件包,这个软件包是 kubernetes 中自带的工具,目录在 hack/make-rules/helpers/go2make/ 。这个工具的主要功能就是计算一组 Go 软件包的所有依赖关系并打印。

  • 在执行 Go 的命令时,都必须使用 hack/run-in-gopath.sh 脚本来执行。因为要执行 Go 的工具,必须确保安装了 golang,并且设置了 GOPATH。这个脚本就是设置了一个临时的 Kubernetes GOPATH 并在其下运行任意命令。运行的格式为:hack / run-in-gopath.sh <命令>

  • go2make 生成的文件在 KUBE_ROOT/.make/go-pkgdeps.mk

generated_files (Makefile.generated_files文件)目标的处理过程是:

  • 先处理依赖的五个 target

  • 然后利用 go2make 工具生成 .make/go-pkgdeps.mk 文件

  • go-pkgdeps.mk 文件直接引入

五个依赖的 target,分别是:

  • gen_prerelease_lifecycle

  • gen_deepcopy

  • gen_defaulter

  • gen_conversion

  • gen_openapi

这五个依赖的 target 其实就是自动生成对应的文件。

1.2.3 代码生成器(Makefile.generated_files文件)

kubernetes 项目中一共有五个代码生成器,分别是:

  • conversion-gen : 自动生成 Convert 函数的代码生成器,用于资源对象的版本转换函数。

  • deepcopy-gen : 自动生成 DeepCopy 函数的代码生成器,用于资源对象的深拷贝函数

  • defaulter-gen : 自动生成 Defaulter 函数的代码生成器,用于资源对象的默认实现或者配置等。

  • openapi-gen : 自动生成 OpenAPI 定义文件(OpenAPI Definition File)的代码生成器

  • prerelease-lifecycle-gen : 自动生成 api-status.csv文件的工具,将为所有 beta API 创建一个 zz_api_status.go文件,该文件指示种类、引入的版本、不推荐使用的版本、将删除的版本。

这些代码生成器工具,在 make all 编译后,都会生成在 output/bin/ 目录下。这些文件都是 Makefile.generatedfiles文件中定义的 target 目标文件。

Tags:

代码生成器通过Tags(标签)来识别一个包是否需要生成代码及确定生成代码的方式,Kubernetes提供的Tags可以分为如下两种:

  • 全局Tags:定义在每个包的 doc.go文件中,对整个包中的类型自动生成代码。例如:pkg/kubelet/apis/config/v1beta1/doc.go 文件中,标记了:// +k8s:deepcopy-gen=package // +k8s:defaulter-gen=TypeMeta 等等。

  • 局部Tags:定义在 Go 语言的类型声明上方,只对指定的类型自动生成代码。例如:vendor/k8s.io/kubelet/config/v1beta1/types.go 文件中,在定义结构体上方,标记了:// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

Tags 格式:

Tags的规则被定义在注释中,通常为:

  • //+tag-name 或

  • //+tag-name=value

其中,tag-name 可以为各个代码生成器的名称,例如 deepcopy-gen、defaulter-gen 等,还可以为 groupName , groupName 表示资源组名称,资源组名称一般使用域名形式命名。

Tags 位置:

  • 局部 Tags 一般定义在类型声明的上方,但如果该类型有注释信息,则局部 Tags 的定义需要与类型声明的注释信息之间至少有一个空行。

  • 全局 Tags 一般定义在 doc.go文件中。

1.2.3 寻找所有带有 // +k8s: 的Tags的文件列表

# ------------------寻找带有 Tags 的文件列表  -------------------------
#

# 这一段是为了判断是否需要更新缓存,缓存文件在 .make/all_go_dirs.mk
# 1. 如果缓存文件 .make/all_go_dirs.mk 不存在,则创建
# 2. 如果缓存文件存在,判断是否有比它更新的目录,以此判断是否需要更新缓存文件
ifeq ($(DBG_MAKEFILE),1)
    $(warning ***** finding all *.go dirs)
endif
# ALL_GO_DIRS 变量 保存了所有的 .go 文件的路径
# cache_go_dirs.sh 脚本是为了判断有没有比缓存文件更新的文件,如果有则更新缓存文件。
ALL_GO_DIRS := $(shell                                                   \
    hack/make-rules/helpers/cache_go_dirs.sh $(META_DIR)/all_go_dirs.mk  \
)
ifeq ($(DBG_MAKEFILE),1)
    $(warning ***** found $(shell echo $(ALL_GO_DIRS) | wc -w) *.go dirs)
endif

# 这一段代码是为了找到所有带 `+k8s:` Tags 的文件列表
ifeq ($(DBG_MAKEFILE),1)
    $(warning ***** finding all +k8s: tags)
endif
# ALL_K8S_TAG_FILES 变量保存了所有带 Tags 的文件列表
# find -maxdepth 1 表示最多搜索一级目录
# xargs 命令的作用,是将标准输入转为命令行参数。
# xargs grep 找到带有 // +k8s: Tags 的文件
ALL_K8S_TAG_FILES := $(shell                             \
    find $(ALL_GO_DIRS) -maxdepth 1 -type f -name \*.go  \
        | xargs grep --color=never -l '^// *+k8s:'       \
)
ifeq ($(DBG_MAKEFILE),1)
    $(warning ***** found $(shell echo $(ALL_K8S_TAG_FILES) | wc -w) +k8s: tagged files)
endif
  • 缓存文件为:.make/all_go_dirs.mk

  • 上面用到了 hack/make-rules/helpers/cache_go_dirs.sh 这个脚本,主要作用是更新缓存文件的

    • 这个脚本有三个作用:

      • 查找,有没有比传入的缓存文件更新的目录项

      • 如果没有,表示之前的构建操作与这一次构建没有区别,直接退出

      • 如果有,则更新缓存文件,将所有的包含 .go 文件的目录写入到 传入的缓存文件中

1.2.3 prerelease-lifecycle 代码生成器

# --------------------- prerelease-lifecycle 代码生成器逻辑-------------------

# 使用 prerelease-lifecycle 代码生成器的 Tags 格式:
#     // +k8s:prerelease-lifecycle-gen=true

# 在每个 package 中生成的文件名称
# GENERATED_FILE_PREFIX 变量在 Main Makefile 中定义为 GENERATED_FILE_PREFIX := zz_generated.
# 所以 prerelease-lifecycle 生成器生成的文件名为 zz_generated.prerelease-lifecycle.go
PRERELEASE_LIFECYCLE_BASENAME := $(GENERATED_FILE_PREFIX)prerelease-lifecycle
PRERELEASE_LIFECYCLE_FILENAME := $(PRERELEASE_LIFECYCLE_BASENAME).go

# 使用 _output/bin/prerelease-lifecycle-gen 工具来生成代码
PRERELEASE_LIFECYCLE_GEN := $(BIN_DIR)/prerelease-lifecycle-gen

# 下面是找到所有 带有 Tags 为 +k8s:prerelease-lifecycle-gen=true 的package
ifeq ($(DBG_MAKEFILE),1)
    $(warning ***** finding all +k8s:prerelease-lifecycle-gen tags)
endif
# 从所有带 `// +k8s:` Tags 的文件列表 ALL_K8S_TAG_FILES 中,找到带 '+k8s:prerelease-lifecycle-gen=true' 的文件的目录列表
# xargs -n 参数指定每次将多少项,作为命令行参数。-n 1 表示将前面管道中的输出作为一次命令行参数
PRERELEASE_LIFECYCLE_DIRS := $(shell                                                 \
    grep --color=never -l '+k8s:prerelease-lifecycle-gen=true' $(ALL_K8S_TAG_FILES)  \
        | xargs -n1 dirname                                                          \
        | LC_ALL=C sort -u                                                           \
)
ifeq ($(DBG_MAKEFILE),1)
    $(warning ***** found $(shell echo $(PRERELEASE_LIFECYCLE_DIRS) | wc -w) +k8s:prerelease-lifecycle-gen tagged dirs)
endif

#  PRERELEASE_LIFECYCLE_FILES 表示在每一个 Tags 为 '+k8s:prerelease-lifecycle-gen=true' 的文件的目录后,添加自动生成的文件名,也就是自动生成文件的路径+名称
# $(addsuffix suffix,names…) 添加后缀,names 可以有多个,用空格分开,例如 $(addsuffix .c,foo bar) 结果为 foo.c bar.c
# PRERELEASE_LIFECYCLE_FILES 表示格式为:path/to/files/zz_generated.prerelease-lifecycle.go 的列表
PRERELEASE_LIFECYCLE_FILES := $(addsuffix /$(PRERELEASE_LIFECYCLE_FILENAME), $(PRERELEASE_LIFECYCLE_DIRS))

# 重置需要生成的软件包列表。
# 生成目录 .make/_output/bin/
$(shell mkdir -p $$(dirname $(META_DIR)/$(PRERELEASE_LIFECYCLE_GEN)))
# 并且删除 .make/_output/bin/prerelease-lifecycle-gen.todo 文件
# todo 文件,表示这个构建需要做的目标
$(shell rm -f $(META_DIR)/$(PRERELEASE_LIFECYCLE_GEN).todo)

# target gen_prerelease_lifecycle 的主逻辑,依赖两个文件:
# 1. _output/bin/prerelease-lifecycle-gen
# 2. .make/_output/bin/prerelease-lifecycle-gen.todo (todo 文件其实就是需要生成代码的目录列表,统计一共生成了多少个文件)
.PHONY: gen_prerelease_lifecycle
gen_prerelease_lifecycle: $(PRERELEASE_LIFECYCLE_GEN) $(META_DIR)/$(PRERELEASE_LIFECYCLE_GEN).todo
    # 判断 .make/_output/bin/prerelease-lifecycle-gen.todo   文件是否存在
 if [[ -s $(META_DIR)/$(PRERELEASE_LIFECYCLE_GEN).todo ]]; then                 \
     # paste 命令 把每个文件以列对列的方式,一列列地加以合并
     # -s 表示 将文件粘贴成一行,-d, 表示使用逗号, 作为分隔符,例如:文件为 aaa\nbbb\n ,那paste结果为 aaa,bbb
     # 单$表示引用makefile定义变量的值,双$$表示引用shell命令中定义的变量的值。
     pkgs=$$(cat $(META_DIR)/$(PRERELEASE_LIFECYCLE_GEN).todo | paste -sd, -);  \
     if [[ "$(DBG_CODEGEN)" == 1 ]]; then                                       \
         echo "DBG: running $(PRERELEASE_LIFECYCLE_GEN) for $$pkgs";            \
     fi;                                                                        \
     # 统计 prerelease-lifecycle-gen.todo 的行数
     N=$$(cat $(META_DIR)/$(PRERELEASE_LIFECYCLE_GEN).todo | wc -l);            \
     echo "Generating prerelease lifecycle code for $$N targets";               \
     # 这一步就是自动生成代码
     # 利用 ./hack/run-in-gopath.sh 脚本 执行 代码生成工具 prerelease-lifecycle-gen 来生成代码,并且传入参数
     # $@ 表示目标文件, $$@ 表示将 目标文件变量放入到 shell 中
     ./hack/run-in-gopath.sh $(PRERELEASE_LIFECYCLE_GEN)                        \
         --v $(KUBE_VERBOSE)                                                    \
         --logtostderr                                                          \
         -i "$$pkgs"                                                            \
         -O $(PRERELEASE_LIFECYCLE_BASENAME)                                    \
         "$$@";                                                                 \
 fi

# 这里的代码是自动生成所有的 path/to/files/zz_generated.prerelease-lifecycle 文件
# makefile 中的 fareach 函数 $(foreach <var>,<list>,<text> )
# 意思是,把参数<list>中的单词逐一取出放到参数<var>所指定的变量中,然后再执行<text>所包含的表达式
# makefile 中的 eval 函数,表示 作为makefile的一部分而被make解析和执行。
# 这段的意思就是,根据 PRERELEASE_LIFECYCLE_DIRS 目录中的每一项,都生成一个 path/to/zz_generated.prerelease-lifecycle.go 的target
# 在 Main Makefile 文件中定义了:PRJ_SRC_PATH := k8s.io/kubernetes
# 这个 target 依赖 GODEPS_k8s.io/kubernetes/path/to/file
$(foreach dir, $(PRERELEASE_LIFECYCLE_DIRS), $(eval                            \
    $(dir)/$(PRERELEASE_LIFECYCLE_FILENAME): $(GODEPS_$(PRJ_SRC_PATH)/$(dir))  \
))

# 为了生成 .make/_output/bin/prerelease-lifecycle-gen.todo 文件
# 定义了 target:.make/_output/bin/prerelease-lifecycle-gen.todo
# 依赖:所有的目标文件 path/to/file/zz_generated.prerelease-lifecycle
$(META_DIR)/$(PRERELEASE_LIFECYCLE_GEN).todo: $(PRERELEASE_LIFECYCLE_FILES)

# 生成所有的 path/to/file/zz_generated.prerelease-lifecycle 文件
# 依赖 _output/bin/prerelease-lifecycle-gen 文件
# 这里也就是生成所有的 zz_generated.prerelease-lifecycle 文件,并且将这些文件的目录组合成 k8s.io/kubernetes/path/to/file 写入 todo 文件
$(PRERELEASE_LIFECYCLE_FILES): $(PRERELEASE_LIFECYCLE_GEN)
 if [[ "$(DBG_CODEGEN)" == 1 ]]; then        \
     echo "DBG: prerelease-lifecycle needed $(@D):";  \
     ls -lf --full-time $@ $? || true;       \
 fi
 # $@ 表示目标文件集,而 $(@D) 就表示 $@ 的目录部分
 # 这句话就是将每一个需要生成代码的目录组合成 k8s.io/kubernetes/path/to/file 追加写入到 .make/_output/bin/prerelease-lifecycle-gen.todo 文件中
 echo $(PRJ_SRC_PATH)/$(@D) >> $(META_DIR)/$(PRERELEASE_LIFECYCLE_GEN).todo

# 编译并生成 prerelease-lifecycle 代码生成器工具
# 依赖:$(GODEPS_k8s.io/kubernetes/vendor/k8s.io/code-generator/cmd/prerelease-lifecycle-gen) 没有找到这个变量,那就依赖为空
$(PRERELEASE_LIFECYCLE_GEN): $(GODEPS_k8s.io/kubernetes/vendor/k8s.io/code-generator/cmd/prerelease-lifecycle-gen)
 # 使用 hack/make-rules/build.sh 脚本来编译生成 prerelease-lifecycle-gen 代码生成器工具,编译的地址在 ./vendor/k8s.io/code-generator/cmd/prerelease-lifecycle-gen
 # 其实 这个目录应该是在 staging/src/k8s.io/code-generator/cmd/prerelease-lifecycle-gen
 KUBE_BUILD_PLATFORMS="" hack/make-rules/build.sh ./vendor/k8s.io/code-generator/cmd/prerelease-lifecycle-gen
 # 创建文件 _output/bin/prerelease-lifecycle-gen
 touch $@

上面的逻辑中,生成了一个 .make/_output/bin/prerelease-lifecycle-gen.todo 文件,这个文件记录了在这次构建中,一共有哪些 package 需要使用 prerelease-lifecycle-gen 代码生成器自动生成代码的。生成的代码名称为:zz_generated.prerelease-lifecycle.go 。

  1. prerelease-lifecycle-gen 是一个自动生成 zz_generated.prerelease-lifecycle.go 文件的代码生成器。

  2. zz_generated.prerelease-lifecycle.go 这个文件中主要包括一些 API prerelease-lifecycle 代码,我的理解就是 API 的生命周期,例如:对于 certificates/v1beta1 API,这个 API 接口是从哪一个版本中出现的,将要在哪个版本中丢弃。

  3. 生成 prerelease-lifecycle 相关函数时,其 Tags 形式可以为:

    • // +k8s:prerelease-lifecycle-gen:deprecated=1.21 -> 表示 生成的 APILifecycleDeprecated 函数返回的版本为 1.21

    • // +k8s:prerelease-lifecycle-gen:replacement=<group>,<version>,<kind> -> 表示 生成的 APILifecycleReplacement 函数返回 schema.GroupVersionKind 结构体

  4. prerelease-lifecycle-gen 生成器一共有这几个函数:

函数名

Tags

说明

APILifecycleIntroduced()

k8s:prerelease-lifecycle-gen:introduced

返回这个 API 接口是从哪个版本开始的

APILifecycleDeprecated()

k8s:prerelease-lifecycle-gen:deprecated

返回这个 API 接口将在哪个版本丢弃,但 API 仍旧会提供服务

APILifecycleReplacement()

k8s:prerelease-lifecycle-gen:replacement=,,

表示这个 API 接口有更新,返回应该使用的类型结构

APILifecycleRemoved()

k8s:prerelease-lifecycle-gen:removed

返回这个 API 接口会在哪个把版本不再提供服务

代码示例:vendor/k8s.io/api/networking/v1beta1/zz_generated.prerelease-lifecycle.go

1.2.4 deepcopy-gen 代码生成器

deepcopy-gen 代码生成器:

  1. deepcopy-gen 是一个自动生成 zz_generated.deepcopy.go 文件的代码生成器。

  2. zz_generated.deepcopy.go 文件主要包括两个函数,DeepCopy() 和 DeepCopyInto(),功能就是深度拷贝,也就是生成一个相同的结构体,两个内存空间。

  3. 生成 deepcopy 函数时,其 Tags 形式可以为:

    • +k8s:deepcopy-gen=true : 为单个类型生成DeepCopy相关函数

    • +k8s:deepcopy-gen=package : 为整个包生成DeepCopy相关函数

    • +k8s:deepcopy-gen=false : 为整个包生成DeepCopy相关函数时,可以忽略单个类型

  4. deepcopy-gen 生成器生成的函数介绍:

函数

说明

Tags

DeepCopy()

复制接收者,并创建一个新的与接收者相同的深度拷贝返回

// +k8s:deepcopy-gen=true

DeepCopyInto(*type)

复制接收者,并创建一个新的与接收者相同的深度拷贝写入到函数参数中

// +k8s:deepcopy-gen=package

DeepCopyObject()

复制接收者,并创建一个新的runtime.Object

// +k8s:deepcopy-gen:interfaces=xxx/runtime.Object

代码示例:vendor/k8s.io/api/core/v1/zz_generated.deepcopy.go

1.2.5 defaulter-gen 代码生成器

  1. defaulter-gen 是一个自动生成 zz_generated.defaults.go 文件的代码生成器。

  2. zz_generated.defaults.go 文件主要功能就是为资源对象生成默认值。每一个对象会生成一个专门的函数,xxxTypeDefaults() ,用于给对象幅默认值。

  3. 生成 Defaulter 函数时,其 Tags 形式可以为:

    • +k8s:defaulter-gen=TypeMeta: 为拥有 TypeMeta 属性的类型生成Defaulter相关函数

    • +k8s:defaulter-gen=ListMeta : 为拥有 ListMeta 属性的类型生成Defaulter相关函数

    • +k8s:defaulter-gen=ObjectMeta : 为拥有 ObjectMeta 属性的类型生成Defaulter相关函数

    • +k8s:defaulter-gen-input=path/to/package : 当前包会依赖于指定的路径包

  4. defaulter-gen 生成器生成的函数介绍:

函数名

Tags

说明

RegisterDefaults()

k8s:defaulter-gen:TypeMeta

将该函数内的 Setxxx 方法注册到 runtime.schema 中,并且公开这个 schema

SetObjectDefaults_Xxxx

None

设置具体某个类型对象 Xxxx 的默认值

代码示例:pkg/apis/batch/v1beta1/zz_generated.defaults.go

1.2.6 conversion-gen代码生成器

  1. conversion-gen 是一个自动生成 zz_generated.conversion.go 文件的代码生成器。

  2. zz_generated.conversion.go 文件主要功能就是为对象在内部和外部类型之间提供转换函数。每一个对象会生成一组转换的函数,例如:Convert_v1alpha1_PolicyList_To_audit_PolicyList() 和 Convert_audit_PolicyList_To_v1alpha1_PolicyList,PolicyList 是一种类型,conversion-gen为该类型生成了Convert函数。

  3. 生成 conversion 函数时,其 Tags 形式可以为:

    • +k8s:conversion-gen= INTERNAL_TYPES_DIR : 为整个包生成Convert相关函数,INTERNAL_TYPES_DIR 用于定义包的导入路径,例如k8s.io/kubernetes/pkg/apis/abac。

    • +k8s:conversion-gen-external-types= EXTERNAL_TYPES_DIR : 为整个包生成Convert相关函数且依赖其他包,EXTERNAL_TYPES_DIR 用于定义其他包的路径,例如k8s.io/api/autoscaling/v1。

  4. conversion-gen 生成器生成的函数介绍:

函数名

Tags

说明

RegisterConversions()

// +k8s:conversion-gen-external-types= EXTERNAL_TYPES_DIR // +k8s:conversion-gen=INTERNAL_TYPES_DIR

将该函数内的 Convert 方法注册到 runtime.schema 中,并且公开这个 schema

Convert_v1alpha1_EventList_To_audit_EventList

None

从 v1alpha1_EventList 转换成 audit_EventList

Convert_audit_EventList_To_v1alpha1_EventList

None

从 audit_EventList 转换成 v1alpha1_EventList

代码示例:vendor/k8s.io/apiserver/pkg/apis/audit/v1alpha1/zz_generated.conversion.go

1.2.7 openapi-gen 代码生成器

  1. openapi-gen 是一个自动生成 zz_generated.openapi.go 文件的代码生成器。

  2. zz_generated.openapi.go 文件主要功能就是可以为其生成 OpenAPI 定义文件,该文件用于kube-apiserver服务上的OpenAPI规范的生成。函数名为:GetOpenAPIDefinitions() ,该函数会返回 map,key 表示 API ,value 表示 定义这个 API 的函数。

  3. 生成 openapi 函数时,其 Tags 形式可以为:

    • +k8s:openapi-gen=true : 为特定类型或包生成OpenAPI定义文件。

    • +k8s:openapi-gen=false : 排除为特定类型或包生成OpenAPI定义文件。

  4. openapi-gen 生成器生成的函数介绍:

函数名

Tags

说明

GetOpenAPIDefinitions()

// +k8s:openapi-gen=true

将该函数会返回一个 map ,是一个 API 的对应关系

schema_API_PATH()

None

返回这个 API 的详细定义,返回应该是一个 Json 对象这个函数可以为:schema_k8sio_api_autoscaling_v1_ContainerResourceMetricSource(ref) 对应的 API 接口为:k8s.io/api/autoscaling/v1.ContainerResourceMetricSource

代码路径:vendor/k8s.io/sample-apiserver/pkg/generated/openapi/zz_generated.openapi.go

1.3 容器环境构建

Kubernetes提供了两种容器环境下的构建方式:

  • make release:构建所有的目标平台(Darwin、Linux、Windows),构建过程会比较久,并同时执行单元测试过程。

  • make quick-release:快速构建,只构建当前平台,并略过单元测试过程。

define RELEASE_SKIP_TESTS_HELP_INFO
# Build a release, but skip tests
#
# Args:
#   KUBE_RELEASE_RUN_TESTS: Whether to run tests. Set to 'y' to run tests anyways.
#   KUBE_FASTBUILD: Whether to cross-compile for other architectures. Set to 'false' to do so.
#   KUBE_DOCKER_REGISTRY: Registry of released images, default to k8s.gcr.io
#   KUBE_BASE_IMAGE_REGISTRY: Registry of base images for controlplane binaries, default to k8s.gcr.io
#
# Example:
#   make release-skip-tests
#   make quick-release
endef
.PHONY: release-skip-tests quick-release
# 如果是打印帮助信息
ifeq ($(PRINT_HELP),y)
release-skip-tests quick-release:
 @echo "$$RELEASE_SKIP_TESTS_HELP_INFO"
# 执行构建过程
else
# 配置两个环境变量用于控制编译过程
# 1. KUBE_RELEASE_RUN_TESTS 是否执行单元测试
# 2. KUBE_FASTBUILD 快速构建,也就是跳过交叉编译
release-skip-tests quick-release: KUBE_RELEASE_RUN_TESTS = n
release-skip-tests quick-release: KUBE_FASTBUILD = true
release-skip-tests quick-release:
 build/release.sh
endif

make quick-release 的逻辑与 make all 的逻辑是相似的,就是最后的构建过程不同,这里主要是使用了 build/release.sh 脚本进行的编译

容器环境的构建过程,主要是在 build/release.sh 脚本中定义:

#!/usr/bin/env bash


set -o errexit
set -o nounset
set -o pipefail

# 源码的 根目录
KUBE_ROOT=$(dirname "${BASH_SOURCE[0]}")/..

# build/common.sh 会加载很多 环境变量,并且加载一些常用的公用方法
source "${KUBE_ROOT}/build/common.sh"
# lib/release.sh 加载一些与 容器环境构建相关的 方法
source "${KUBE_ROOT}/build/lib/release.sh"

# 是否开启 单元测试,默认开启
KUBE_RELEASE_RUN_TESTS=${KUBE_RELEASE_RUN_TESTS-y}

# 构建步骤:
# 1. 容器构建环境的配置和验证
kube::build::verify_prereqs
# 2. build image 构建镜像
kube::build::build_image
# 3. 构建方法
kube::build::run_build_command make cross
#    3.1 是否开启单元测试
if [[ $KUBE_RELEASE_RUN_TESTS =~ ^[yY]$ ]]; then
  kube::build::run_build_command make test
  kube::build::run_build_command make test-integration
fi

# 4. 将文件从容器中拷贝到主机
kube::build::copy_output

# 5. 打包
kube::release::package_tarballs

从上面的代码中,可以看到 Kubernetes 容器环境构建过程分为几步:

  1. 容器构建环境的配置和验证(kube::build::verify_prereqs)

  2. build image 构建镜像(kube::build::build_image)

  3. 具体构建方法(kube::build::run_build_command)

  4. 将文件从容器中拷贝到主机(kube::build::copy_output)

  5. 打包(kube::build::package_tarballs)

步骤1-环境配置与验证

(kube::build::verify_prereqs) 进行构建环境的配置及验证。该过程会检查本机是否安装了Docker容器环境,而对于Darwin平台,该过程会检查本机是否安装了docker-machine环境。kube::build::verify_prereqs 方法在 build/common.sh ,其具体内容为:

# 拉取镜像的 project 名称
readonly KUBE_BUILD_IMAGE_REPO=kube-build
# 拉取镜像的 仓库
readonly KUBE_DOCKER_REGISTRY="${KUBE_DOCKER_REGISTRY:-k8s.gcr.io}"
readonly KUBE_BASE_IMAGE_REGISTRY="${KUBE_BASE_IMAGE_REGISTRY:-k8s.gcr.io/build-image}"

# KUBE_BUILD_IMAGE_CROSS_TAG 表示 要拉取 的基本镜像的 tag 名: v1.16.1-1
readonly KUBE_BUILD_IMAGE_CROSS_TAG="$(cat "${KUBE_ROOT}/build/build-image/cross/VERSION")"

# image 的版本号,我这里是 5 ,
readonly KUBE_BUILD_IMAGE_VERSION_BASE="$(cat "${KUBE_ROOT}/build/build-image/VERSION")"
readonly KUBE_BUILD_IMAGE_VERSION="${KUBE_BUILD_IMAGE_VERSION_BASE}-${KUBE_BUILD_IMAGE_CROSS_TAG}"


# 验证是否安装了 docker 环境
# 传入参数:是否 要求 有docker环境,默认 true
function kube::build::verify_prereqs() {
  local -r require_docker=${1:-true}
  kube::log::status "Verifying Prerequisites...."

  # 确认 tar 命令
  kube::build::ensure_tar || return 1
  # 确认 rsync 命令
  kube::build::ensure_rsync || return 1
  # 如果必须要有 docker 运行环境
  if ${require_docker}; then
    # 确认 docker 环境
    kube::build::ensure_docker_in_path || return 1
    # 如果是 Darwin 系统
    if kube::build::is_osx; then
        # 检查 docker.sock
        kube::build::docker_available_on_osx || return 1
    fi
    # 检查 是否可以运行 docker 命令
    kube::util::ensure_docker_daemon_connectivity || return 1
    # 打印 docker Version 信息
    if (( KUBE_VERBOSE > 6 )); then
      kube::log::status "Docker Version:"
      "${DOCKER[@]}" version | kube::log::info_from_stdin
    fi
  fi

  # 当前的 git 分支
  KUBE_GIT_BRANCH=$(git symbolic-ref --short -q HEAD 2>/dev/null || true)
  # 用 md5 命令 求 hash 值
  KUBE_ROOT_HASH=$(kube::build::short_hash "${HOSTNAME:-}:${KUBE_ROOT}:${KUBE_GIT_BRANCH}")
  # build 出 image 的 tag前缀 为 build-Hash值
  KUBE_BUILD_IMAGE_TAG_BASE="build-${KUBE_ROOT_HASH}"
  # KUBE_BUILD_IMAGE_VERSION 为 5-v1.16.1-1
  # build 出 image 的 整个 tag
  KUBE_BUILD_IMAGE_TAG="${KUBE_BUILD_IMAGE_TAG_BASE}-${KUBE_BUILD_IMAGE_VERSION}"
  # 需要拉取的镜像名称:kube-build:build-HASH-5-v1.16.1-1
  KUBE_BUILD_IMAGE="${KUBE_BUILD_IMAGE_REPO}:${KUBE_BUILD_IMAGE_TAG}"
  # 这里一共需要 三个容器来进行构建工作:
  # 1. BUILD 容器,构建容器
  # 2. RSYNC 容器,同步数据容器
  # 3. DATA 容器,存储容器
  # build container Name : kube-build-HASH-5-v1.16.1-1
  KUBE_BUILD_CONTAINER_NAME_BASE="kube-build-${KUBE_ROOT_HASH}"
  KUBE_BUILD_CONTAINER_NAME="${KUBE_BUILD_CONTAINER_NAME_BASE}-${KUBE_BUILD_IMAGE_VERSION}"
  # RSYNC container NAME: kube-rsync-build-HASH-5-v1.16.1-1
  KUBE_RSYNC_CONTAINER_NAME_BASE="kube-rsync-${KUBE_ROOT_HASH}"
  KUBE_RSYNC_CONTAINER_NAME="${KUBE_RSYNC_CONTAINER_NAME_BASE}-${KUBE_BUILD_IMAGE_VERSION}"
  # DATA container Name: kube-build-data-build-HASH-5-v1.16.1-1
  KUBE_DATA_CONTAINER_NAME_BASE="kube-build-data-${KUBE_ROOT_HASH}"
  KUBE_DATA_CONTAINER_NAME="${KUBE_DATA_CONTAINER_NAME_BASE}-${KUBE_BUILD_IMAGE_VERSION}"
  # DATA 容器挂载目录的路径
  DOCKER_MOUNT_ARGS=(--volumes-from "${KUBE_DATA_CONTAINER_NAME}")
  # 这个是 image 的 output 路径
  LOCAL_OUTPUT_BUILD_CONTEXT="${LOCAL_OUTPUT_IMAGE_STAGING}/${KUBE_BUILD_IMAGE}"

  # 设置 git 相关的环境变量
  kube::version::get_version_vars
  # 将环境变量 保存在 指定的文件中
  kube::version::save_version_vars "${KUBE_ROOT}/.dockerized-kube-version-defs"

  # Without this, the user's umask can leak through.
  umask 0022
}

根据 kube::build::verify_prereqs 的源码,可以看到这个函数的功能:

  • 验证系统中有没有 tar、rsync、docker 的环境

  • 设置相关的环境变量

  • 将环境变量保存在文件中

在设置环境变量时,我们发现,设置了三个容器的名称,在容器环境构建过程中,主要是这三个容器镜像参与其中,分别:

  • build 容器(kube-cross):即构建容器,在该容器中会对代码文件执行构建操作,完成后其会被删除。

  • data 容器:即存储容器,用于存放构建过程中所需的所有文件。

  • rsync 容器:即同步容器,用于在容器和主机之间传输数据,完成后其会被删除。

这里用到的基础镜像 kube-cross ,需要从 k8s.gcr.io 仓库拉取,我这里用了代理拉取。完整的基础镜像名称为: k8s.gcr.io/build-image/kube-cross:v1.16.1-1

步骤2-编译镜像

(kube::build::build_image) 主要是用来编译镜像,一共需要编译上面提到的三个镜像。该函数在 build/common.sh 源码如下:

# 这个是 image 的 output 路径
LOCAL_OUTPUT_BUILD_CONTEXT="${LOCAL_OUTPUT_IMAGE_STAGING}/${KUBE_BUILD_IMAGE}"

function kube::build::build_image() {
  # 创建 编译容器 的 Dockerfile 的目录, 目录为: _output/images/kube-build:build-HASH-5-v1.16.1-1
  mkdir -p "${LOCAL_OUTPUT_BUILD_CONTEXT}"
  # Make sure the context directory owned by the right user for syncing sources to container.
  # 修改 Dockerfile 所在的目录 的 属主和属组
  chown -R "${USER_ID}":"${GROUP_ID}" "${LOCAL_OUTPUT_BUILD_CONTEXT}"

  # 将 时区文件 拷贝到 该目录中
  cp /etc/localtime "${LOCAL_OUTPUT_BUILD_CONTEXT}/"
  chmod u+w "${LOCAL_OUTPUT_BUILD_CONTEXT}/localtime"

  # 拷贝 build/build-image/Dockerfile 文件 和 build/build-image/rsyncd.sh 脚本
  cp "${KUBE_ROOT}/build/build-image/Dockerfile" "${LOCAL_OUTPUT_BUILD_CONTEXT}/Dockerfile"
  cp "${KUBE_ROOT}/build/build-image/rsyncd.sh" "${LOCAL_OUTPUT_BUILD_CONTEXT}/"
  # dd 可从标准输入或文件中读取数据,根据指定的格式来转换数据,再输出到文件、设备或标准输出。
  # 参数:if=文件名:输入文件名; of=文件名:输出文件名 ;bs=bytes:同时设置读入/输出的块大小为bytes个字节 ; count=blocks:仅拷贝blocks个块,块大小等于ibs指定的字节数。
  # 生成随机的密码
  dd if=/dev/urandom bs=512 count=1 2>/dev/null | LC_ALL=C tr -dc 'A-Za-z0-9' | dd bs=32 count=1 2>/dev/null >"${LOCAL_OUTPUT_BUILD_CONTEXT}/rsyncd.password"
  chmod go= "${LOCAL_OUTPUT_BUILD_CONTEXT}/rsyncd.password"

  # 使用 docker 命令 build image
  # 第一个参数:需要 build 的 image 名称
  # 第二个参数:Dockerfile 所在的目录
  # 第三个参数:-pull 参数,默认 true,表示是否下载最新的版本
  # 第四个参数:--build-args 参数,表示build时的环境变量
  # kube::build::docker_build 这个函数就是执行一个 docker build 命令
  # 完整的 docker build 命令为:docker build -t "${image}" "--pull=${pull}" "${build_args[@]}" "${context_dir}"
  kube::build::docker_build "${KUBE_BUILD_IMAGE}" "${LOCAL_OUTPUT_BUILD_CONTEXT}" 'false' "--build-arg=KUBE_BUILD_IMAGE_CROSS_TAG=${KUBE_BUILD_IMAGE_CROSS_TAG} --build-arg=KUBE_BASE_IMAGE_REGISTRY=${KUBE_BASE_IMAGE_REGISTRY}"

  # Clean up old versions of everything
  # 清除 所有满足正则 的 container
  # 第一个参数,要清除的 container 名称的前缀
  # 第二个参数,要保留的 container 名称
  # 这里主要是清除 之前 编译构建的 BUILD、RSYNC、DATA cantainer
  kube::build::docker_delete_old_containers "${KUBE_BUILD_CONTAINER_NAME_BASE}" "${KUBE_BUILD_CONTAINER_NAME}"
  kube::build::docker_delete_old_containers "${KUBE_RSYNC_CONTAINER_NAME_BASE}" "${KUBE_RSYNC_CONTAINER_NAME}"
  kube::build::docker_delete_old_containers "${KUBE_DATA_CONTAINER_NAME_BASE}" "${KUBE_DATA_CONTAINER_NAME}"

  # 删除所有与标签前缀匹配的 image(“当前”版本除外)
  kube::build::docker_delete_old_images "${KUBE_BUILD_IMAGE_REPO}" "${KUBE_BUILD_IMAGE_TAG_BASE}" "${KUBE_BUILD_IMAGE_TAG}"
  # 确保 DATA 容器运行
  kube::build::ensure_data_container
  # 将 本机的数据 拷贝到 容器中
  kube::build::sync_to_container
}

构建容器镜像的流程如下。

  • 通过 mkdir 命令创建构建镜像的文件夹(即_output/images/…)。

  • 通过 cp 命令复制构建镜像所需的相关文件,如Dockerfile文件和rsyncd同步脚本等。

  • 通过 kube::build::docker_build 函数,构建容器镜像。

  • 通过 kube::build::ensure_data_container 函数,运行存储容器并挂载Volume。

  • 通过 kube::build::sync_to_container 函数,运行同步容器并挂载存储容器的 Volume,然后通过 rsync 命令同步 Kubernetes 源码到存储容器的 Volume。

步骤3-构建方法

此时,容器构建环境已经准备好,下面开始运行构建容器并在构建容器内部执行构建Kubernetes源码的操作,构建过程在 kube::build::run_build_command make cross。 该函数位于:build/common.sh,代码示例如下:

#  <container name> <extra docker args> -- <command>
# 传入参数格式为:<container name> <extra docker args> -- <command>
function kube::build::run_build_command_ex() {
  # $# 表示参数的个数
  # 如果参数 == 0 报错
  [[ $# != 0 ]] || {
    echo "Invalid input - please specify a container name." >&2
    return 4
  }
  # 第一个参数为 容器的名称
  local container_name="${1}"
  shift

  # 运行运行时的 选项
  # --name 容器的名称
  # --user 以哪个用户运行容器
  # --hostname 容器的hostname
  # --volumes-from 挂载目录
  local -a docker_run_opts=(
    "--name=${container_name}"
    "--user=$(id -u):$(id -g)"
    "--hostname=${HOSTNAME}"
    "${DOCKER_MOUNT_ARGS[@]}"
  )

  local detach=false

  # 此时已经取出了第一个参数 $1 ,并且 shift 后,$1 就为第二个桉树
  # $# 就为 总参数个数-1
  [[ $# != 0 ]] || {
    echo "Invalid input - please specify docker arguments followed by --." >&2
    return 4
  }
  # until 循环执行一系列命令直至条件为 true 时停止。
  # 此时 $1 为 第二个参数 extra docker args,也就是循环遍历所有的 选项
  until [ -z "${1-}" ]; do
    # 如果是  --  标识符,直接跳出
    if [[ "$1" == "--" ]]; then
      shift
      break
    fi
    # 否则,将选项添加进  docker_run_opts
    docker_run_opts+=("$1")
    if [[ "$1" == "-d" || "$1" == "--detach" ]]; then
      detach=true
    fi
    shift
  done

  # 在 -- 标识符后,是编译的命令,如果没有 则报错
  [[ $# != 0 ]] || {
    echo "Invalid input - please specify a command to run." >&2
    return 4
  }

  local -a cmd=()
  # 标准的 参数循环方法
  until [ -z "${1-}" ]; do
    cmd+=("$1")
    shift
  done

  # 添加环境变量:
  docker_run_opts+=(
    --env "KUBE_FASTBUILD=${KUBE_FASTBUILD:-false}"
    --env "KUBE_BUILDER_OS=${OSTYPE:-notdetected}"
    --env "KUBE_VERBOSE=${KUBE_VERBOSE}"
    --env "KUBE_BUILD_WITH_COVERAGE=${KUBE_BUILD_WITH_COVERAGE:-}"
    --env "KUBE_BUILD_PLATFORMS=${KUBE_BUILD_PLATFORMS:-}"
    --env "GOFLAGS=${GOFLAGS:-}"
    --env "GOGCFLAGS=${GOGCFLAGS:-}"
    --env "SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH:-}"
  )

  # use GOLDFLAGS only if it is set explicitly.
  if [[ -v GOLDFLAGS ]]; then
    docker_run_opts+=(
      --env "GOLDFLAGS=${GOLDFLAGS:-}"
    )
  fi

  if [[ -n "${DOCKER_CGROUP_PARENT:-}" ]]; then
    kube::log::status "Using ${DOCKER_CGROUP_PARENT} as container cgroup parent"
    docker_run_opts+=(--cgroup-parent "${DOCKER_CGROUP_PARENT}")
  fi
      
  if [[ -t 0 ]]; then
    docker_run_opts+=(--interactive --tty)
  elif [[ "${detach}" == false ]]; then
    docker_run_opts+=("--attach=stdout" "--attach=stderr")
  fi

  # docker_cmd 是一个列表,里面包含了 docker run 的完整命令
  local -ra docker_cmd=(
    "${DOCKER[@]}" run "${docker_run_opts[@]}" "${KUBE_BUILD_IMAGE}")

  # 删掉之前构建时的同名 container
  kube::build::destroy_container "${container_name}"
  
  # 执行构建操作的 具体命令
  # docekr_cmd 是 docker run 的完整命令,而 cmd 是 构建的命令,也就是 make cross
  "${docker_cmd[@]}" "${cmd[@]}"
  
  # 判断是否保留容器
  if [[ "${detach}" == false ]]; then
    kube::build::destroy_container "${container_name}"
  fi
}

通过${docker_cmd[@]}""${cmd[@]}命令执行构建操作(即在容器内执行make cross命令)。容器内的构建过程与本地环境下的构建过程相同

步骤4-将文件从容器中拷贝到主机

(kube::build::copy_output)使用同步容器,将编译后的代码文件复制到主机上。该函数位于:build/common.sh,源码为:

# 将 编译后的所有文件 从容器中 拷贝到 主机
function kube::build::copy_output() {
  kube::log::status "Syncing out of container"

  # 开启 RSYNCD 容器,并且会设置 该容器的 ip 地址 到 KUBE_RSYNC_ADDR 环境变量
  kube::build::start_rsyncd_container

  # 使用 rsync 命令过滤 目录 进行有选择的拷贝
  kube::build::rsync \
    --prune-empty-dirs \
    --filter='- /_temp/' \
    --filter='+ /vendor/' \
    --filter='+ /staging/***/Godeps/**' \
    --filter='+ /_output/dockerized/bin/**' \
    --filter='+ zz_generated.*' \
    --filter='+ generated.proto' \
    --filter='+ *.pb.go' \
    --filter='+ types.go' \
    --filter='+ */' \
    --filter='- /**' \
    "rsync://k8s@${KUBE_RSYNC_ADDR}/k8s/" "${KUBE_ROOT}"
 
  # 关闭 RSYNCD 容器
  kube::build::stop_rsyncd_container
}

根据 kube::build::copy_output 函数的源码,该函数的步骤为:

  1. 启动 RSYNCD 容器,并且获取该容器的 ip 地址,存储在 KUBE_RSYNC_ADDR 环境变量中。

  2. 使用 rsync 命令,对构建后的目录进行过滤,将需要的目录,从容器中拷贝到本地目录。

  3. 停止 RSYNCD 容器,并且 unset KUBE_RSYNC_ADDR 。

步骤5-打包

(kube::release::package_tarballs)函数用于打包,将二进制文件打包到 output 目录中。最终,代码文件以 tar.gz 压缩包的形式输出至 output/release-tars 文件夹。该函数位于:build/lib/release.sh,源码为:

function kube::release::package_tarballs() {
  # Clean out any old releases
  # 删除之前构建的目录
  rm -rf "${RELEASE_STAGE}" "${RELEASE_TARS}" "${RELEASE_IMAGES}"
  mkdir -p "${RELEASE_TARS}"
  
  # 下面就是使用 tar 来压缩 各个文件夹
  kube::release::package_src_tarball &
  kube::release::package_client_tarballs &
  kube::release::package_kube_manifests_tarball &
  kube::util::wait-for-jobs || { kube::log::error "previous tarball phase failed"; return 1; }

  kube::release::package_node_tarballs &
  kube::release::package_server_tarballs &
  kube::util::wait-for-jobs || { kube::log::error "previous tarball phase failed"; return 1; }

  kube::release::package_final_tarball & # _final depends on some of the previous phases
  kube::release::package_test_tarballs & # _test doesn't depend on anything
  kube::util::wait-for-jobs || { kube::log::error "previous tarball phase failed"; return 1; }
}

打包很简单,就是利用 tar 命令对不同的目录分别进行打包操作。

1.4 Bazel环境构建

Bazel 是 Google 公司开源的一个自动化软件构建和测试工具。Bazel 使用分布式缓存和增量构建方法,使构建更加快速。其支持构建任务,包括运行编译器和链接器以生成可执行程序和库。Bazel 与 Make、Gradle 及 Maven 等构建工具类似,但Bazel在构建速度、可扩展性、灵活性及跨语言和对不同平台的支持上更加出色。

Bazel 优势:

  • 高级别的构建语言:项目以BUILD语言进行描述。BUILD是一种简洁的文本格式,可描述多个小而互相关联的库、二进制程序和测试程序组成的项目。

  • 支持多平台:相同的工具和BUILD文件可以为不同架构或平台构建软件。

  • 再现性:在BUILD文件中,必须明确为每个库、测试程序、二进制文件指定其直接依赖。在修改源码文件后,Bazel使用这个依赖信息就可以知道哪些东西必须重新构建,哪些任务可以并行执行。这意味着所有的构建都是以增量的形式构建的并能够每次都生成相同的结果。

  • 可扩展性强:Bazel可以处理大型程序的构建;在Google公司内,一个二进制程序通常有超过100KB的源码文件,在代码文件没有被改动的情况下,构建过程大约需要200ms。

  • 构建速度快:支持增量编译。对依赖关系进行了优化,从而支持并发执行。

一般在根目录下有一个 WORKSPACE(工作区)文件,用于指定当前目录是 Bazel 的一个工作区域,该文件一般存放在项目根目录下。另外,项目中包含一个或多个 BUILD 文件,用于告诉 Bazel 如何进行构建。Bazel 工作原理大致分为3部分:

  1. 加载与 Target 相关的 BUILD 文件。

  2. 分析 BUILD 文件的内容,生成ActionGraph。

  3. 执行 Action Graph,最后产出 Outputs。

这里我们的k8s源码,显然没有支持这种构建方式。但是早期的k8s是支持这种构建方式的。

这里我们看一下https://github.com/grpc-ecosystem/grpc-gateway

可以找到WORKSPACE以及bazel等文件

做为了解内容


k8s源码解析(2)--构建方式
http://47.123.5.226:8090//archives/k8syuan-ma-jie-xi-2---gou-jian-fang-shi
作者
pony
发布于
2024年05月09日
许可协议