自己动手写docker2
目录:
构建镜像
pivot_root
pivot_root是一个系统调用,用于改变root文件系统。pivot_root可以将当前进程的root文件系统移动到put_old目录,然后使new_root成为新的root文件系统。new_root和put_old不能同时存在当前root的同一个文件系统
pivot_root和chroot不同的是,pivot_root将整个系统切换到一个新的目录,而移除了对之前root文件系统的依赖,可以umount掉原来的root文件系统。而chroot是针对进程,而系统的其他部分还运行在旧的root文件系统
pivotRoot方法逻辑为
func pivotRoot(root string) error {
/**
为了使当前root的老 root 和新 root 不在同一个文件系统下,我们把root重新mount了一次
bind mount是把相同的内容换了一个挂载点的挂载方法
*/
if err := syscall.Mount(root, root, "bind", syscall.MS_BIND|syscall.MS_REC, ""); err != nil {
return fmt.Errorf("Mount rootfs to itself error: %v", err)
}
// 创建 rootfs/.pivot_root 存储 old_root
pivotDir := filepath.Join(root, ".pivot_root")
if err := os.Mkdir(pivotDir, 0777); err != nil {
return err
}
// pivot_root 到新的rootfs, 现在老的 old_root 是挂载在rootfs/.pivot_root
// 挂载点现在依然可以在mount命令中看到
if err := syscall.PivotRoot(root, pivotDir); err != nil {
return fmt.Errorf("pivot_root %v", err)
}
// 修改当前的工作目录到根目录
if err := syscall.Chdir("/"); err != nil {
return fmt.Errorf("chdir / %v", err)
}
pivotDir = filepath.Join("/", ".pivot_root")
// umount rootfs/.pivot_root
if err := syscall.Unmount(pivotDir, syscall.MNT_DETACH); err != nil {
return fmt.Errorf("unmount pivot_root dir %v", err)
}
// 删除临时文件夹
return os.Remove(pivotDir)
}
进行pivotRoot和挂载操作
func setUpMount() {
pwd, err := os.Getwd()
if err != nil {
log.Errorf("Get current location error %v", err)
return
}
log.Infof("Current location is %s", pwd)
pivotRoot(pwd)
//mount proc
defaultMountFlags := syscall.MS_NOEXEC | syscall.MS_NOSUID | syscall.MS_NODEV
syscall.Mount("proc", "/proc", "proc", uintptr(defaultMountFlags), "")
syscall.Mount("tmpfs", "/dev", "tmpfs", syscall.MS_NOSUID|syscall.MS_STRICTATIME, "mode=755")
}
根目录可以使用busybox的
mkdir busybox
docker pull busybox
docker run -d busybox top -b
docker export -o busybox.tar <container-id>
tar xf busybox.tar -C busybox/
cd busybox
在busybox目录启动容器
使用aufs包装busybox
docker在启动容器的时候,除了镜像的只读层larer,会启动两个layer,container-init layer和write layer
创建只读层,将tar包解压到指定目录
func CreateReadOnlyLayer(rootURL string) {
busyboxURL := rootURL + "busybox/"
busyboxTarURL := rootURL + "busybox.tar"
exist, err := PathExists(busyboxURL)
if err != nil {
log.Infof("Fail to judge whether dir %s exists. %v", busyboxURL, err)
}
if exist == false {
if err := os.Mkdir(busyboxURL, 0777); err != nil {
log.Errorf("Mkdir dir %s error. %v", busyboxURL, err)
}
if _, err := exec.Command("tar", "-xvf", busyboxTarURL, "-C", busyboxURL).CombinedOutput(); err != nil {
log.Errorf("Untar dir %s error %v", busyboxURL, err)
}
}
}
创建可写层
func CreateWriteLayer(rootURL string) {
writeURL := rootURL + "writeLayer/"
if err := os.Mkdir(writeURL, 0777); err != nil {
log.Errorf("Mkdir dir %s error. %v", writeURL, err)
}
}
通过aufs合并挂载
func CreateMountPoint(rootURL string, mntURL string) {
if err := os.Mkdir(mntURL, 0777); err != nil {
log.Errorf("Mkdir dir %s error. %v", mntURL, err)
}
dirs := "dirs=" + rootURL + "writeLayer:" + rootURL + "busybox"
cmd := exec.Command("mount", "-t", "aufs", "-o", dirs, "none", mntURL)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
log.Errorf("%v", err)
}
}
整体就是
//Create a AUFS filesystem as container root workspace
func NewWorkSpace(rootURL string, mntURL string) {
CreateReadOnlyLayer(rootURL)
CreateWriteLayer(rootURL)
CreateMountPoint(rootURL, mntURL)
}
父进程在fork子进程之前,会将这些层挂载,并配置好子进程再进行启动
func NewParentProcess(tty bool) (*exec.Cmd, *os.File) {
readPipe, writePipe, err := NewPipe()
if err != nil {
log.Errorf("New pipe error %v", err)
return nil, nil
}
cmd := exec.Command("/proc/self/exe", "init")
cmd.SysProcAttr = &syscall.SysProcAttr{
Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS |
syscall.CLONE_NEWNET | syscall.CLONE_NEWIPC,
}
if tty {
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
}
cmd.ExtraFiles = []*os.File{readPipe}
mntURL := "/root/mnt/"
rootURL := "/root/"
NewWorkSpace(rootURL, mntURL)
// 执行目录替换为mntURL
cmd.Dir = mntURL
return cmd, writePipe
}
在删除容器的时候父进程也会将容器对应的层删除
- umount通过aufs挂载的目录,并删除
- 删除可写层
//Delete the AUFS filesystem while container exit
func DeleteWorkSpace(rootURL string, mntURL string){
DeleteMountPoint(rootURL, mntURL)
DeleteWriteLayer(rootURL)
}
func DeleteMountPoint(rootURL string, mntURL string){
cmd := exec.Command("umount", mntURL)
cmd.Stdout=os.Stdout
cmd.Stderr=os.Stderr
if err := cmd.Run(); err != nil {
log.Errorf("%v",err)
}
if err := os.RemoveAll(mntURL); err != nil {
log.Errorf("Remove dir %s error %v", mntURL, err)
}
}
func DeleteWriteLayer(rootURL string) {
writeURL := rootURL + "writeLayer/"
if err := os.RemoveAll(writeURL); err != nil {
log.Errorf("Remove dir %s error %v", writeURL, err)
}
volume数据卷
就是在之前的基础上,容器启动时
- 创建只读层
- 创建读写层
- 创建挂载点,将只读层和读写层通过aufs的方式合并挂载到挂载点
- 将挂载点作为容器根目录
容器退出时
- 卸载挂载点,并删除
- 删除读写层
添加-v标签识别挂载目录
Flags: []cli.Flag{
cli.BoolFlag{
Name: "ti",
Usage: "enable tty",
},
cli.StringFlag{
Name: "v",
Usage: "volume",
},
},
在完成之前的挂载后,挂载volume
func NewWorkSpace(rootURL string, mntURL string, volume string) {
CreateReadOnlyLayer(rootURL)
CreateWriteLayer(rootURL)
CreateMountPoint(rootURL, mntURL)
if(volume != ""){
volumeURLs := volumeUrlExtract(volume)
length := len(volumeURLs)
if(length == 2 && volumeURLs[0] != "" && volumeURLs[1] !=""){
MountVolume(rootURL, mntURL, volumeURLs)
log.Infof("%q",volumeURLs)
}else{
log.Infof("Volume parameter input is not correct.")
}
}
}
MountVolume在mnt挂载点中创建挂载目录,将volume也通过aufs的方式挂载到mnt挂载点的目录中
func MountVolume(rootURL string, mntURL string, volumeURLs []string) {
parentUrl := volumeURLs[0]
if err := os.Mkdir(parentUrl, 0777); err != nil {
log.Infof("Mkdir parent dir %s error. %v", parentUrl, err)
}
containerUrl := volumeURLs[1]
containerVolumeURL := mntURL + containerUrl
if err := os.Mkdir(containerVolumeURL, 0777); err != nil {
log.Infof("Mkdir container dir %s error. %v", containerVolumeURL, err)
}
dirs := "dirs=" + parentUrl
cmd := exec.Command("mount", "-t", "aufs", "-o", dirs, "none", containerVolumeURL)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
log.Errorf("Mount volume failed. %v", err)
}
}
退出的时候先卸载volume的挂载点,然后再执行之前的退出流程
func DeleteMountPoint(rootURL string, mntURL string){
cmd := exec.Command("umount", mntURL)
cmd.Stdout=os.Stdout
cmd.Stderr=os.Stderr
if err := cmd.Run(); err != nil {
log.Errorf("%v",err)
}
if err := os.RemoveAll(mntURL); err != nil {
log.Infof("Remove mountpoint dir %s error %v", mntURL, err)
}
}
func DeleteWorkSpace(rootURL string, mntURL string, volume string){
if(volume != ""){
volumeURLs := volumeUrlExtract(volume)
length := len(volumeURLs)
if(length == 2 && volumeURLs[0] != "" && volumeURLs[1] !=""){
DeleteMountPointWithVolume(rootURL, mntURL, volumeURLs)
}else{
DeleteMountPoint(rootURL, mntURL)
}
}else {
DeleteMountPoint(rootURL, mntURL)
}
DeleteWriteLayer(rootURL)
}
镜像打包
容器退出的时候是会删除可写层的内容,commit的功能是将运行状态保留下来
添加commit命令
app.Commands = []cli.Command{
initCommand,
runCommand,
commitCommand,
}
对应的操作
var commitCommand = cli.Command{
Name: "commit",
Usage: "commit a container into image",
Action: func(context *cli.Context) error {
if len(context.Args()) < 1 {
return fmt.Errorf("Missing container name")
}
imageName := context.Args().Get(0)
//commitContainer(containerName)
commitContainer(imageName)
return nil
},
}
将文件系统的目录进行打包
func commitContainer(imageName string){
mntURL := "/root/mnt"
imageTar := "/root/" + imageName + ".tar"
fmt.Printf("%s",imageTar)
if _, err := exec.Command("tar", "-czf", imageTar, "-C", mntURL, ".").CombinedOutput(); err != nil {
log.Errorf("Tar folder %s error %v", mntURL, err)
}
}
构建容器进阶
容器后台运行
docker是通过runc的detach的功能,使得runc启动容器后,交由父进程containerd-shim接管
cli加入-d参数
var runCommand = cli.Command{
Name: "run",
Usage: `Create a container with namespace and cgroups limit
mydocker run -ti [command]`,
Flags: []cli.Flag{
cli.BoolFlag{
Name: "ti",
Usage: "enable tty",
},
cli.BoolFlag{
Name: "d",
Usage: "detach container",
},
...
对于-it和-d服务有不同的操作,但是不能同时生效
createTty := context.Bool("ti")
detach := context.Bool("d")
if createTty && detach {
return fmt.Errorf("ti and d paramter can not both provided")
}
然后对这两种情况进行判断处理
func Run(tty bool, comArray []string, res *subsystems.ResourceConfig) {
parent, writePipe := container.NewParentProcess(tty)
if parent == nil {
log.Errorf("New parent process error")
return
}
if err := parent.Start(); err != nil {
log.Error(err)
}
// use mydocker-cgroup as cgroup name
cgroupManager := cgroups.NewCgroupManager("mydocker-cgroup")
defer cgroupManager.Destroy()
cgroupManager.Set(res)
cgroupManager.Apply(parent.Process.Pid)
sendInitCommand(comArray, writePipe)
if tty {
parent.Wait()
}
}
如果为-ti
, 有tty终端就需要parent.Wait()等待子进程的退出,而detach创建的容器不需要等待,子进程容器由init进程管理
查看运行的容器
首先要有容器名,增加启动的flag
cli.StringFlag{
Name: "name",
Usage: "container name",
},
将容器名称和Pid等信息记录,容器记录
type ContainerInfo struct {
Pid string `json:"pid"` //容器的init进程在宿主机上的 PID
Id string `json:"id"` //容器Id
Name string `json:"name"` //容器名
Command string `json:"command"` //容器内init运行命令
CreatedTime string `json:"createTime"` //创建时间
Status string `json:"status"` //容器的状态
}
一个生成Id的方法
func randStringBytes(n int) string {
letterBytes := "1234567890"
rand.Seed(time.Now().UnixNano())
b := make([]byte, n)
for i := range b {
b[i] = letterBytes[rand.Intn(len(letterBytes))]
}
return string(b)
}
然后将容器相关信息写入文件
func recordContainerInfo(containerPID int, commandArray []string, containerName string) (string, error) {
// 生成随机Id
id := randStringBytes(10)
createTime := time.Now().Format("2006-01-02 15:04:05")
command := strings.Join(commandArray, "")
if containerName == "" {
containerName = id
}
containerInfo := &container.ContainerInfo{
Id: id,
Pid: strconv.Itoa(containerPID),
Command: command,
CreatedTime: createTime,
Status: container.RUNNING,
Name: containerName,
}
jsonBytes, err := json.Marshal(containerInfo)
if err != nil {
log.Errorf("Record container info error %v", err)
return "", err
}
jsonStr := string(jsonBytes)
// 配置信息路径
dirUrl := fmt.Sprintf(container.DefaultInfoLocation, containerName)
if err := os.MkdirAll(dirUrl, 0622); err != nil {
log.Errorf("Mkdir error %s error %v", dirUrl, err)
return "", err
}
fileName := dirUrl + "/" + container.ConfigName
file, err := os.Create(fileName)
defer file.Close()
if err != nil {
log.Errorf("Create file %s error %v", fileName, err)
return "", err
}
if _, err := file.WriteString(jsonStr); err != nil {
log.Errorf("File write string error %v", err)
return "", err
}
return containerName, nil
}
在Run方法中启动容器的之后执行这个方法
func Run(tty bool, comArray []string, res *subsystems.ResourceConfig, containerName string) {
...
if err := parent.Start(); err != nil {
log.Error(err)
}
//record container info
containerName, err := recordContainerInfo(parent.Process.Pid, comArray, containerName)
...
如果是使用了tty,在退出的时候需要将这些写入的数据文件删除
然后才是提供查看运行的容器的ps命令
var listCommand = cli.Command{
Name: "ps",
Usage: "list all the containers",
Action: func(context *cli.Context) error {
ListContainers()
return nil
},
}
ListContainers方法将存储信息的文件路径下的文件进行解析,并进行格式化输出
func ListContainers() {
dirURL := fmt.Sprintf(container.DefaultInfoLocation, "")
dirURL = dirURL[:len(dirURL)-1]
// 获取存储路径下的文件
files, err := ioutil.ReadDir(dirURL)
if err != nil {
log.Errorf("Read dir %s error %v", dirURL, err)
return
}
var containers []*container.ContainerInfo
for _, file := range files {
tmpContainer, err := getContainerInfo(file)
if err != nil {
log.Errorf("Get container info error %v", err)
continue
}
containers = append(containers, tmpContainer)
}
w := tabwriter.NewWriter(os.Stdout, 12, 1, 3, ' ', 0)
fmt.Fprint(w, "ID\tNAME\tPID\tSTATUS\tCOMMAND\tCREATED\n")
for _, item := range containers {
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n",
item.Id,
item.Name,
item.Pid,
item.Status,
item.Command,
item.CreatedTime)
}
if err := w.Flush(); err != nil {
log.Errorf("Flush error %v", err)
return
}
}
查看容器日志
实现的是docker logs命令,原理是将进程的标准输出挂载到日志文件中进行读取
将标准输出指向到log日志
func NewParentProcess(tty bool, containerName string) (*exec.Cmd, *os.File) {
readPipe, writePipe, err := NewPipe()
if err != nil {
log.Errorf("New pipe error %v", err)
return nil, nil
}
cmd := exec.Command("/proc/self/exe", "init")
cmd.SysProcAttr = &syscall.SysProcAttr{
Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS |
syscall.CLONE_NEWNET | syscall.CLONE_NEWIPC,
}
if tty {
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
} else {
// 创建日志目录
dirURL := fmt.Sprintf(DefaultInfoLocation, containerName)
if err := os.MkdirAll(dirURL, 0622); err != nil {
log.Errorf("NewParentProcess mkdir %s error %v", dirURL, err)
return nil, nil
}
stdLogFilePath := dirURL + ContainerLogFile
stdLogFile, err := os.Create(stdLogFilePath)
if err != nil {
log.Errorf("NewParentProcess create file %s error %v", stdLogFilePath, err)
return nil, nil
}
cmd.Stdout = stdLogFile
}
cmd.ExtraFiles = []*os.File{readPipe}
cmd.Dir = "/root/busybox"
return cmd, writePipe
}
然后是读取日志的log命令
var logCommand = cli.Command{
Name: "logs",
Usage: "print logs of a container",
Action: func(context *cli.Context) error {
if len(context.Args()) < 1 {
return fmt.Errorf("Please input your container name")
}
containerName := context.Args().Get(0)
logContainer(containerName)
return nil
},
}
对应的方法对日志文件读取并进行输出
func logContainer(containerName string) {
dirURL := fmt.Sprintf(container.DefaultInfoLocation, containerName)
logFileLocation := dirURL + container.ContainerLogFile
file, err := os.Open(logFileLocation)
defer file.Close()
if err != nil {
log.Errorf("Log container open file %s error %v", logFileLocation, err)
return
}
content, err := ioutil.ReadAll(file)
if err != nil {
log.Errorf("Log container read file %s error %v", logFileLocation, err)
return
}
fmt.Fprint(os.Stdout, string(content))
}
进入容器的namespaces
进入容器namespace,就是exec对应的功能了。
setns系统调用,可以根据pid进入指定的namespace,原理是打开/proc/[pid]/ns/目录下的文件。但是对于mount namespace,一个多线程的进程是不能使用setns调用进入命名空间的。对于go来说每启动一个程序都会进入多线程状态,所以无法直接使用系统调用进入mount namespace,需要借助c来实现
Cgo允许go程序调用c的函数和标准库,示例
package rand
/*
#include <stdlib.h>
*/
import "C"
func Random() int {
return int(C.random())
}
func Seed(i int) {
C.srandom(C.uint(i))
}
这里Go的标准库是并没有这个包的,因为本来就不是一个包,而是Cgo创建的一个特殊命名空间,用来与C的命名空间交互,分别调用了C中的random和uint函数
这里调用setns
package nsenter
/*
#include <errno.h>
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
// __attribute__类似构造函数((constructor)),一旦被引用就会自动执行
__attribute__((constructor)) void enter_namespace(void) {
char *mydocker_pid;
mydocker_pid = getenv("mydocker_pid");
// 从环境变量中获取需要进入的PID
if (mydocker_pid) {
//fprintf(stdout, "got mydocker_pid=%s\n", mydocker_pid);
} else {
//fprintf(stdout, "missing mydocker_pid env skip nsenter");
return;
}
char *mydocker_cmd;
// 从环境变量中获取需要执行的命令
mydocker_cmd = getenv("mydocker_cmd");
if (mydocker_cmd) {
//fprintf(stdout, "got mydocker_cmd=%s\n", mydocker_cmd);
} else {
//fprintf(stdout, "missing mydocker_cmd env skip nsenter");
return;
}
int i;
char nspath[1024];
char *namespaces[] = { "ipc", "uts", "net", "pid", "mnt" };
for (i=0; i<5; i++) {
sprintf(nspath, "/proc/%s/ns/%s", mydocker_pid, namespaces[i]);
int fd = open(nspath, O_RDONLY);
if (setns(fd, 0) == -1) {
//fprintf(stderr, "setns on %s namespace failed: %s\n", namespaces[i], strerror(errno));
} else {
//fprintf(stdout, "setns on %s namespace succeeded\n", namespaces[i]);
}
close(fd);
}
// 在进入的ns中执行凝练
int res = system(mydocker_cmd);
exit(0);
return;
}
*/
import "C"
对于exec命令,此时调用ExecContainer会执行一个cgo的程序,但是因为没有环境变量就退出了
var execCommand = cli.Command{
Name: "exec",
Usage: "exec a command into container",
Action: func(context *cli.Context) error {
//This is for callback
if os.Getenv(ENV_EXEC_PID) != "" {
log.Infof("pid callback pid %s", os.Getgid())
return nil
}
if len(context.Args()) < 2 {
return fmt.Errorf("Missing container name or command")
}
containerName := context.Args().Get(0)
var commandArray []string
for _, arg := range context.Args().Tail() {
commandArray = append(commandArray, arg)
}
ExecContainer(containerName, commandArray)
return nil
},
}
调用ExecContainer写入环境变量,再通过/proc/self/exec再执行cgo程序就成正常执行
func ExecContainer(containerName string, comArray []string) {
pid, err := getContainerPidByName(containerName)
if err != nil {
log.Errorf("Exec container getContainerPidByName %s error %v", containerName, err)
return
}
cmdStr := strings.Join(comArray, " ")
log.Infof("container pid %s", pid)
log.Infof("command %s", cmdStr)
cmd := exec.Command("/proc/self/exe", "exec")
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
os.Setenv(ENV_EXEC_PID, pid)
os.Setenv(ENV_EXEC_CMD, cmdStr)
if err := cmd.Run(); err != nil {
log.Errorf("Exec container %s error %v", containerName, err)
}
}
func getContainerPidByName(containerName string) (string, error) {
dirURL := fmt.Sprintf(container.DefaultInfoLocation, containerName)
configFilePath := dirURL + container.ConfigName
contentBytes, err := ioutil.ReadFile(configFilePath)
if err != nil {
return "", err
}
var containerInfo container.ContainerInfo
if err := json.Unmarshal(contentBytes, &containerInfo); err != nil {
return "", err
}
return containerInfo.Pid, nil
}
容器停止
容器停止只需要找到容器对应的主进程pid,发送SIGTERM信号就行了
var stopCommand = cli.Command{
Name: "stop",
Usage: "stop a container",
Action: func(context *cli.Context) error {
if len(context.Args()) < 1 {
return fmt.Errorf("Missing container name")
}
containerName := context.Args().Get(0)
stopContainer(containerName)
return nil
},
}
对应的方法
func stopContainer(containerName string) {
pid, err := GetContainerPidByName(containerName)
if err != nil {
log.Errorf("Get contaienr pid by name %s error %v", containerName, err)
return
}
pidInt, err := strconv.Atoi(pid)
if err != nil {
log.Errorf("Conver pid from string to int error %v", err)
return
}
if err := syscall.Kill(pidInt, syscall.SIGTERM); err != nil {
log.Errorf("Stop container %s error %v", containerName, err)
return
}
containerInfo, err := getContainerInfoByName(containerName)
if err != nil {
log.Errorf("Get container %s info error %v", containerName, err)
return
}
containerInfo.Status = container.STOP
containerInfo.Pid = " "
newContentBytes, err := json.Marshal(containerInfo)
if err != nil {
log.Errorf("Json marshal %s error %v", containerName, err)
return
}
dirURL := fmt.Sprintf(container.DefaultInfoLocation, containerName)
configFilePath := dirURL + container.ConfigName
if err := ioutil.WriteFile(configFilePath, newContentBytes, 0622); err != nil {
log.Errorf("Write file %s error", configFilePath, err)
}
}
容器删除
删除的信息包括存储的信息和容器层数据
var removeCommand = cli.Command{
Name: "rm",
Usage: "remove unused containers",
Action: func(context *cli.Context) error {
if len(context.Args()) < 1 {
return fmt.Errorf("Missing container name")
}
containerName := context.Args().Get(0)
removeContainer(containerName)
return nil
},
}
对应的操作
func removeContainer(containerName string) {
containerInfo, err := getContainerInfoByName(containerName)
if err != nil {
log.Errorf("Get container %s info error %v", containerName, err)
return
}
if containerInfo.Status != container.STOP {
log.Errorf("Couldn't remove running container")
return
}
dirURL := fmt.Sprintf(container.DefaultInfoLocation, containerName)
if err := os.RemoveAll(dirURL); err != nil {
log.Errorf("Remove file %s error %v", dirURL, err)
return
}
}
通过容器制作镜像
我们需要将当前容器的数据进行打包
var commitCommand = cli.Command{
Name: "commit",
Usage: "commit a container into image",
Action: func(context *cli.Context) error {
if len(context.Args()) < 2 {
return fmt.Errorf("Missing container name and image name")
}
containerName := context.Args().Get(0)
imageName := context.Args().Get(1)
commitContainer(containerName, imageName)
return nil
},
}
对应的操作
func commitContainer(containerName, imageName string){
mntURL := fmt.Sprintf(container.MntUrl, containerName)
mntURL += "/"
imageTar := container.RootUrl + "/" + imageName + ".tar"
if _, err := exec.Command("tar", "-czf", imageTar, "-C", mntURL, ".").CombinedOutput(); err != nil {
log.Errorf("Tar folder %s error %v", mntURL, err)
}
}
但是启动的时候需要为每一个容器配置不同的image和mnt namespace
获取镜像名
//get image name
imageName := cmdArray[0]
cmdArray = cmdArray[1:]
启动的时候也指定镜像
parent, writePipe := container.NewParentProcess(tty, containerName, volume, imageName, envSlice)
每个容器的aufs挂载的目录也会不同了,读写层目录也是和原来一样区分开
MntUrl string = "/root/mnt/%s"
WriteLayerUrl string = "/root/writeLayer/%s"
获取镜像层的方式就是到指定的镜像目录获取对应的镜像tar包存储路径,然后解压到指定目录
在启动的时候进行aufs挂载的方式
如果希望镜像是分层的,就需要单独在目录存储每层的读写层数据,然后一个这些读写层的数据
容器指定环境变量运行
环境变量参考docker的-e参数,然后在运行子进程的时候传入
func NewParentProcess(tty bool, containerName, volume, imageName string, envSlice []string) (*exec.Cmd, *os.File) {
...
cmd.Env = append(os.Environ(), envSlice...)
...
}
但是通过exec进入容器是获取不到环境变量的,因为exec是一个单独的进程,通过setns进入到容器命名空间的,所以需要根据容器名获取容器PID,获取到PID下的环境变量
func getEnvsByPid(pid string) []string {
path := fmt.Sprintf("/proc/%s/environ", pid)
contentBytes, err := ioutil.ReadFile(path)
if err != nil {
log.Errorf("Read file %s error %v", path, err)
return nil
}
//env split by \u0000
//多个环境变量的分隔符为"\u0000"
envs := strings.Split(string(contentBytes), "\u0000")
return envs
}
将宿主机的环境变量和容器的环境变量组合作为exec进入容器的环境变量
containerEnvs := getEnvsByPid(pid)
cmd.Env = append(os.Environ(), containerEnvs...)