K3s 构建
由于 K3s 仓库提供的构建脚本做了很多事情,所以实际底层是如何构建的反倒不清不楚了,当然之所为去看底下的实现细节是因为网络环境所致。

构建准备

安装 yq 工具,避免 download 脚本执行失败:

❯ sudo pacman -S go-yq

构建 k3s 过程中可能出现如下警告:

❯ make download
./.dapper download
DEPRECATED: The legacy builder is deprecated and will be removed in a future release.
            Install the buildx component to build images with BuildKit:
            https://docs.docker.com/go/buildx/

需要安装 buildx:

❯ sudo pacman -S docker-buildx
❯ pacman -Ql docker-buildx
docker-buildx /usr/lib/docker/cli-plugins/
docker-buildx /usr/lib/docker/cli-plugins/docker-buildx

Docker build wrapper
https://github.com/rancher/dapper

Docker Build architecture
https://docs.docker.com/build/architecture/

buildx - Installing
https://github.com/docker/buildx#manual-download

dapper 执行过程中可能出现如下错误:

❯ make download
./.dapper download
[+] Building 0.1s (5/11)                                                                 
 => ERROR [2/8] RUN apk -U --no-cache add bash
ERROR: failed to solve: process "/bin/sh -c apk -U --no-cache add bash && if [ \"$(go env GOARCH)\" = \"amd64\" ]; then apk -U --no-cache add mingw-w64-gcc; fi" did not complete successfully: failed to create endpoint qus5uzen3gyvq3zkvs4pqgw87 on network bridge: failed to add the host (veth4abe592) <=> sandbox (veth52cbefa) pair interfaces: operation not supported
FATA[0000] exit status 1                                
make: *** [Makefile:13: download] Error 1

记得更新内核之后重启机器。

In my case, the error appears every time I update my Linux kernel. It disappears when I restart the computer.

Docker: failed to add the pair interfaces (operation not supported)
https://serverfault.com/questions/738773/docker-failed-to-add-the-pair-interfaces-operation-not-supported

由于 go 的限制,Dockerfile.dapper 中 GOPROXY 只能设置成 direct

RUN GOPROXY=direct go install golang.org/x/tools/cmd/goimports@gopls/v0.11.0

否则会出现如下的错误:

export GOPROXY=https://goproxy.cn
❯ go install golang.org/x/tools/cmd/goimports@gopls/v0.11.0
go: golang.org/x/tools/cmd/goimports@gopls/v0.11.0: golang.org/x/tools/cmd/goimports@gopls/v0.11.0: invalid version: version "gopls/v0.11.0" invalid: disallowed version string

cmd/go/internal/modfetch: treat malformed versions as “not found” on the proxy path
https://github.com/golang/go/issues/32955

另外建议将 dapper 的 mode 显式设置成 bind(默认的 auto 在我的机器上是 cp):

diff --git a/Makefile b/Makefile
index aba74a95d2..0fad11ec3c 100644
--- a/Makefile
+++ b/Makefile
@@ -10,7 +10,7 @@ GO_FILES ?= $$(find . -name '*.go' | grep -v generated)
        @mv .dapper.tmp .dapper
 
 $(TARGETS): .dapper
-       ./.dapper $@
+       ./.dapper -m bind $@

正式构建

cd k3s
❯ mkdir -p build/data
❯ make download
❯ make generate

make download, make generate 执行的命令是 ./.dapper download, ./.dapper generate,而实际的处理流程是先用 dapper 构建一个容器镜像,然后基于这个容器镜像创建容器并执行 scripts/download, scripts/generate 脚本。

如果 dapper 所需的容器镜像构建成功,可以使用如下命令查看容器:

❯ docker images
REPOSITORY   TAG                 IMAGE ID       CREATED             SIZE
k3s          master              215e8fb356e5   About an hour ago   1.91GB
❯ docker run -it --rm -v .:/go/src/github.com/k3s-io/k3s/ k3s:master /bin/bash

上一节提到的问题都是 dapper 带来的,因此也完全可以不使用 dapper 准备构建环境,而直接在本地执行相应的脚本:

cd k3s
❯ sudo rm -rf bin build
❯ ./scripts/download
❯ ./scripts/generate

执行之后会生成如下的文件:

❯ make

注意 Makefile 中的定义 .DEFAULT_GOAL := ci,因此 make 等同于 make ci,等效于如下命令(不完全等同,可以阅读 scripts/ci 脚本进行理解):

❯ ./scripts/build
❯ ./scripts/package-cli

其中 package-cli 属于 package 的一部分,package 除了执行 package-cli 外,会创建基于 alpine 的 k3s agent 镜像,以及打包用于无网络环境(airgap)安装所需的其他容器镜像(如 klipper-helm,klipper-lb,local-path-provisioner 等)。

最终生成 k3s 可执行文件如下:

❯ ls dist/artifacts/k3s
dist/artifacts/k3s

需要注意的是,buildpackage-cli 都会生成 k3s 可执行文件,build 根据源文件 cmd/server/main.go 生成的 k3s 可执行文件在 bin 目录下,而 package-cli 根据源文件 cmd/k3s/main.go 生成的 k3s 可执行文件在 dist/artifacts 目录下,此外,根目录下的源文件 main.go 实际上也可以构建 k3s 可执行文件,它和 cmd/server/main.go 相比功能稍有差异,如没有 ctrtoken 子命令且无法作为 containerd 使用,但主要的目的还是用来执行 go generate 以生成资源文件用于 go-bindata 打包。

package-cli 将整个 bin 目录打包进其生成的 k3s 可执行文件(--data-dir 选项决定解压路径,--prefer-bundled-bin 选项决定 PATH 环境变量的优先级),package-cli 生成的 k3s 可执行文件在运行时将作为父进程 exec 调用 build 生成的 k3s 可执行文件。

❯ ls -l /var/lib/rancher/k3s/data/current/bin | grep k3s
lrwxrwxrwx 1 root root         3 Jun 15 09:12 containerd -> k3s
lrwxrwxrwx 1 root root         3 Jun 15 09:12 crictl -> k3s
lrwxrwxrwx 1 root root         3 Jun 15 09:12 ctr -> k3s
-rwxr-xr-x 1 root root 180893608 Jun 14 16:17 k3s
lrwxrwxrwx 1 root root         3 Jun 15 09:12 k3s-agent -> k3s
lrwxrwxrwx 1 root root         3 Jun 15 09:12 k3s-certificate -> k3s
lrwxrwxrwx 1 root root         3 Jun 15 09:12 k3s-completion -> k3s
lrwxrwxrwx 1 root root         3 Jun 15 09:12 k3s-etcd-snapshot -> k3s
lrwxrwxrwx 1 root root         3 Jun 15 09:12 k3s-secrets-encrypt -> k3s
lrwxrwxrwx 1 root root         3 Jun 15 09:12 k3s-server -> k3s
lrwxrwxrwx 1 root root         3 Jun 15 09:12 k3s-token -> k3s
lrwxrwxrwx 1 root root         3 Jun 15 09:12 kubectl -> k3s

影响 build 脚本的环境变量如下:

export DEBUG=true
export STATIC_BUILD=true

其中 DEBUG 用于生成带调试符号的 k3s 可执行文件,STATIC_BUILD 用于静态编译,而 package-cli 脚本则没有环境变量用于控制构建,如要生成带调试符号的 k3s 可执行文件需要手工修改 package-cli 脚本:

--- a/scripts/package-cli
+++ b/scripts/package-cli
@@ -56,11 +56,10 @@ CMD_NAME=dist/artifacts/k3s${BIN_SUFFIX}
 LDFLAGS="
     -X github.com/k3s-io/k3s/pkg/version.Version=$VERSION
     -X github.com/k3s-io/k3s/pkg/version.GitCommit=${COMMIT:0:8}
-    -w -s
 "
 TAGS="urfave_cli_no_docs"
 STATIC="-extldflags '-static'"
-CGO_ENABLED=0 "${GO}" build -tags "$TAGS" -ldflags "$LDFLAGS $STATIC" -o ${CMD_NAME} ./cmd/k3s/main.go
+CGO_ENABLED=0 "${GO}" build -tags "$TAGS" -gcflags "all=-N -l" -ldflags "$LDFLAGS $STATIC" -o ${CMD_NAME} ./cmd/k3s/main.go

根据上面对 buildpackage-cli 脚本的分析可知,如果只是开发环境进行调试的话,可以直接使用 cmd/server/main.go 进行构建并测试:

❯ ./scripts/download
❯ mkdir -p build/data
❯ mkdir -p build/static
❯ go generate
❯ go build -tags ctrd -o k3s ./cmd/server

或者直接使用根目录下的 main.go 进行构建并测试(不支持 containerd,无需指定 -tags 条件构建选项):

❯ ./scripts/download
❯ mkdir -p build/data
❯ mkdir -p build/static
❯ go generate
❯ go build

不使用 dist/artifacts/k3s 带来一个讨厌的问题是命令行自动补全没法用:

❯ ./k3s serFATA[0000] flag provided but not defined: -generate-bash-completion

❯ ./k3s --generate-bash-completion
Incorrect Usage. flag provided but not defined: -generate-bash-completion

可以简单的规避如下:

--- a/cmd/server/main.go
+++ b/cmd/server/main.go
@@ -43,6 +43,7 @@ func main() {
        os.Args[0] = cmd
 
        app := cmds.NewApp()
+       app.EnableBashCompletion = true
        app.Commands = []cli.Command{
                cmds.NewServerCommand(server.Run),
                cmds.NewAgentCommand(agent.Run),
diff --git a/main.go b/main.go
index ad09773f3e..36cab5a519 100644
--- a/main.go
+++ b/main.go
@@ -27,6 +27,7 @@ import (
 
 func main() {
        app := cmds.NewApp()
+       app.EnableBashCompletion = true
        app.Commands = []cli.Command{
                cmds.NewServerCommand(server.Run),
                cmds.NewAgentCommand(agent.Run),

自动补全

zsh

❯ vi ~/.zshrc
fpath+=(~/.oh-my-zsh/completion.d)
❯ mkdir ~/.oh-my-zsh/completion.d

❯ k3s completion zsh > ~/.oh-my-zsh/completion.d/_k3s
❯ rm -f ~/.zcompdump*

注意 fpath 的修改要在 source 之前。

Not sure where to put completions with Oh My Zsh
https://github.com/ohmyzsh/ohmyzsh/discussions/10774

bash

❯ k3s completion bash -i
Autocomplete for bash added to: /home/runsisi/.bashrc

或:

❯ k3s completion bash | sudo tee /usr/share/bash-completion/completions/k3s

命令行工具

k3s 可执行文件可以作为 kubectl, crictl, ctr, containerd 等程序使用:

❯ k3s
NAME:
   k3s - Kubernetes, but small and simple

USAGE:
   k3s [global options] command [command options] [arguments...]

VERSION:
   v1.27.2+k3s-b66a1183 (b66a1183)

COMMANDS:
   server           Run management server
   agent            Run node agent
   kubectl          Run kubectl
   crictl           Run crictl
   ctr              Run ctr

可以建立相应的符号链接直接使用,如:

❯ k3s ctr -v
ctr github.com/k3s-io/containerd v1.7.1-k3s1

❯ sudo ln -sr dist/artifacts/k3s /usr/local/bin/ctr
❯ /usr/local/bin/ctr -v
ctr github.com/k3s-io/containerd v1.7.1-k3s1

需要注意的是,前面提到的不使用 dist/artifacts/k3s 带来的另一个讨厌的问题是 kubectl, crictl, ctr 子命令都必须使用符号链接进行使用(不过 containerd 在任何时候都需要通过符号链接使用,而根目录下的 main.go 根本就不支持 containerd)。

Creating a Multi-Call Linux Binary
https://www.redbooks.ibm.com/abstracts/tips0092.html


最后修改于 2023-06-21