NVIDIA-Container-Toolkit—源码阅读—ctk

NVIDIA-Container-Toolkit—CodeReading—ctk

Posted by PYQ on September 26, 2023

nvidia-ctk

main.go

定义了一个options结构体,用于表示CLI启动的模式,包括Debug和Quiet模式:

1
2
3
4
type options struct {
	Debug bool
	Quiet bool
}

main函数创建了一个叫nvidia-ctk的cli程序,flag包括–debug和–quiet,这两个flag和options中的两个变量相关联,用于设置logger的日志级别,分别是DebugLevel和ErrorLevel,前者用于调试目的的日志级别,会输出详细的调试信息,后者用于输出错误信息。

子命令包括就是如下的各种命令。

config

create-default

只有一个create-default.go,属于defaultsubcommand包。

定义了一个command结构体:包含一个logger日志记录器,可以使用Debugf、Errorf、Info等方法进行日志记录。

1
2
3
type command struct {
	logger logger.Interface
}

NewCommand使得创建的命令可使用logger调用相关的方法进行日志记录。

1
2
3
4
5
6
func NewCommand(logger logger.Interface) *cli.Command {
	c := command{
		logger: logger,
	}
	return c.build()	
}

build函数创建了一个default命令,用于生成默认的nvidia-ctk配置文件,包括一个–output的flag,用于指定输出文件,若没有指定,默认为标准输出。

default在执行前会调用validateFlags函数,其实是调用了opts.Validate()检查参数,检查opts(options)的InPlace和Output(对应了–output)。

runc函数是default命令的执行函数,主要包括

  • 创建或加载配置文件(toml文件),涉及到internal中的相关代码
  • 创建输出目录和输出文件
  • 写入保存配置文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func (m command) run(c *cli.Context, opts *flags.Options) error {
	cfgToml, err := config.New()
	if err != nil {
		return fmt.Errorf("unable to load or create config: %v", err)
	}
	if err := opts.EnsureOutputFolder(); err != nil {
		return fmt.Errorf("failed to create output directory: %v", err)
	}
	output, err := opts.CreateOutput()
	if err != nil {
		return fmt.Errorf("failed to open output file: %v", err)
	}
	defer output.Close()
	_, err = cfgToml.Save(output)
	if err != nil {
		return fmt.Errorf("failed to write output: %v", err)
	}
	return nil
}

flags

options.go

属于flags包。

定义了一个Options结构体:

1
2
3
4
5
type Options struct {
	Config  string
	Output  string
	InPlace bool
}

Validate函数用于检查参数,–in-place和–output只能设置一个:

1
2
3
4
5
6
func (o Options) Validate() error {
	if o.InPlace && o.Output != "" {
		return fmt.Errorf("cannot specify both --in-place and --output")
	}
	return nil
}

GetOutput函数用于返回配置中的有效输出位置:设置了–in-place则返回Config,否则返回Output。

1
2
3
4
5
6
func (o Options) GetOutput() string {
	if o.InPlace {
		return o.Config
	}
	return o.Output
}

EnsureOutputFolder用于创建输出文件夹,未指定则不做任何事。

1
2
3
4
5
6
7
8
9
10
func (o Options) EnsureOutputFolder() error {
	output := o.GetOutput()
	if output == "" {
		return nil
	}
	if dir := filepath.Dir(output); dir != "" {
		return os.MkdirAll(dir, 0755)
	}
	return nil
}

CreateOutput函数创建输出文件,标准输出或者具体的输出文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
func (o Options) CreateOutput() (io.WriteCloser, error) {
	output := o.GetOutput()
	if output == "" {
		return nullCloser{os.Stdout}, nil
	}
	return os.Create(output)
}
type nullCloser struct {
	io.Writer
}
func (d nullCloser) Close() error {
	return nil
}

config.go

属于config包。

定义了一些结构体:options包括Config、InPlace和Output

1
2
3
4
5
6
7
type command struct {
	logger logger.Interface
}
type options struct {
	flags.Options
	sets cli.StringSlice
}

其他定义:

1
2
3
type configToml config.Toml
var errInvalidConfigOption = errors.New("invalid config option")
var errInvalidFormat = errors.New("invalid format")

build函数同样定义了一个config命令,包括如下四个参数:

1
2
3
4
--config-file
--set
--in-place
--output

config包含default作为子命令。

run函数是config命令的真正执行函数:

  • 创建一个新的配置文件config.toml
  • 遍历opts.sets中的元素,即–set选项,将其值转换为键值对,并设置到config.toml文件中
  • 确保输出文件夹的存在
  • 创建输出文件
  • 将配置文件写入保存到输出文件

setFlagToKeyValue函数用于将–set标志转换为键值对:例如–set “colors=red,green,blue” 则转换为colors [red green blue]

info

属于info包。

仅一个info.go文件,定义了一个info命令,没有任何flag和子命令,但似乎也没有该命令的运行函数

runtime

runtime.go

属于runtime包。

定义了一个runtime命令,子命令包括configure。

configure

属于configure包,仅一个configure.go。

定义的一些常量和结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const (
	defaultRuntime = "docker"
	// defaultNVIDIARuntimeName is the default name to use in configs for the NVIDIA Container Runtime
	defaultNVIDIARuntimeName = "nvidia"
	// defaultNVIDIARuntimeExecutable is the default NVIDIA Container Runtime executable file name
	defaultNVIDIARuntimeExecutable          = "nvidia-container-runtime"
	defaultNVIDIARuntimeExpecutablePath     = "/usr/bin/nvidia-container-runtime"
	defaultNVIDIARuntimeHookExpecutablePath = "/usr/bin/nvidia-container-runtime-hook"
	defaultContainerdConfigFilePath = "/etc/containerd/config.toml"
	defaultCrioConfigFilePath       = "/etc/crio/crio.conf"
	defaultDockerConfigFilePath     = "/etc/docker/daemon.json"
)
type config struct {
	dryRun         bool
	runtime        string
	configFilePath string
	mode           string      // 看下面代码包括oci-hook和config-file两种模式
	hookFilePath   string
	nvidiaRuntime struct {
		name         string
		path         string
		hookPath     string
		setAsDefault bool
	}
}

定义了一个configure命令,flag如下:

1
2
3
4
5
6
7
8
9
10
11
12
--dry-run  		更新配置但不写入磁盘
--runtime  		运行时,包括docker、cri-o、containerd,默认为
docker
--config   		目标运行时的配置文件目录
--config-mode 	运行时的配置模式(支持多种配置机制的运行时)
--oci-hook-path --config-mode=oci-hook时的钩子执行文件路径
--nvidia-runtime-name 定义nvidia runtime的名字,默认是nvidia
--nvidia-runtime-path 定义nvidia runtime的路径,别名runtime-path,默认
是/usr/bin/nvidia-container-runtime
--nvidia-runtime-hook-path 定义nvidia runtime hook的路径,默
认是/usr/bin/nvidia-container-runtime-hook
--nvidia-set-as-default  是否将nvidia runtime设置为默认运行时,别名set-as-default

validateFlags函数是configure命令运行前的操作:主要是检查运行时是否是绝对路径、配置的模式以及目标运行时。

configureWrapper函数是configure的实际运行函数,用于加载nvidia runtime到目标运行时中。

1
2
3
4
5
6
7
8
9
func (m command) configureWrapper(c *cli.Context, config *config) error {
	switch config.mode {
	case "oci-hook":
		return m.configureOCIHook(c, config)
	case "config-file":
		return m.configureConfigFile(c, config)
	}
	return fmt.Errorf("unsupported config-mode: %v", config.mode)
}

configureConfigFile更新目标运行时的配置文件来添加nvidia runtime的支持,原理是根据config.runtime去调用pkg/config/engine的docker、containerd和crio对其配置文件进行配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
func (m command) configureConfigFile(c *cli.Context, config *config) error {
	configFilePath := config.resolveConfigFilePath()
	var cfg engine.Interface
	var err error
	switch config.runtime {
	case "containerd":
		cfg, err = containerd.New(
			containerd.WithLogger(m.logger),
			containerd.WithPath(configFilePath),
		)
	case "crio":
		cfg, err = crio.New(
			crio.WithLogger(m.logger),
			crio.WithPath(configFilePath),
		)
	case "docker":
		cfg, err = docker.New(
			docker.WithLogger(m.logger),
			docker.WithPath(configFilePath),
		)
	default:
		err = fmt.Errorf("unrecognized runtime '%v'", config.runtime)
	}
	if err != nil || cfg == nil {
		return fmt.Errorf("unable to load config for runtime %v: %v", config.runtime, err)
	}
	err = cfg.AddRuntime(
		config.nvidiaRuntime.name,
		config.nvidiaRuntime.path,
		config.nvidiaRuntime.setAsDefault,
	)
	if err != nil {
		return fmt.Errorf("unable to update config: %v", err)
	}
	outputPath := config.getOuputConfigPath()
	n, err := cfg.Save(outputPath)
	if err != nil {
		return fmt.Errorf("unable to flush config: %v", err)
	}
	if outputPath != "" {
		if n == 0 {
			m.logger.Infof("Removed empty config from %v", outputPath)
		} else {
			m.logger.Infof("Wrote updated config to %v", outputPath)
		}
		m.logger.Infof("It is recommended that %v daemon be restarted.", config.runtime)
	}
	return nil
}

configureOCIHook用于将config.nvidiaRuntime.hookPath中的内容写入到config.hookFilePath中:

1
2
3
4
5
6
7
func (m *command) configureOCIHook(c *cli.Context, config *config) error {
	err := ocihook.CreateHook(config.hookFilePath, config.nvidiaRuntime.hookPath)
	if err != nil {
		return fmt.Errorf("error creating OCI hook: %v", err)
	}
	return nil
}

resolveConfigFilePath函数和getOuputConfigPath函数则是返回配置文件目录:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func (c *config) resolveConfigFilePath() string {
	if c.configFilePath != "" {
		return c.configFilePath
	}
	switch c.runtime {
	case "containerd":
		return defaultContainerdConfigFilePath
	case "crio":
		return defaultCrioConfigFilePath
	case "docker":
		return defaultDockerConfigFilePath
	}
	return ""
}
func (c *config) getOuputConfigPath() string {
	if c.dryRun {
		return ""
	}
	return c.resolveConfigFilePath()
}

hook

hook.go

属于hook包。

创建了一个hook命令,包含子命令

chmod

属于chmod包,就一个chmod.go。

config结构体:

1
2
3
4
5
type config struct {
	paths         cli.StringSlice
	mode          string
	containerSpec string
}

定义了一个chmod命令,包括–path、–mode和–container-spec三个flag。

在chmod命令执行前,会执行validateFlags函数来就检查命令参数,主要是,mode和path是否为空。

run和internel中的部分联系紧密,后续补充。

getPaths函数用于判断root目录下是否有paths中的路径:

1
2
3
4
5
6
7
8
9
10
11
12
func (m command) getPaths(root string, paths []string) []string {
	var pathsInRoot []string
	for _, f := range paths {
		path := filepath.Join(root, f)
		if _, err := os.Stat(path); err != nil {
			m.logger.Debugf("Skipping path %q: %v", path, err)
			continue
		}
		pathsInRoot = append(pathsInRoot, path)
	}
	return pathsInRoot
}

属于symlinks包,只有一个create-symlinks.go。

config结构体:

1
2
3
4
5
6
type config struct {
	hostRoot      string
	filenames     cli.StringSlice
	links         cli.StringSlice
	containerSpec string
}

创建了一个create-symlinks命令,包含的flag值:

1
2
3
4
--host-root		 用于指定主机文件系统上的根目录以解析符号链接
--csv-filename 	 用于指定要处理的(CSV)文件名,用户可以多次使用该标志来指定多个文件名
--link			 链接名,可多个
--container-spec 用于指定 OCI 容器规范的路径

changeRoot函数首先判断path是否为绝对路径(相对路径直接返回),如果是绝对路径,再解析出相对于当前路径current的相对路径,最后在与new进行拼接组成新的绝对路径并返回。

这里是相对路径的话为啥直接就返回path了呢

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func changeRoot(current string, new string, path string) (string, error) {
	if !filepath.IsAbs(path) {
		return path, nil
	}
	relative := path
	if current != "" {
		r, err := filepath.Rel(current, path)
		if err != nil {
			return "", err
		}
		relative = r
	}
	return filepath.Join(new, relative), nil
}

Locate函数用于获取指定文件名的符号链接目标。它首先检查文件是否为符号链接,如果是,则返回符号链接的目标路径,否则返回一个空切片。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func (m command) Locate(filename string) ([]string, error) {
	info, err := os.Lstat(filename)
	if err != nil {
		return nil, fmt.Errorf("failed to get file info: %v", info)
	}
	if info.Mode()&os.ModeSymlink == 0 {
		m.logger.Debugf("%v is not a symlink", filename)
		return nil, nil
	}
	target, err := os.Readlink(filename)
	if err != nil {
		return nil, fmt.Errorf("error checking symlink: %v", err)
	}
	m.logger.Debugf("Resolved link: '%v' => '%v'", filename, target)
	return []string{target}, nil
}

createLink函数用于创建一个linkpath到target的链接,linkpath是link在containerRoot中的路径:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func (m command) createLink(created map[string]bool, hostRoot string, containerRoot string, target string, link string) error {
	linkPath, err := changeRoot(hostRoot, containerRoot, link)
	if err != nil {
		m.logger.Warningf("Failed to resolve path for link %v relative to %v: %v", link, containerRoot, err)
	}
	if created[linkPath] {
		m.logger.Debugf("Link %v already created", linkPath)
		return nil
	}
	targetPath, err := changeRoot(hostRoot, "/", target)
	if err != nil {
		m.logger.Warningf("Failed to resolve path for target %v relative to %v: %v", target, "/", err)
	}
	m.logger.Infof("Symlinking %v to %v", linkPath, targetPath)
	err = os.MkdirAll(filepath.Dir(linkPath), 0755)
	if err != nil {
		return fmt.Errorf("failed to create directory: %v", err)
	}
	err = os.Symlink(target, linkPath)
	if err != nil {
		return fmt.Errorf("failed to create symlink: %v", err)
	}
	return nil
}

run函数看的有点迷惑,总体来说是加载容器状态,解析 CSV 文件,定位符号链接,创建符号链接,并处理自定义链接。

update-ldcache

属于ldcache包,仅有一个upate-ldcache.go。

似乎和gpu的一些东西有关,创建了一个update-ldcache命令。