从零开始写 Docker(十五)---实现 mydocker run -e 支持环境变量传递

docker,mydocker,run · 浏览次数 : 0

小编点评

**实现 mydocker run -e flag 的方法** ```go // NewParentProcess 设置环境变量 func NewParentProcess(tty bool, volume, containerId, imageName string, envSlice []string) (*exec.Cmd, *os.File) { // 省略其他内容 cmd.Env = append(os.Environ(), envSlice...) return cmd, writePipe } ``` **修改 mydocker exec 命令** ```go // ExecContainer 获取容器中的环境变量 func ExecContainer(containerName string, comArray []string) { // 省略其他内容 // 把容器中的环境变量append到 exec 进程 containerEnvs := getEnvsByPid(pid) cmd.Env = append(os.Environ(), containerEnvs...) } ``` **测试** ```go // 测试 ./mydocker run -d -name c1 -e user=17x -e name=mydocker busybox top func main() { // 创建容器 containerId := ExecContainer("c1", []string{"user=17x", "name=mydocker"}) // 停止指定容器 stopContainer(containerId) // 进入容器 execContainer(containerId, []string{"/proc/self/exe", "init"}) } ``` **运行示例** ``` # 克隆代码 git clone -b feat-run-e https://github.com/lixd/mydocker.gitcd mydocker # 编译go mod tidygo build . go mod tidygo build . # 测试 ./mydocker run -d -name c1 -e user=17x -e name=mydocker busybox top ./mydocker run -d -name c1 -e user=17x -e name=mydocker busybox top ``` **输出** ``` c1 user=17x name=mydocker ```

正文

mydocker-run-e.png

本文为从零开始写 Docker 系列第十五篇,实现 mydocker run -e, 支持在启动容器时指定环境变量,让容器内运行的程序可以使用外部传递的环境变量。


完整代码见:https://github.com/lixd/mydocker
欢迎 Star


推荐阅读以下文章对 docker 基本实现有一个大致认识:


开发环境如下:

root@mydocker:~# lsb_release -a
No LSB modules are available.
Distributor ID:	Ubuntu
Description:	Ubuntu 20.04.2 LTS
Release:	20.04
Codename:	focal
root@mydocker:~# uname -r
5.4.0-74-generic

注意:需要使用 root 用户

1. 概述

本节实现mydocker run -e flag,支持在启动容器时指定环境变量,让容器内运行的程序可以使用外部传递的环境变量。

2. 实现

实现也比较简单,就是在构建 cmd 的时候指定 Env 参数。

  • 1)run 命令增加 -e 参数
  • 2)cmd 中指定 Env 参数

run 命令增加 -e flag

在原来的基础上,增加 -e 选项指定环境变量,由于可能存在多个环境变量,因此允许用户通过多次使用 -e 选项来传递多个环境变量。

var runCommand = cli.Command{
	Name: "run",
	Usage: `Create a container with namespace and cgroups limit
			mydocker run -it [command]
			mydocker run -d -name [containerName] [imageName] [command]`,
	Flags: []cli.Flag{
    // 省略其他内容
		cli.StringSliceFlag{ // 增加 -e flag
			Name:  "e",
			Usage: "set environment,e.g. -e name=mydocker",
		},
	},

	Action: func(context *cli.Context) error {
		if len(context.Args()) < 1 {
			return fmt.Errorf("missing container command")
		}

		envSlice := context.StringSlice("e") // 获取 env 并传递
		Run(tty, cmdArray, envSlice, resConf, volume, containerName, imageName)
		return nil
	},
}

注意到这里的类型是cli. StringSliceFlag,即字符串数组参数,因为这是针对传入多个环境变量的情况。

然后增加对环境变量的解析,并且传递给 Run 函数。

cmd 对象指定 Env 参数

由于原来的 command 实际就是容器启动的进程,所以只需要在原来的基础上,增加一下环境变量的配置即可。

默认情况下,新启动进程的环境变量都是继承于原来父进程的环境变量,但是如果手动指定了环境变量,那么这里就会覆盖掉原来继承自父进程的变量

由于在容器的进程中,有时候还需要使用原来父进程的环境变量,比如 PATH 等,因此这里会使用 os.Environ() 来获取宿主机的环境变量,然后把自定义的变量加进去。

func NewParentProcess(tty bool, volume, containerId, imageName string, envSlice []string) (*exec.Cmd, *os.File) {
    // 省略其他内容
	cmd.Env = append(os.Environ(), envSlice...)
	return cmd, writePipe
}

到此,环境变量的实现就完成了。

3. 测试

通过 -e 注入两个环境变量测试一下

$ go build .
./mydocker run -it -name c1 -e user=17x -e name=mydocker busybox sh

然后在容器中查看环境变量

/ # env|grep user
user=17x
/ # env|grep name
name=mydocker

这里可以看到,手动指定的环境变量 user=17xname=mydocker 都已经可以在容器内可见了。

说明,-e flag 基本 ok。

下面创建一个后台运行的容器,查看一下是否可以。

root@mydocker:~/feat-run-e/mydocker# ./mydocker run -d -name c2 -e user=17x -e name=mydocker busybox top

查看 ID

root@mydocker:~/feat-run-e/mydocker# ./mydocker ps
ID           NAME        PID         STATUS      COMMAND     CREATED
9250006592   c2          228185      running     top         2024-02-27 10:37:09

然后通过 exec 命令进入容器,查看环境变量

root@mydocker:~/feat-run-e/mydocker# ./mydocker exec 9250006592 sh
# 容器内
/ # env|grep user
/ #

可以发现,并没有看到创建时指定的环境变量。

这里看不到环境变量的原因是:exec 命令其实是 mydocker 创建
的另外一个进程,这个进程的父进程其实是宿主机的的进程,并不是容器进程的。

因为在 Cgo 里面使用了 setns 系统调用,才使得这个进程进入到了容器内的命名空间

由于环境变量是继承自父进程的,因此这个 exec 进程的环境变量其实是继承自宿主机的,所以在 exec 进程内看到的环境变量其实是宿主机的环境变量

因此需要修改一下 exec 命令实现,使其能够看到容器中的环境变量。

4. 修改 mydocker exec 命令

首先提供了一个函数,可以根据指定的 PID 来获取对应进程的环境变量。

// getEnvsByPid 读取指定PID进程的环境变量
func getEnvsByPid(pid string) []string {
	path := fmt.Sprintf("/proc/%s/environ", pid)
	contentBytes, err := os.ReadFile(path)
	if err != nil {
		log.Errorf("Read file %s error %v", path, err)
		return nil
	}
	// env split by \u0000
	envs := strings.Split(string(contentBytes), "\u0000")
	return envs
}

由于进程存放环境变量的位置是/proc/<PID>/environ,因此根据给定的 PID 去读取这个文件,便可以获取环境变量。

在文件的内容中,每个环境变量之间是通过\u0000分割的,因此以此为标记来获取环境变量数组。

然后再启动 exec 进程时把容器中的环境变量也一并带上:

func ExecContainer(containerName string, comArray []string) {
	// 省略其他内容
	// 把指定PID进程的环境变量传递给新启动的进程,实现通过exec命令也能查询到容器的环境变量
	containerEnvs := getEnvsByPid(pid)
	cmd.Env = append(os.Environ(), containerEnvs...)
}

这样,exec 到容器内之后就可以看到所有的环境变量了。

再次测试一下,使用通用的 exec 命令进入容器,查看能否看到环境变量

root@mydocker:~/feat-run-e/mydocker# ./mydocker exec 9250006592 sh
/ # env|grep user
user=17x
/ # env|grep name
name=mydocker

ok,mydocker exec 已经可以获取到容器中的环境变量了。

5. 小结

本章实现了mydocker run -e flag 的添加,支持启动容器时传递环境变量到容器中。

核心实现就是启动 cmd 时指定 Env 参数,具体如下:

	cmd := exec.Command("/proc/self/exe", "init")
	cmd.Env = append(os.Environ(), envSlice...)

同时修改了 exec 命令,将容器中的环境变量append 到 exec 进程,便于查看。


【从零开始写 Docker 系列】持续更新中,搜索公众号【探索云原生】订阅,阅读更多文章。


完整代码见:https://github.com/lixd/mydocker
欢迎关注~

相关代码见 refactor-isolate-rootfs 分支,测试脚本如下:

需要提前在 /var/lib/mydocker/image 目录准备好 busybox.tar 文件,具体见第四篇第二节。

# 克隆代码
git clone -b feat-run-e https://github.com/lixd/mydocker.git
cd mydocker
# 拉取依赖并编译
go mod tidy
go build .
# 测试 
./mydocker run -d -name c1 -e user=17x -e name=mydocker busybox top
# 查看容器 Id
./mydocker ps
# stop 停止指定容器
./mydocker exec ${containerId} sh

与从零开始写 Docker(十五)---实现 mydocker run -e 支持环境变量传递相似的内容:

从零开始写 Docker(十三)---实现 mydocker rm 删除容器

本文为从零开始写 Docker 系列第十三篇,实现类似 docker rm 的功能,使得我们能够删除容器。 完整代码见:https://github.com/lixd/mydocker 欢迎 Star 推荐阅读以下文章对 docker 基本实现有一个大致认识: 核心原理:深入理解 Docker 核心

从零开始写 Docker(十二)---实现 mydocker stop 停止容器

本文为从零开始写 Docker 系列第十二篇,实现类似 docker stop 的功能,使得我们能够停止指定容器。 完整代码见:https://github.com/lixd/mydocker 欢迎 Star 推荐阅读以下文章对 docker 基本实现有一个大致认识: 核心原理:深入理解 Docke

从零开始写 Docker(十四)---重构:实现容器间 rootfs 隔离

本文为从零开始写 Docker 系列第十四篇,实现容器间的 rootfs 隔离,使得多个容器间互不影响。 完整代码见:https://github.com/lixd/mydocker 欢迎 Star 推荐阅读以下文章对 docker 基本实现有一个大致认识: 核心原理:深入理解 Docker 核心原

从零开始写 Docker(十八)---容器网络实现(下):为容器插上”网线“

本文为从零开始写 Docker 系列第十八篇,利用 linux 下的 Veth、Bridge、iptables 等等相关技术,构建容器网络模型,为容器插上”网线“。 完整代码见:https://github.com/lixd/mydocker 欢迎 Star 推荐阅读以下文章对 docker 基本实

从零开始写 Docker(十七)---容器网络实现(中):为容器插上”网线“

本文为从零开始写 Docker 系列第十七篇,利用 linux 下的 Veth、Bridge、iptables 等等相关技术,构建容器网络模型,为容器插上”网线“。 完整代码见:https://github.com/lixd/mydocker 欢迎 Star 推荐阅读以下文章对 docker 基本实

从零开始写 Docker(十六)---容器网络实现(上):为容器插上”网线”

本文为从零开始写 Docker 系列第十六篇,利用 linux 下的 Veth、Bridge、iptables 等等相关技术,构建容器网络模型,为容器插上”网线“。

《系列二》-- 6、从零开始的 bean 创建

[TOC] > 阅读之前要注意的东西:本文就是主打流水账式的源码阅读,主导的是一个参考,主要内容需要看官自己去源码中验证。全系列文章基于 spring 源码 5.x 版本。 写在开始前的话: 阅读spring 源码实在是一件庞大的工作,不说全部内容,单就最基本核心部分包含的东西就需要很长时间去消化了

手绘二维码

看到二维码,很容易猜到黑白相间的小方格就是二进制比特。那么这些比特是怎么得到的?小方格又是按照什么规则排布的?今天咱们就从零开始将一个 url 画成二维码。 考虑到大多数人可能不太了解二维码,所以先讲下基础概念。你也可以先看看左耳朵耗子写的二维码的生成细节和原理。 版本 二维码一共有 40 个尺寸,

从零开始带你上手体验Sermant自定义插件开发

本文对Sermant的自定义插件开发的流程进行了体验和探索,包括项目编译、运行、动态配置验证、插件拦截原理等内容,希望对初次体验Sermant高效开发插件的开发者有所帮助。

从零开始学Spring Boot系列-集成Spring Security实现用户认证与授权

在Web应用程序中,安全性是一个至关重要的方面。Spring Security是Spring框架的一个子项目,用于提供安全访问控制的功能。通过集成Spring Security,我们可以轻松实现用户认证、授权、加密、会话管理等安全功能。本篇文章将指导大家从零开始,在Spring Boot项目中集成S