├── .DS_Store ├── .editorconfig ├── .gitattributes ├── .gitignore ├── .golangci.yaml ├── LICENSE ├── Makefile ├── README.md ├── cgroups ├── cgroup.go ├── cgroup_manager_v1.go ├── cgroup_manager_v2.go ├── fs │ ├── cpu.go │ ├── cpuset.go │ ├── memory.go │ ├── memory_test.go │ ├── subsystem.go │ ├── utils.go │ └── utils_test.go ├── fs2 │ ├── cpu.go │ ├── cpuset.go │ ├── defaultpath.go │ ├── memory.go │ ├── subsystems.go │ └── utils.go ├── resource │ ├── resource.go │ └── subsystem.go └── util.go ├── commit.go ├── constant └── mode.go ├── container ├── container_info.go ├── container_process.go ├── init.go ├── rootfs.go └── volume.go ├── example ├── cgo │ └── main.go └── main.go ├── exec.go ├── go.mod ├── list.go ├── logs.go ├── main.go ├── main_command.go ├── network ├── bridge_driver.go ├── bridge_driver_test.go ├── ipam.go ├── ipam_test.go ├── model.go └── network.go ├── nsenter └── nsenter.go ├── run.go ├── stop.go └── utils ├── file.go ├── rootfs.go └── volume.go /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lixd/mydocker/61a7db6fc67993f9ec1f6743861bef6900b391fc/.DS_Store -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | ; https://editorconfig.org/ 2 | 3 | root = true 4 | 5 | [*] 6 | insert_final_newline = true 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | indent_style = space 10 | indent_size = 2 11 | 12 | [{Makefile,go.mod,go.sum,*.go,.gitmodules}] 13 | indent_style = tab 14 | indent_size = 4 15 | 16 | [*.md] 17 | indent_size = 4 18 | trim_trailing_whitespace = false 19 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # We'll let Git's auto-detection algorithm infer if a file is text. If it is, 2 | # enforce LF line endings regardless of OS or git configurations. 3 | * text=auto eol=lf 4 | 5 | # Isolate binary files in case the auto-detection algorithm fails and 6 | # marks them as text files (which could brick them). 7 | *.{png,jpg,jpeg,gif,webp,woff,woff2} binary -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ip2region.db 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | *.log 14 | .idea 15 | go.sum 16 | /wbl/ 17 | # ip2geo 数据库 18 | *.mmdb 19 | # ip2region 数据库 20 | *.db 21 | dist 22 | -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | linters-settings: 2 | misspell: 3 | locale: US 4 | gofmt: 5 | # simplify code: gofmt with `-s` option, true by default 6 | simplify: true 7 | unused: 8 | # Select the Go version to target. The default is '1.13'. 9 | go: "1.17" 10 | revive: 11 | # see https://github.com/mgechev/revive#available-rules for details. 12 | ignore-generated-header: true 13 | severity: warning 14 | rules: 15 | - name: indent-error-flow 16 | severity: warning 17 | - name: add-constant 18 | severity: warning 19 | arguments: 20 | - maxLitCount: "3" 21 | allowStrs: '""' 22 | allowInts: "0,1,2" 23 | allowFloats: "0.0,0.,1.0,1.,2.0,2." 24 | # all linters see: https://golangci-lint.run/usage/linters/ 25 | linters: 26 | disable-all: true 27 | enable: 28 | - revive 29 | - goimports 30 | - misspell 31 | - gofmt 32 | - unused 33 | - typecheck 34 | - govet 35 | - ineffassign 36 | - gosimple 37 | - deadcode 38 | - structcheck 39 | - errcheck 40 | service: 41 | golangci-lint-version: 1.43.0 # use the fixed version to not introduce new linters unexpectedly 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 意琦行 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Makefile 基础教程 2 | # https://www.kancloud.cn/kancloud/make-command/45596 3 | # https://seisman.github.io/how-to-write-makefile/overview.html 4 | 5 | .PHONY:build 6 | build: 7 | go build -v . 8 | 9 | .PHONY:run 10 | run: 11 | go run main.go 12 | 13 | .PHONY:test 14 | test: 15 | go test ./... 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mydocker 2 | 3 | ## 1. 概述 4 | 5 | 参考[《自己动手写 docker》](https://github.com/xianlubird/mydocker),自己动手从零开始实现一个简易的 docker 以及配套教程。 6 | > 再次感谢几位作者大佬 7 | 8 | 具体差异如下: 9 | 10 | * UnionFS替换:从AUFS 替换为 Overlayfs 11 | * 依赖管理更新:从 go vendor 替换为 Go Module 12 | * 一些写法上的优化调整 13 | 14 | 15 | ### 微信公众号:探索云原生 16 | 17 | > 鸽了很久之后,终于开通了,欢迎关注。 18 | 19 | 一个云原生打工人的探索之路,专注云原生,Go,坚持分享最佳实践、经验干货。 20 | 21 | `从零开始写 Docker` 系列持续更新中,扫描下面二维码,关注我即时获取更新~ 22 | 23 | ![](https://img.lixueduan.com/about/wechat/qrcode_search.png) 24 | 25 | 26 | 27 | ### 个人博客:指月小筑(探索云原生) 28 | 在线阅读:[指月小筑(探索云原生)](https://www.lixueduan.com/categories/docker/) 29 | 30 | 31 | ## 2. 基础知识 32 | 33 | 推荐阅读以下文章对 Docker 核心原理有一个大致认识: 34 | * **核心原理**:[深入理解 Docker 核心原理:Namespace、Cgroups 和 Rootfs](https://www.lixueduan.com/posts/docker/03-container-core/) 35 | * **基于 namespace 的视图隔离**:[探索 Linux Namespace:Docker 隔离的神奇背后](https://www.lixueduan.com/posts/docker/05-namespace/) 36 | * **基于 cgroups 的资源限制** 37 | * [初探 Linux Cgroups:资源控制的奇妙世界](https://www.lixueduan.com/posts/docker/06-cgroups-1/) 38 | * [深入剖析 Linux Cgroups 子系统:资源精细管理](https://www.lixueduan.com/posts/docker/07-cgroups-2/) 39 | * [Docker 与 Linux Cgroups:资源隔离的魔法之旅](https://www.lixueduan.com/posts/docker/08-cgroups-3/) 40 | * **基于 overlayfs 的文件系统**:[Docker 魔法解密:探索 UnionFS 与 OverlayFS](https://www.lixueduan.com/posts/docker/09-ufs-overlayfs/) 41 | * **基于 veth pair、bridge、iptables 等等技术的 Docker 网络**:[揭秘 Docker 网络:手动实现 Docker 桥接网络](https://www.lixueduan.com/posts/docker/10-bridge-network/) 42 | 43 | 通过上述文章,大家对 Docker 的实现原理已经有了初步的认知,接下来我们就用 Golang 手动实现一下自己的 docker(mydocker)。 44 | 45 | 46 | ## 3. 具体实现 47 | 48 | ### 构造容器 49 | 50 | 本章构造了一个简单的容器,具有基本的 Namespace 隔离,确定了基本的开发架构,后续在此基础上继续完善即可。 51 | 52 | 第一篇: 53 | * [从零开始写 Docker:实现 run 命令](https://www.lixueduan.com/posts/docker/mydocker/01-mydocker-run/) 54 | * 代码分支 [feat-run](https://github.com/lixd/mydocker/tree/feat-run) 55 | 56 | 第二篇: 57 | * [从零开始写 Docker(二)---优化:使用匿名管道传参](https://www.lixueduan.com/posts/docker/mydocker/02-passing-param-by-pipe/) 58 | * 代码分支 [opt-passing-param-by-pipe](https://github.com/lixd/mydocker/tree/opt-passing-param-by-pipe) 59 | 60 | 第三篇: 61 | * [从零开始写 Docker(三)---基于 cgroups 实现资源限制](https://www.lixueduan.com/posts/docker/mydocker/03-resource-limit-by-cgroups/) 62 | * 代码分支 [feat-cgroup](https://github.com/lixd/mydocker/tree/feat-cgroup) 63 | 64 | 65 | 66 | 67 | 68 | ### 构造镜像 69 | 70 | 本章首先使用 busybox 作为基础镜像创建了一个容器,理解了什么是 rootfs,以及如何使用 rootfs 来打造容器的基本运行环境。 71 | 72 | 然后,使用 OverlayFS 来构建了一个拥有二层模式的镜像,对于最上层可写层的修改不会影响到基础层。这里就基本解释了镜像分层存储的原理。 73 | 74 | 之后使用 -v 参数做了一个 volume 挂载的例子,介绍了如何将容器外部的文件系统挂载到容器中,并且让它可以访问。 75 | 76 | 最后实现了一个简单版本的容器镜像打包。 77 | 78 | 这一章主要针对镜像的存储及文件系统做了基本的原理性介绍,通过这几个例子,可以很好地理解镜像是如何构建的,第 5 章会基于这些基础做更多的扩展。 79 | 80 | 第四篇: 81 | 82 | * [从零开始写 Docker(四)---使用 pivotRoot 切换 rootfs 实现文件系统隔离](https://www.lixueduan.com/posts/docker/mydocker/04-change-rootfs-by-pivot-root/) 83 | * 代码分支 [feat-rootfs](https://github.com/lixd/mydocker/tree/feat-rootfs) 84 | 85 | 第五篇: 86 | 87 | * [从零开始写 Docker(五)---基于 overlayfs 实现写操作隔离](https://www.lixueduan.com/posts/docker/mydocker/05-isolate-operate-by-overlayfs/) 88 | * 代码分支 [feat-overlayfs](https://github.com/lixd/mydocker/tree/feat-overlayfs) 89 | 90 | 第六篇: 91 | 92 | * [从零开始写 Docker(六)---实现 mydocker run -v 支持数据卷挂载](https://www.lixueduan.com/posts/docker/mydocker/06-volume-by-bind-mount/) 93 | * 代码分支 [feat-volume](https://github.com/lixd/mydocker/tree/feat-volume) 94 | 95 | 第七篇: 96 | 97 | * [从零开始写 Docker(七)---实现 mydocker commit 打包容器成镜像](https://www.lixueduan.com/posts/docker/mydocker/07-mydocker-commit/) 98 | * 代码分支 [feat-commit](https://github.com/lixd/mydocker/tree/feat-commit) 99 | 100 | 101 | ### 构建容器进阶 102 | 103 | 本章实现了容器操作的基本功能。 104 | 105 | * 首先实现了容器的后台运行,然后将容器的状态在文件系统上做了存储。 106 | * 通过这些存储信息,又可以实现列出当前容器信息的功能。 107 | * 并且, 基于后台运行的容器,我们可以去手动停止容器,并清除掉容器的存储信息。 108 | * 最后修改了上一章镜像的存储结构,使得多个容器可以并存,且存储的内容互不干扰。 109 | 110 | 第八篇: 111 | 112 | * [从零开始写 Docker(八)---实现 mydocker run -d 支持后台运行容器](https://www.lixueduan.com/posts/docker/mydocker/08-mydocker-run-d/) 113 | * 代码分支 [feat-run-d](https://github.com/lixd/mydocker/tree/feat-run-d) 114 | 115 | 第九篇: 116 | 117 | * [从零开始写 Docker(九)---实现 mydocker ps 查看运行中的容器](https://www.lixueduan.com/posts/docker/mydocker/09-mydocker-ps/) 118 | * 代码分支 [feat-ps](https://github.com/lixd/mydocker/tree/feat-ps) 119 | 120 | 121 | 第十篇: 122 | 123 | * [从零开始写 Docker(十)---实现 mydocker logs 查看容器日志](https://www.lixueduan.com/posts/docker/mydocker/10-mydocker-logs/) 124 | * 代码分支 [feat-logs](https://github.com/lixd/mydocker/tree/feat-logs) 125 | 126 | 127 | 128 | 第十一篇: 129 | 130 | * [从零开始写 Docker(十一)---实现 mydocker exec 进入容器内部](https://www.lixueduan.com/posts/docker/mydocker/11-mydocker-exec/) 131 | * 代码分支 [feat-exec](https://github.com/lixd/mydocker/tree/feat-exec) 132 | 133 | 134 | 135 | 第十二篇: 136 | 137 | * [从零开始写 Docker(十二)---实现 mydocker stop 停止容器](https://www.lixueduan.com/posts/docker/mydocker/12-mydocker-stop/) 138 | * 代码分支 [feat-stop](https://github.com/lixd/mydocker/tree/feat-stop) 139 | 140 | 141 | 142 | 第十三篇: 143 | 144 | * [从零开始写 Docker(十三)---实现 mydocker rm 删除容器](https://www.lixueduan.com/posts/docker/mydocker/13-mydocker-rm/) 145 | * 代码分支 [feat-rm](https://github.com/lixd/mydocker/tree/feat-rm) 146 | 147 | 148 | 149 | 第十四篇: 150 | 151 | * [从零开始写 Docker(十四)---重构:实现容器间 rootfs 隔离](https://www.lixueduan.com/posts/docker/mydocker/14-isolation-rootfs-between-containers/) 152 | * 代码分支 [refactor-isolate-rootfs](https://github.com/lixd/mydocker/tree/refactor-isolate-rootfs) 153 | > refactor: 文件系统重构,为不同容器提供独立的rootfs. feat: 更新rm命令,删除容器时移除对应文件系统. feat: 更新commit命令,实现对不同容器打包. 154 | 155 | 156 | 157 | 第十五篇: 158 | 159 | * [从零开始写 Docker(十五)---实现 mydocker run -e 支持环境变量传递](https://www.lixueduan.com/posts/docker/mydocker/15-mydocker-run-e/) 160 | * 代码分支 [feat-run-e](https://github.com/lixd/mydocker/tree/feat-run-e) 161 | 162 | 163 | ### 容器网络 164 | 165 | 在这一章中,首先手动给一个容器配置了网路,并通过这个过程了解了 Linux 虚拟网络设备和操作。然后构建了容器网络的概念模型和模块调用关系、IP 地址分配方案,以及网络模块的接口设计和实现,并且通过实现 Bridge 166 | 驱动给容器连上了“网线”。 167 | 168 | 前置:[揭秘 Docker 网络:手动实现 Docker 桥接网络](https://www.lixueduan.com/posts/docker/10-bridge-network/) 169 | 170 | * 第十六篇: 171 | 172 | * [从零开始写 Docker(十六)---容器网络实现(上):为容器插上”网线“](https://www.lixueduan.com/posts/docker/mydocker/16-network-1/) 173 | * 代码分支 [feat-network-1](https://github.com/lixd/mydocker/tree/feat-network1) 174 | 175 | 第十七篇: 176 | 177 | * [从零开始写 Docker(十七)---容器网络实现(中):为容器插上”网线“](https://www.lixueduan.com/posts/docker/mydocker/16-network-2/) 178 | * 代码分支 [feat-network-2](https://github.com/lixd/mydocker/tree/feat-network2) 179 | 180 | 181 | 第十八篇: 182 | 183 | * [从零开始写 Docker(十八)---容器网络实现(下):为容器插上”网线“](https://www.lixueduan.com/posts/docker/mydocker/16-network-3/) 184 | * 代码分支 [feat-network-3](https://github.com/lixd/mydocker/tree/feat-network3) 185 | 186 | 187 | 第十九篇: 188 | 189 | * [从零开始写 Docker(十九)---增加 cgroup v2 支持](https://www.lixueduan.com/posts/docker/mydocker/19-cgroup-v2/) 190 | * 代码分支 [feat-cgroup-v2](https://github.com/lixd/mydocker/tree/feat-cgroup-v2) 191 | 192 | 193 | --- 194 | 最后打个广告,扫描下面二维码,关注我即时获取更多文章~ 195 | 196 | ![](https://img.lixueduan.com/about/wechat/qrcode_search.png) 197 | -------------------------------------------------------------------------------- /cgroups/cgroup.go: -------------------------------------------------------------------------------- 1 | package cgroups 2 | 3 | import ( 4 | log "github.com/sirupsen/logrus" 5 | "mydocker/cgroups/resource" 6 | ) 7 | 8 | type CgroupManager interface { 9 | Apply(pid int) error 10 | Set(res *resource.ResourceConfig) error 11 | Destroy() error 12 | } 13 | 14 | func NewCgroupManager(path string) CgroupManager { 15 | if IsCgroup2UnifiedMode() { 16 | log.Infof("use cgroup v2") 17 | return NewCgroupManagerV2(path) 18 | } 19 | log.Infof("use cgroup v1") 20 | return NewCgroupManagerV1(path) 21 | } 22 | -------------------------------------------------------------------------------- /cgroups/cgroup_manager_v1.go: -------------------------------------------------------------------------------- 1 | package cgroups 2 | 3 | import ( 4 | "github.com/sirupsen/logrus" 5 | "mydocker/cgroups/fs" 6 | "mydocker/cgroups/resource" 7 | ) 8 | 9 | type CgroupManagerV1 struct { 10 | // cgroup在hierarchy中的路径 相当于创建的cgroup目录相对于root cgroup目录的路径 11 | Path string 12 | // 资源配置 13 | Resource *resource.ResourceConfig 14 | Subsystems []resource.Subsystem 15 | } 16 | 17 | func NewCgroupManagerV1(path string) *CgroupManagerV1 { 18 | return &CgroupManagerV1{ 19 | Path: path, 20 | Subsystems: fs.SubsystemsIns, 21 | } 22 | } 23 | 24 | // Apply 将进程pid加入到这个cgroup中 25 | func (c *CgroupManagerV1) Apply(pid int) error { 26 | for _, subSysIns := range c.Subsystems { 27 | err := subSysIns.Apply(c.Path, pid) 28 | if err != nil { 29 | logrus.Errorf("apply subsystem:%s err:%s", subSysIns.Name(), err) 30 | } 31 | } 32 | return nil 33 | } 34 | 35 | // Set 设置cgroup资源限制 36 | func (c *CgroupManagerV1) Set(res *resource.ResourceConfig) error { 37 | for _, subSysIns := range c.Subsystems { 38 | err := subSysIns.Set(c.Path, res) 39 | if err != nil { 40 | logrus.Errorf("apply subsystem:%s err:%s", subSysIns.Name(), err) 41 | } 42 | } 43 | return nil 44 | } 45 | 46 | // Destroy 释放cgroup 47 | func (c *CgroupManagerV1) Destroy() error { 48 | for _, subSysIns := range c.Subsystems { 49 | if err := subSysIns.Remove(c.Path); err != nil { 50 | logrus.Warnf("remove cgroup fail %v", err) 51 | } 52 | } 53 | return nil 54 | } 55 | -------------------------------------------------------------------------------- /cgroups/cgroup_manager_v2.go: -------------------------------------------------------------------------------- 1 | package cgroups 2 | 3 | import ( 4 | "github.com/sirupsen/logrus" 5 | "mydocker/cgroups/fs2" 6 | "mydocker/cgroups/resource" 7 | ) 8 | 9 | type CgroupManagerV2 struct { 10 | Path string 11 | Resource *resource.ResourceConfig 12 | Subsystems []resource.Subsystem 13 | } 14 | 15 | func NewCgroupManagerV2(path string) *CgroupManagerV2 { 16 | return &CgroupManagerV2{ 17 | Path: path, 18 | Subsystems: fs2.Subsystems, 19 | } 20 | } 21 | 22 | // Apply 将进程pid加入到这个cgroup中 23 | func (c *CgroupManagerV2) Apply(pid int) error { 24 | for _, subSysIns := range c.Subsystems { 25 | err := subSysIns.Apply(c.Path, pid) 26 | if err != nil { 27 | logrus.Errorf("apply subsystem:%s err:%s", subSysIns.Name(), err) 28 | } 29 | } 30 | return nil 31 | } 32 | 33 | // Set 设置cgroup资源限制 34 | func (c *CgroupManagerV2) Set(res *resource.ResourceConfig) error { 35 | for _, subSysIns := range c.Subsystems { 36 | err := subSysIns.Set(c.Path, res) 37 | if err != nil { 38 | logrus.Errorf("apply subsystem:%s err:%s", subSysIns.Name(), err) 39 | } 40 | } 41 | return nil 42 | } 43 | 44 | // Destroy 释放cgroup 45 | func (c *CgroupManagerV2) Destroy() error { 46 | for _, subSysIns := range c.Subsystems { 47 | if err := subSysIns.Remove(c.Path); err != nil { 48 | logrus.Warnf("remove cgroup fail %v", err) 49 | } 50 | } 51 | return nil 52 | } 53 | -------------------------------------------------------------------------------- /cgroups/fs/cpu.go: -------------------------------------------------------------------------------- 1 | package fs 2 | 3 | import ( 4 | "fmt" 5 | "mydocker/cgroups/resource" 6 | "os" 7 | "path" 8 | "strconv" 9 | 10 | "mydocker/constant" 11 | ) 12 | 13 | type CpuSubSystem struct { 14 | } 15 | 16 | const ( 17 | PeriodDefault = 100000 18 | Percent = 100 19 | ) 20 | 21 | func (s *CpuSubSystem) Name() string { 22 | return "cpu" 23 | } 24 | 25 | func (s *CpuSubSystem) Set(cgroupPath string, res *resource.ResourceConfig) error { 26 | if res.CpuCfsQuota == 0 { 27 | return nil 28 | } 29 | subsysCgroupPath, err := getCgroupPath(s.Name(), cgroupPath, true) 30 | if err != nil { 31 | return err 32 | } 33 | // cpu.cfs_period_us & cpu.cfs_quota_us 控制的是CPU使用时间,单位是微秒,比如每1秒钟,这个进程只能使用200ms,相当于只能用20%的CPU 34 | if res.CpuCfsQuota != 0 { 35 | // cpu.cfs_period_us 默认为100000,即100ms 36 | if err = os.WriteFile(path.Join(subsysCgroupPath, "cpu.cfs_period_us"), []byte(strconv.Itoa(PeriodDefault)), constant.Perm0644); err != nil { 37 | return fmt.Errorf("set cgroup cpu share fail %v", err) 38 | } 39 | // cpu.cfs_quota_us 则根据用户传递的参数来控制,比如参数为20,就是限制为20%CPU,所以把cpu.cfs_quota_us设置为cpu.cfs_period_us的20%就行 40 | // 这里只是简单的计算了下,并没有处理一些特殊情况,比如负数什么的 41 | if err = os.WriteFile(path.Join(subsysCgroupPath, "cpu.cfs_quota_us"), []byte(strconv.Itoa(PeriodDefault/Percent*res.CpuCfsQuota)), constant.Perm0644); err != nil { 42 | return fmt.Errorf("set cgroup cpu share fail %v", err) 43 | } 44 | } 45 | return nil 46 | } 47 | 48 | func (s *CpuSubSystem) Apply(cgroupPath string, pid int) error { 49 | subsysCgroupPath, err := getCgroupPath(s.Name(), cgroupPath, false) 50 | if err != nil { 51 | return fmt.Errorf("get cgroup %s error: %v", cgroupPath, err) 52 | } 53 | if err = os.WriteFile(path.Join(subsysCgroupPath, "tasks"), []byte(strconv.Itoa(pid)), constant.Perm0644); err != nil { 54 | return fmt.Errorf("set cgroup proc fail %v", err) 55 | } 56 | return nil 57 | } 58 | 59 | func (s *CpuSubSystem) Remove(cgroupPath string) error { 60 | subsysCgroupPath, err := getCgroupPath(s.Name(), cgroupPath, false) 61 | if err != nil { 62 | return err 63 | } 64 | return os.RemoveAll(subsysCgroupPath) 65 | } 66 | -------------------------------------------------------------------------------- /cgroups/fs/cpuset.go: -------------------------------------------------------------------------------- 1 | package fs 2 | 3 | import ( 4 | "fmt" 5 | "mydocker/cgroups/resource" 6 | "os" 7 | "path" 8 | "strconv" 9 | 10 | "mydocker/constant" 11 | 12 | "github.com/pkg/errors" 13 | ) 14 | 15 | type CpusetSubSystem struct { 16 | } 17 | 18 | func (s *CpusetSubSystem) Name() string { 19 | return "cpuset" 20 | } 21 | 22 | func (s *CpusetSubSystem) Set(cgroupPath string, res *resource.ResourceConfig) error { 23 | if res.CpuSet == "" { 24 | return nil 25 | } 26 | subsysCgroupPath, err := getCgroupPath(s.Name(), cgroupPath, true) 27 | if err != nil { 28 | return err 29 | } 30 | if err := os.WriteFile(path.Join(subsysCgroupPath, "cpuset.cpus"), []byte(res.CpuSet), constant.Perm0644); err != nil { 31 | return fmt.Errorf("set cgroup cpuset fail %v", err) 32 | } 33 | return nil 34 | } 35 | 36 | func (s *CpusetSubSystem) Apply(cgroupPath string, pid int) error { 37 | subsysCgroupPath, err := getCgroupPath(s.Name(), cgroupPath, true) 38 | if err != nil { 39 | return errors.Wrapf(err, "get cgroup %s", cgroupPath) 40 | 41 | } 42 | if err := os.WriteFile(path.Join(subsysCgroupPath, "tasks"), []byte(strconv.Itoa(pid)), constant.Perm0644); err != nil { 43 | return fmt.Errorf("set cgroup proc fail %v", err) 44 | } 45 | return nil 46 | } 47 | 48 | func (s *CpusetSubSystem) Remove(cgroupPath string) error { 49 | subsysCgroupPath, err := getCgroupPath(s.Name(), cgroupPath, false) 50 | if err != nil { 51 | return err 52 | } 53 | return os.RemoveAll(subsysCgroupPath) 54 | } 55 | -------------------------------------------------------------------------------- /cgroups/fs/memory.go: -------------------------------------------------------------------------------- 1 | package fs 2 | 3 | import ( 4 | "fmt" 5 | "mydocker/cgroups/resource" 6 | "os" 7 | "path" 8 | "strconv" 9 | 10 | "mydocker/constant" 11 | 12 | "github.com/pkg/errors" 13 | ) 14 | 15 | type MemorySubSystem struct { 16 | } 17 | 18 | // Name 返回cgroup名字 19 | func (s *MemorySubSystem) Name() string { 20 | return "memory" 21 | } 22 | 23 | // Set 设置cgroupPath对应的cgroup的内存资源限制 24 | func (s *MemorySubSystem) Set(cgroupPath string, res *resource.ResourceConfig) error { 25 | if res.MemoryLimit == "" { 26 | return nil 27 | } 28 | subsysCgroupPath, err := getCgroupPath(s.Name(), cgroupPath, true) 29 | if err != nil { 30 | return err 31 | } 32 | // 设置这个cgroup的内存限制,即将限制写入到cgroup对应目录的memory.limit_in_bytes 文件中。 33 | if err := os.WriteFile(path.Join(subsysCgroupPath, "memory.limit_in_bytes"), []byte(res.MemoryLimit), constant.Perm0644); err != nil { 34 | return fmt.Errorf("set cgroup memory fail %v", err) 35 | } 36 | return nil 37 | } 38 | 39 | // Apply 将pid加入到cgroupPath对应的cgroup中 40 | func (s *MemorySubSystem) Apply(cgroupPath string, pid int) error { 41 | subsysCgroupPath, err := getCgroupPath(s.Name(), cgroupPath, true) 42 | if err != nil { 43 | return errors.Wrapf(err, "get cgroup %s", cgroupPath) 44 | } 45 | if err := os.WriteFile(path.Join(subsysCgroupPath, "tasks"), []byte(strconv.Itoa(pid)), constant.Perm0644); err != nil { 46 | return fmt.Errorf("set cgroup proc fail %v", err) 47 | } 48 | return nil 49 | } 50 | 51 | // Remove 删除cgroupPath对应的cgroup 52 | func (s *MemorySubSystem) Remove(cgroupPath string) error { 53 | subsysCgroupPath, err := getCgroupPath(s.Name(), cgroupPath, false) 54 | if err != nil { 55 | return err 56 | } 57 | return os.RemoveAll(subsysCgroupPath) 58 | } 59 | -------------------------------------------------------------------------------- /cgroups/fs/memory_test.go: -------------------------------------------------------------------------------- 1 | package fs 2 | 3 | import ( 4 | "os" 5 | "path" 6 | "testing" 7 | ) 8 | 9 | func TestMemoryCgroup(t *testing.T) { 10 | memSubSys := MemorySubSystem{} 11 | resConfig := &ResourceConfig{ 12 | MemoryLimit: "1000m", 13 | } 14 | testCgroup := "testmemlimit" 15 | 16 | if err := memSubSys.Set(testCgroup, resConfig); err != nil { 17 | t.Fatalf("cgroup fail %v", err) 18 | } 19 | stat, _ := os.Stat(path.Join(findCgroupMountpoint("memory"), testCgroup)) 20 | t.Logf("cgroup stats: %+v", stat) 21 | 22 | if err := memSubSys.Apply(testCgroup, os.Getpid(), resConfig); err != nil { 23 | t.Fatalf("cgroup Apply %v", err) 24 | } 25 | // 将进程移回到根Cgroup节点 26 | if err := memSubSys.Apply("", os.Getpid(), resConfig); err != nil { 27 | t.Fatalf("cgroup Apply %v", err) 28 | } 29 | 30 | if err := memSubSys.Remove(testCgroup); err != nil { 31 | t.Fatalf("cgroup remove %v", err) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /cgroups/fs/subsystem.go: -------------------------------------------------------------------------------- 1 | package fs 2 | 3 | import "mydocker/cgroups/resource" 4 | 5 | // SubsystemsIns 通过不同的subsystem初始化实例创建资源限制处理链数组 6 | var SubsystemsIns = []resource.Subsystem{ 7 | &CpusetSubSystem{}, 8 | &MemorySubSystem{}, 9 | &CpuSubSystem{}, 10 | } 11 | -------------------------------------------------------------------------------- /cgroups/fs/utils.go: -------------------------------------------------------------------------------- 1 | package fs 2 | 3 | import ( 4 | "bufio" 5 | "os" 6 | "path" 7 | "strings" 8 | 9 | "mydocker/constant" 10 | 11 | log "github.com/sirupsen/logrus" 12 | 13 | "github.com/pkg/errors" 14 | ) 15 | 16 | const mountPointIndex = 4 17 | 18 | // getCgroupPath 找到cgroup在文件系统中的绝对路径 19 | /* 20 | 实际就是将根目录和cgroup名称拼接成一个路径。 21 | 如果指定了自动创建,就先检测一下是否存在,如果对应的目录不存在,则说明cgroup不存在,这里就给创建一个 22 | */ 23 | func getCgroupPath(subsystem string, cgroupPath string, autoCreate bool) (string, error) { 24 | // 不需要自动创建就直接返回 25 | cgroupRoot := findCgroupMountpoint(subsystem) 26 | absPath := path.Join(cgroupRoot, cgroupPath) 27 | if !autoCreate { 28 | return absPath, nil 29 | } 30 | // 指定自动创建时才判断是否存在 31 | _, err := os.Stat(absPath) 32 | // 只有不存在才创建 33 | if err != nil && os.IsNotExist(err) { 34 | err = os.Mkdir(absPath, constant.Perm0755) 35 | return absPath, err 36 | } 37 | // 其他错误或者没有错误都直接返回,如果err=nil,那么errors.Wrap(err, "")也会是nil 38 | return absPath, errors.Wrap(err, "create cgroup") 39 | } 40 | 41 | // findCgroupMountpoint 通过/proc/self/mountinfo找出挂载了某个subsystem的hierarchy cgroup根节点所在的目录 42 | func findCgroupMountpoint(subsystem string) string { 43 | // /proc/self/mountinfo 为当前进程的 mountinfo 信息 44 | // 可以直接通过 cat /proc/self/mountinfo 命令查看 45 | f, err := os.Open("/proc/self/mountinfo") 46 | if err != nil { 47 | return "" 48 | } 49 | defer f.Close() 50 | // 这里主要根据各种字符串处理来找到目标位置 51 | scanner := bufio.NewScanner(f) 52 | for scanner.Scan() { 53 | // txt 大概是这样的:104 85 0:20 / /sys/fs/cgroup/memory rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,memory 54 | txt := scanner.Text() 55 | // 然后按照空格分割 56 | fields := strings.Split(txt, " ") 57 | // 对最后一个元素按逗号进行分割,这里的最后一个元素就是 rw,memory 58 | // 其中的的 memory 就表示这是一个 memory subsystem 59 | subsystems := strings.Split(fields[len(fields)-1], ",") 60 | for _, opt := range subsystems { 61 | if opt == subsystem { 62 | // 如果等于指定的 subsystem,那么就返回这个挂载点跟目录,就是第四个元素, 63 | // 这里就是`/sys/fs/cgroup/memory`,即我们要找的根目录 64 | return fields[mountPointIndex] 65 | } 66 | } 67 | } 68 | 69 | if err = scanner.Err(); err != nil { 70 | log.Error("read err:", err) 71 | return "" 72 | } 73 | return "" 74 | } 75 | -------------------------------------------------------------------------------- /cgroups/fs/utils_test.go: -------------------------------------------------------------------------------- 1 | package fs 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestFindCgroupMountpoint(t *testing.T) { 8 | t.Logf("cpu subsystem mount point %v\n", findCgroupMountpoint("cpu")) 9 | t.Logf("cpuset subsystem mount point %v\n", findCgroupMountpoint("cpuset")) 10 | t.Logf("memory subsystem mount point %v\n", findCgroupMountpoint("memory")) 11 | } 12 | -------------------------------------------------------------------------------- /cgroups/fs2/cpu.go: -------------------------------------------------------------------------------- 1 | package fs2 2 | 3 | import ( 4 | "fmt" 5 | "mydocker/cgroups/resource" 6 | "os" 7 | "path" 8 | "strconv" 9 | 10 | "mydocker/constant" 11 | ) 12 | 13 | type CpuSubSystem struct { 14 | } 15 | 16 | const ( 17 | PeriodDefault = 100000 18 | Percent = 100 19 | ) 20 | 21 | func (s *CpuSubSystem) Name() string { 22 | return "cpu" 23 | } 24 | 25 | func (s *CpuSubSystem) Set(cgroupPath string, res *resource.ResourceConfig) error { 26 | if res.CpuCfsQuota == 0 { 27 | return nil 28 | } 29 | subCgroupPath, err := getCgroupPath(cgroupPath, true) 30 | if err != nil { 31 | return err 32 | } 33 | 34 | // cpu.cfs_period_us & cpu.cfs_quota_us 控制的是CPU使用时间,单位是微秒,比如每1秒钟,这个进程只能使用200ms,相当于只能用20%的CPU 35 | // v2 中直接将 cpu.cfs_period_us & cpu.cfs_quota_us 统一记录到 cpu.max 中,比如 5000 10000 这样就是限制使用 50% cpu 36 | if res.CpuCfsQuota != 0 { 37 | // cpu.cfs_quota_us 则根据用户传递的参数来控制,比如参数为20,就是限制为20%CPU,所以把cpu.cfs_quota_us设置为cpu.cfs_period_us的20%就行 38 | // 这里只是简单的计算了下,并没有处理一些特殊情况,比如负数什么的 39 | if err = os.WriteFile(path.Join(subCgroupPath, "cpu.max"), []byte(fmt.Sprintf("%s %s", strconv.Itoa(PeriodDefault/Percent*res.CpuCfsQuota), PeriodDefault)), constant.Perm0644); err != nil { 40 | return fmt.Errorf("set cgroup cpu share fail %v", err) 41 | } 42 | } 43 | return nil 44 | } 45 | 46 | func (s *CpuSubSystem) Apply(cgroupPath string, pid int) error { 47 | return applyCgroup(pid, cgroupPath) 48 | } 49 | 50 | func (s *CpuSubSystem) Remove(cgroupPath string) error { 51 | subCgroupPath, err := getCgroupPath(cgroupPath, false) 52 | if err != nil { 53 | return err 54 | } 55 | return os.RemoveAll(subCgroupPath) 56 | } 57 | -------------------------------------------------------------------------------- /cgroups/fs2/cpuset.go: -------------------------------------------------------------------------------- 1 | package fs2 2 | 3 | import ( 4 | "fmt" 5 | "mydocker/cgroups/resource" 6 | "mydocker/constant" 7 | "os" 8 | "path" 9 | ) 10 | 11 | type CpusetSubSystem struct { 12 | } 13 | 14 | func (s *CpusetSubSystem) Name() string { 15 | return "cpuset" 16 | } 17 | 18 | func (s *CpusetSubSystem) Set(cgroupPath string, res *resource.ResourceConfig) error { 19 | if res.CpuSet == "" { 20 | return nil 21 | } 22 | subCgroupPath, err := getCgroupPath(cgroupPath, true) 23 | if err != nil { 24 | return err 25 | } 26 | if err := os.WriteFile(path.Join(subCgroupPath, "cpuset.cpus"), []byte(res.CpuSet), constant.Perm0644); err != nil { 27 | return fmt.Errorf("set cgroup cpuset fail %v", err) 28 | } 29 | return nil 30 | } 31 | 32 | func (s *CpusetSubSystem) Apply(cgroupPath string, pid int) error { 33 | return applyCgroup(pid, cgroupPath) 34 | } 35 | 36 | func (s *CpusetSubSystem) Remove(cgroupPath string) error { 37 | subsysCgroupPath, err := getCgroupPath(cgroupPath, false) 38 | if err != nil { 39 | return err 40 | } 41 | return os.RemoveAll(subsysCgroupPath) 42 | } 43 | -------------------------------------------------------------------------------- /cgroups/fs2/defaultpath.go: -------------------------------------------------------------------------------- 1 | package fs2 2 | 3 | const UnifiedMountpoint = "/sys/fs/cgroup" 4 | -------------------------------------------------------------------------------- /cgroups/fs2/memory.go: -------------------------------------------------------------------------------- 1 | package fs2 2 | 3 | import ( 4 | "fmt" 5 | "mydocker/cgroups/resource" 6 | "mydocker/constant" 7 | "os" 8 | "path" 9 | ) 10 | 11 | type MemorySubSystem struct { 12 | } 13 | 14 | // Name 返回cgroup名字 15 | func (s *MemorySubSystem) Name() string { 16 | return "memory" 17 | } 18 | 19 | // Set 设置cgroupPath对应的cgroup的内存资源限制 20 | func (s *MemorySubSystem) Set(cgroupPath string, res *resource.ResourceConfig) error { 21 | if res.MemoryLimit == "" { 22 | return nil 23 | } 24 | subCgroupPath, err := getCgroupPath(cgroupPath, true) 25 | if err != nil { 26 | return err 27 | } 28 | // 设置这个cgroup的内存限制,即将限制写入到cgroup对应目录的memory.limit_in_bytes 文件中。 29 | if err := os.WriteFile(path.Join(subCgroupPath, "memory.max"), []byte(res.MemoryLimit), constant.Perm0644); err != nil { 30 | return fmt.Errorf("set cgroup memory fail %v", err) 31 | } 32 | return nil 33 | } 34 | 35 | // Apply 将pid加入到cgroupPath对应的cgroup中 36 | func (s *MemorySubSystem) Apply(cgroupPath string, pid int) error { 37 | return applyCgroup(pid, cgroupPath) 38 | } 39 | 40 | // Remove 删除cgroupPath对应的cgroup 41 | func (s *MemorySubSystem) Remove(cgroupPath string) error { 42 | subCgroupPath, err := getCgroupPath(cgroupPath, false) 43 | if err != nil { 44 | return err 45 | } 46 | return os.RemoveAll(subCgroupPath) 47 | } 48 | -------------------------------------------------------------------------------- /cgroups/fs2/subsystems.go: -------------------------------------------------------------------------------- 1 | package fs2 2 | 3 | import ( 4 | "mydocker/cgroups/resource" 5 | ) 6 | 7 | var Subsystems = []resource.Subsystem{ 8 | &CpusetSubSystem{}, 9 | &MemorySubSystem{}, 10 | &CpuSubSystem{}, 11 | } 12 | -------------------------------------------------------------------------------- /cgroups/fs2/utils.go: -------------------------------------------------------------------------------- 1 | package fs2 2 | 3 | import ( 4 | "fmt" 5 | "mydocker/constant" 6 | "os" 7 | "path" 8 | "strconv" 9 | 10 | "github.com/pkg/errors" 11 | ) 12 | 13 | // getCgroupPath 找到cgroup在文件系统中的绝对路径 14 | /* 15 | 实际就是将根目录和cgroup名称拼接成一个路径。 16 | 如果指定了自动创建,就先检测一下是否存在,如果对应的目录不存在,则说明cgroup不存在,这里就给创建一个 17 | */ 18 | func getCgroupPath(cgroupPath string, autoCreate bool) (string, error) { 19 | // 不需要自动创建就直接返回 20 | cgroupRoot := UnifiedMountpoint 21 | absPath := path.Join(cgroupRoot, cgroupPath) 22 | if !autoCreate { 23 | return absPath, nil 24 | } 25 | // 指定自动创建时才判断是否存在 26 | _, err := os.Stat(absPath) 27 | // 只有不存在才创建 28 | if err != nil && os.IsNotExist(err) { 29 | err = os.Mkdir(absPath, constant.Perm0755) 30 | return absPath, err 31 | } 32 | return absPath, errors.Wrap(err, "create cgroup") 33 | } 34 | 35 | func applyCgroup(pid int, cgroupPath string) error { 36 | subCgroupPath, err := getCgroupPath(cgroupPath, true) 37 | if err != nil { 38 | return errors.Wrapf(err, "get cgroup %s", cgroupPath) 39 | } 40 | if err = os.WriteFile(path.Join(subCgroupPath, "cgroup.procs"), []byte(strconv.Itoa(pid)), 41 | constant.Perm0644); err != nil { 42 | return fmt.Errorf("set cgroup proc fail %v", err) 43 | } 44 | return nil 45 | } 46 | -------------------------------------------------------------------------------- /cgroups/resource/resource.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | // ResourceConfig 用于传递资源限制配置的结构体,包含内存限制,CPU 时间片权重,CPU核心数 4 | type ResourceConfig struct { 5 | MemoryLimit string 6 | CpuCfsQuota int 7 | CpuSet string 8 | } 9 | -------------------------------------------------------------------------------- /cgroups/resource/subsystem.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | // Subsystem 接口,每个Subsystem可以实现下面的4个接口, 4 | // 这里将cgroup抽象成了path,原因是cgroup在hierarchy的路径,便是虚拟文件系统中的虚拟路径 5 | // Set、Apply、Remove 这3个接口都判断一下,如果没有传配置信息进来就不处理,直接返回。 6 | type Subsystem interface { 7 | // Name 返回当前Subsystem的名称,比如cpu、memory 8 | Name() string 9 | // Set 设置某个cgroup在这个Subsystem中的资源限制 10 | Set(path string, res *ResourceConfig) error 11 | // Apply 将进程添加到某个cgroup中 12 | Apply(path string, pid int) error 13 | // Remove 移除某个cgroup 14 | Remove(path string) error 15 | } 16 | -------------------------------------------------------------------------------- /cgroups/util.go: -------------------------------------------------------------------------------- 1 | package cgroups 2 | 3 | import ( 4 | "os" 5 | "sync" 6 | 7 | "golang.org/x/sys/unix" 8 | ) 9 | 10 | const ( 11 | unifiedMountpoint = "/sys/fs/cgroup" 12 | ) 13 | 14 | var ( 15 | isUnifiedOnce sync.Once 16 | isUnified bool 17 | ) 18 | 19 | // IsCgroup2UnifiedMode returns whether we are running in cgroup v2 unified mode. 20 | func IsCgroup2UnifiedMode() bool { 21 | isUnifiedOnce.Do(func() { 22 | var st unix.Statfs_t 23 | err := unix.Statfs(unifiedMountpoint, &st) 24 | if err != nil && os.IsNotExist(err) { 25 | // For rootless containers, sweep it under the rug. 26 | isUnified = false 27 | return 28 | } 29 | isUnified = st.Type == unix.CGROUP2_SUPER_MAGIC 30 | }) 31 | return isUnified 32 | } 33 | -------------------------------------------------------------------------------- /commit.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/pkg/errors" 5 | log "github.com/sirupsen/logrus" 6 | "mydocker/utils" 7 | "os/exec" 8 | ) 9 | 10 | var ErrImageAlreadyExists = errors.New("Image Already Exists") 11 | 12 | func commitContainer(containerID, imageName string) error { 13 | mntPath := utils.GetMerged(containerID) 14 | imageTar := utils.GetImage(imageName) 15 | exists, err := utils.PathExists(imageTar) 16 | if err != nil { 17 | return errors.WithMessagef(err, "check is image [%s/%s] exist failed", imageName, imageTar) 18 | } 19 | if exists { 20 | return ErrImageAlreadyExists 21 | } 22 | log.Infof("commitContainer imageTar:%s", imageTar) 23 | if _, err = exec.Command("tar", "-czf", imageTar, "-C", mntPath, ".").CombinedOutput(); err != nil { 24 | return errors.WithMessagef(err, "tar folder %s failed", mntPath) 25 | } 26 | return nil 27 | } 28 | -------------------------------------------------------------------------------- /constant/mode.go: -------------------------------------------------------------------------------- 1 | package constant 2 | 3 | const ( 4 | Perm0777 = 0777 // 用户具、组用户和其它用户都有读/写/执行权限 5 | Perm0755 = 0755 // 用户具有读/写/执行权限,组用户和其它用户具有读写权限; 6 | Perm0644 = 0644 // 用户具有读写权限,组用户和其它用户具有只读权限; 7 | Perm0622 = 0622 // 用户具有读/写权限,组用户和其它用户具只写权限; 8 | ) 9 | -------------------------------------------------------------------------------- /container/container_info.go: -------------------------------------------------------------------------------- 1 | package container 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/pkg/errors" 7 | "math/rand" 8 | "mydocker/constant" 9 | "os" 10 | "path" 11 | "strconv" 12 | "strings" 13 | "time" 14 | ) 15 | 16 | func RecordContainerInfo(containerPID int, commandArray []string, containerName, containerId, volume, networkName, ip string, portMapping []string) (*Info, error) { 17 | // 如果未指定容器名,则使用随机生成的containerID 18 | if containerName == "" { 19 | containerName = containerId 20 | } 21 | command := strings.Join(commandArray, "") 22 | containerInfo := &Info{ 23 | Pid: strconv.Itoa(containerPID), 24 | Id: containerId, 25 | Name: containerName, 26 | Command: command, 27 | CreatedTime: time.Now().Format("2006-01-02 15:04:05"), 28 | Status: RUNNING, 29 | Volume: volume, 30 | NetworkName: networkName, 31 | PortMapping: portMapping, 32 | IP: ip, 33 | } 34 | 35 | jsonBytes, err := json.Marshal(containerInfo) 36 | if err != nil { 37 | return containerInfo, errors.WithMessage(err, "container info marshal failed") 38 | } 39 | jsonStr := string(jsonBytes) 40 | // 拼接出存储容器信息文件的路径,如果目录不存在则级联创建 41 | dirPath := fmt.Sprintf(InfoLocFormat, containerId) 42 | if err = os.MkdirAll(dirPath, constant.Perm0622); err != nil { 43 | return containerInfo, errors.WithMessagef(err, "mkdir %s failed", dirPath) 44 | } 45 | // 将容器信息写入文件 46 | fileName := path.Join(dirPath, ConfigName) 47 | file, err := os.Create(fileName) 48 | if err != nil { 49 | return containerInfo, errors.WithMessagef(err, "create file %s failed", fileName) 50 | } 51 | defer file.Close() 52 | if _, err = file.WriteString(jsonStr); err != nil { 53 | return containerInfo, errors.WithMessagef(err, "write container info to file %s failed", fileName) 54 | } 55 | return containerInfo, nil 56 | } 57 | 58 | func DeleteContainerInfo(containerID string) error { 59 | dirPath := fmt.Sprintf(InfoLocFormat, containerID) 60 | if err := os.RemoveAll(dirPath); err != nil { 61 | return errors.WithMessagef(err, "remove dir %s failed", dirPath) 62 | } 63 | return nil 64 | } 65 | 66 | func GenerateContainerID() string { 67 | return randStringBytes(IDLength) 68 | } 69 | 70 | func randStringBytes(n int) string { 71 | letterBytes := "1234567890" 72 | b := make([]byte, n) 73 | for i := range b { 74 | b[i] = letterBytes[rand.Intn(len(letterBytes))] 75 | } 76 | return string(b) 77 | } 78 | 79 | // GetLogfile build logfile name by containerId 80 | func GetLogfile(containerId string) string { 81 | return fmt.Sprintf(LogFile, containerId) 82 | } 83 | -------------------------------------------------------------------------------- /container/container_process.go: -------------------------------------------------------------------------------- 1 | package container 2 | 3 | import ( 4 | "fmt" 5 | "mydocker/constant" 6 | "mydocker/utils" 7 | "os" 8 | "os/exec" 9 | "syscall" 10 | 11 | log "github.com/sirupsen/logrus" 12 | ) 13 | 14 | const ( 15 | RUNNING = "running" 16 | STOP = "stopped" 17 | Exit = "exited" 18 | InfoLoc = "/var/lib/mydocker/containers/" 19 | InfoLocFormat = InfoLoc + "%s/" 20 | ConfigName = "config.json" 21 | IDLength = 10 22 | LogFile = "%s-json.log" 23 | ) 24 | 25 | type Info struct { 26 | Pid string `json:"pid"` // 容器的init进程在宿主机上的 PID 27 | Id string `json:"id"` // 容器Id 28 | Name string `json:"name"` // 容器名 29 | Command string `json:"command"` // 容器内init运行命令 30 | CreatedTime string `json:"createTime"` // 创建时间 31 | Status string `json:"status"` // 容器的状态 32 | Volume string `json:"volume"` // 容器挂载的 volume 33 | NetworkName string `json:"networkName"` // 容器所在的网络 34 | PortMapping []string `json:"portmapping"` // 端口映射 35 | IP string `json:"ip"` 36 | } 37 | 38 | // NewParentProcess 构建 command 用于启动一个新进程 39 | /* 40 | 这里是父进程,也就是当前进程执行的内容。 41 | 1.这里的/proc/se1f/exe调用中,/proc/self/ 指的是当前运行进程自己的环境,exec 其实就是自己调用了自己,使用这种方式对创建出来的进程进行初始化 42 | 2.后面的args是参数,其中init是传递给本进程的第一个参数,在本例中,其实就是会去调用initCommand去初始化进程的一些环境和资源 43 | 3.下面的clone参数就是去fork出来一个新进程,并且使用了namespace隔离新创建的进程和外部环境。 44 | 4.如果用户指定了-it参数,就需要把当前进程的输入输出导入到标准输入输出上 45 | */ 46 | func NewParentProcess(tty bool, volume, containerId, imageName string, envSlice []string) (*exec.Cmd, *os.File) { 47 | // 创建匿名管道用于传递参数,将readPipe作为子进程的ExtraFiles,子进程从readPipe中读取参数 48 | // 父进程中则通过writePipe将参数写入管道 49 | readPipe, writePipe, err := os.Pipe() 50 | if err != nil { 51 | log.Errorf("New pipe error %v", err) 52 | return nil, nil 53 | } 54 | cmd := exec.Command("/proc/self/exe", "init") 55 | cmd.SysProcAttr = &syscall.SysProcAttr{ 56 | Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS | syscall.CLONE_NEWNET | syscall.CLONE_NEWIPC, 57 | } 58 | if tty { 59 | cmd.Stdin = os.Stdin 60 | cmd.Stdout = os.Stdout 61 | cmd.Stderr = os.Stderr 62 | } else { 63 | // 对于后台运行容器,将 stdout、stderr 重定向到日志文件中,便于后续查看 64 | dirPath := fmt.Sprintf(InfoLocFormat, containerId) 65 | if err = os.MkdirAll(dirPath, constant.Perm0622); err != nil { 66 | log.Errorf("NewParentProcess mkdir %s error %v", dirPath, err) 67 | return nil, nil 68 | } 69 | stdLogFilePath := dirPath + GetLogfile(containerId) 70 | stdLogFile, err := os.Create(stdLogFilePath) 71 | if err != nil { 72 | log.Errorf("NewParentProcess create file %s error %v", stdLogFilePath, err) 73 | return nil, nil 74 | } 75 | cmd.Stdout = stdLogFile 76 | cmd.Stderr = stdLogFile 77 | } 78 | cmd.Env = append(os.Environ(), envSlice...) 79 | cmd.ExtraFiles = []*os.File{readPipe} 80 | NewWorkSpace(containerId, imageName, volume) 81 | cmd.Dir = utils.GetMerged(containerId) 82 | return cmd, writePipe 83 | } 84 | -------------------------------------------------------------------------------- /container/init.go: -------------------------------------------------------------------------------- 1 | package container 2 | 3 | import ( 4 | "github.com/pkg/errors" 5 | log "github.com/sirupsen/logrus" 6 | "io" 7 | "os" 8 | "os/exec" 9 | "path/filepath" 10 | "strings" 11 | "syscall" 12 | ) 13 | 14 | // RunContainerInitProcess 启动容器的init进程 15 | /* 16 | 这里的init函数是在容器内部执行的,也就是说,代码执行到这里后,容器所在的进程其实就已经创建出来了, 17 | 这是本容器执行的第一一个进程。 18 | 使用mount先去挂载proc文件系统,以便后面通过ps等系统命令去查看当前进程资源的情况。 19 | */ 20 | func RunContainerInitProcess() error { 21 | // 根据参数获取命令的完整路径 此时不需要再输入完整命令了 22 | 23 | // 从 pipe 中读取命令 24 | cmdArray := readUserCommand() 25 | if len(cmdArray) == 0 { 26 | return errors.New("run container get user command error, cmdArray is nil") 27 | } 28 | 29 | // 挂载文件系统 30 | setUpMount() 31 | 32 | path, err := exec.LookPath(cmdArray[0]) 33 | if err != nil { 34 | log.Errorf("Exec loop path error %v", err) 35 | return err 36 | } 37 | log.Infof("Find path %s", path) 38 | if err = syscall.Exec(path, cmdArray[0:], os.Environ()); err != nil { 39 | log.Errorf("RunContainerInitProcess exec :" + err.Error()) 40 | } 41 | return nil 42 | } 43 | 44 | const fdIndex = 3 45 | 46 | func readUserCommand() []string { 47 | // uintptr(3 )就是指 index 为3的文件描述符,也就是传递进来的管道的另一端,至于为什么是3,具体解释如下: 48 | /* 因为每个进程默认都会有3个文件描述符,分别是标准输入、标准输出、标准错误。这3个是子进程一创建的时候就会默认带着的, 49 | 前面通过ExtraFiles方式带过来的 readPipe 理所当然地就成为了第4个。 50 | 在进程中可以通过index方式读取对应的文件,比如 51 | index0:标准输入 52 | index1:标准输出 53 | index2:标准错误 54 | index3:带过来的第一个FD,也就是readPipe 55 | 由于可以带多个FD过来,所以这里的3就不是固定的了。 56 | 比如像这样:cmd.ExtraFiles = []*os.File{a,b,c,readPipe} 这里带了4个文件过来,分别的index就是3,4,5,6 57 | 那么我们的 readPipe 就是 index6,读取时就要像这样:pipe := os.NewFile(uintptr(6), "pipe") 58 | */ 59 | pipe := os.NewFile(uintptr(fdIndex), "pipe") 60 | defer pipe.Close() 61 | msg, err := io.ReadAll(pipe) 62 | if err != nil { 63 | log.Errorf("init read pipe error %v", err) 64 | return nil 65 | } 66 | msgStr := string(msg) 67 | return strings.Split(msgStr, " ") 68 | } 69 | 70 | /* 71 | * 72 | Init 挂载点 73 | */ 74 | func setUpMount() { 75 | pwd, err := os.Getwd() 76 | if err != nil { 77 | log.Errorf("Get current location error %v", err) 78 | return 79 | } 80 | log.Infof("Current location is %s", pwd) 81 | 82 | // systemd 加入linux之后, mount namespace 就变成 shared by default, 所以你必须显示 83 | // 声明你要这个新的mount namespace独立。 84 | // 如果不先做 private mount,会导致挂载事件外泄,后续执行 pivotRoot 会出现 invalid argument 错误 85 | err = syscall.Mount("", "/", "", syscall.MS_PRIVATE|syscall.MS_REC, "") 86 | 87 | err = pivotRoot(pwd) 88 | if err != nil { 89 | log.Errorf("pivotRoot failed,detail: %v", err) 90 | return 91 | } 92 | 93 | // mount /proc 94 | defaultMountFlags := syscall.MS_NOEXEC | syscall.MS_NOSUID | syscall.MS_NODEV 95 | _ = syscall.Mount("proc", "/proc", "proc", uintptr(defaultMountFlags), "") 96 | // 由于前面 pivotRoot 切换了 rootfs,因此这里重新 mount 一下 /dev 目录 97 | // tmpfs 是基于 件系 使用 RAM、swap 分区来存储。 98 | // 不挂载 /dev,会导致容器内部无法访问和使用许多设备,这可能导致系统无法正常工作 99 | syscall.Mount("tmpfs", "/dev", "tmpfs", syscall.MS_NOSUID|syscall.MS_STRICTATIME, "mode=755") 100 | } 101 | 102 | func pivotRoot(root string) error { 103 | /** 104 | NOTE:PivotRoot调用有限制,newRoot和oldRoot不能在同一个文件系统下。 105 | 因此,为了使当前root的老root和新root不在同一个文件系统下,这里把root重新mount了一次。 106 | bind mount是把相同的内容换了一个挂载点的挂载方法 107 | */ 108 | if err := syscall.Mount(root, root, "bind", syscall.MS_BIND|syscall.MS_REC, ""); err != nil { 109 | return errors.Wrap(err, "mount rootfs to itself") 110 | } 111 | // 创建 rootfs/.pivot_root 目录用于存储 old_root 112 | pivotDir := filepath.Join(root, ".pivot_root") 113 | if err := os.Mkdir(pivotDir, 0777); err != nil { 114 | return err 115 | } 116 | // 执行pivot_root调用,将系统rootfs切换到新的rootfs, 117 | // PivotRoot调用会把 old_root挂载到pivotDir,也就是rootfs/.pivot_root,挂载点现在依然可以在mount命令中看到 118 | if err := syscall.PivotRoot(root, pivotDir); err != nil { 119 | return errors.WithMessagef(err, "pivotRoot failed,new_root:%v old_put:%v", root, pivotDir) 120 | } 121 | // 修改当前的工作目录到根目录 122 | if err := syscall.Chdir("/"); err != nil { 123 | return errors.WithMessage(err, "chdir to / failed") 124 | } 125 | 126 | // 最后再把old_root umount了,即 umount rootfs/.pivot_root 127 | // 由于当前已经是在 rootfs 下了,就不能再用上面的rootfs/.pivot_root这个路径了,现在直接用/.pivot_root这个路径即可 128 | pivotDir = filepath.Join("/", ".pivot_root") 129 | if err := syscall.Unmount(pivotDir, syscall.MNT_DETACH); err != nil { 130 | return errors.WithMessage(err, "unmount pivot_root dir") 131 | } 132 | // 删除临时文件夹 133 | return os.Remove(pivotDir) 134 | } 135 | -------------------------------------------------------------------------------- /container/rootfs.go: -------------------------------------------------------------------------------- 1 | package container 2 | 3 | import ( 4 | log "github.com/sirupsen/logrus" 5 | "mydocker/utils" 6 | "os" 7 | "os/exec" 8 | ) 9 | 10 | // NewWorkSpace Create an Overlay2 filesystem as container root workspace 11 | /* 12 | 1)创建lower层 13 | 2)创建upper、worker层 14 | 3)创建merged目录并挂载overlayFS 15 | 4)如果有指定volume则挂载volume 16 | */ 17 | func NewWorkSpace(containerID, imageName, volume string) { 18 | createLower(containerID, imageName) 19 | createDirs(containerID) 20 | mountOverlayFS(containerID) 21 | 22 | // 如果指定了volume则还需要mount volume 23 | if volume != "" { 24 | mntPath := utils.GetMerged(containerID) 25 | hostPath, containerPath, err := volumeExtract(volume) 26 | if err != nil { 27 | log.Errorf("extract volume failed,maybe volume parameter input is not correct,detail:%v", err) 28 | return 29 | } 30 | mountVolume(mntPath, hostPath, containerPath) 31 | } 32 | } 33 | 34 | // DeleteWorkSpace Delete the UFS filesystem while container exit 35 | /* 36 | 和创建相反 37 | 1)有volume则卸载volume 38 | 2)卸载并移除merged目录 39 | 3)卸载并移除upper、worker层 40 | */ 41 | func DeleteWorkSpace(containerID, volume string) { 42 | // 如果指定了volume则需要umount volume 43 | // NOTE: 一定要要先 umount volume ,然后再删除目录,否则由于 bind mount 存在,删除临时目录会导致 volume 目录中的数据丢失。 44 | if volume != "" { 45 | _, containerPath, err := volumeExtract(volume) 46 | if err != nil { 47 | log.Errorf("extract volume failed,maybe volume parameter input is not correct,detail:%v", err) 48 | return 49 | } 50 | mntPath := utils.GetMerged(containerID) 51 | umountVolume(mntPath, containerPath) 52 | } 53 | 54 | umountOverlayFS(containerID) 55 | deleteDirs(containerID) 56 | } 57 | 58 | // createLower 根据 containerID, imageName 准备 lower 层目录 59 | func createLower(containerID, imageName string) { 60 | // 根据 containerID 拼接出 lower 目录 61 | // 根据 imageName 找到镜像 tar,并解压到 lower 目录中 62 | lowerPath := utils.GetLower(containerID) 63 | imagePath := utils.GetImage(imageName) 64 | log.Infof("lower:%s image.tar:%s", lowerPath, imagePath) 65 | // 检查目录是否已经存在 66 | exist, err := utils.PathExists(lowerPath) 67 | if err != nil { 68 | log.Infof("Fail to judge whether dir %s exists. %v", lowerPath, err) 69 | } 70 | // 不存在则创建目录并将image.tar解压到lower文件夹中 71 | if !exist { 72 | if err = os.MkdirAll(lowerPath, 0777); err != nil { 73 | log.Errorf("Mkdir dir %s error. %v", lowerPath, err) 74 | } 75 | if _, err = exec.Command("tar", "-xvf", imagePath, "-C", lowerPath).CombinedOutput(); err != nil { 76 | log.Errorf("Untar dir %s error %v", lowerPath, err) 77 | } 78 | } 79 | } 80 | 81 | // createDirs 创建overlayfs需要的的merged、upper、worker目录 82 | func createDirs(containerID string) { 83 | dirs := []string{ 84 | utils.GetMerged(containerID), 85 | utils.GetUpper(containerID), 86 | utils.GetWorker(containerID), 87 | } 88 | 89 | for _, dir := range dirs { 90 | if err := os.Mkdir(dir, 0777); err != nil { 91 | log.Errorf("mkdir dir %s error. %v", dir, err) 92 | } 93 | } 94 | } 95 | 96 | // mountOverlayFS 挂载overlayfs 97 | func mountOverlayFS(containerID string) { 98 | // 拼接参数 99 | // e.g. lowerdir=/root/busybox,upperdir=/root/upper,workdir=/root/work 100 | dirs := utils.GetOverlayFSDirs(utils.GetLower(containerID), utils.GetUpper(containerID), utils.GetWorker(containerID)) 101 | mergedPath := utils.GetMerged(containerID) 102 | //完整命令:mount -t overlay overlay -o lowerdir=/root/{containerID}/lower,upperdir=/root/{containerID}/upper,workdir=/root/{containerID}/work /root/{containerID}/merged 103 | cmd := exec.Command("mount", "-t", "overlay", "overlay", "-o", dirs, mergedPath) 104 | log.Infof("mount overlayfs: [%s]", cmd.String()) 105 | cmd.Stdout = os.Stdout 106 | cmd.Stderr = os.Stderr 107 | if err := cmd.Run(); err != nil { 108 | log.Errorf("%v", err) 109 | } 110 | } 111 | 112 | func umountOverlayFS(containerID string) { 113 | mntPath := utils.GetMerged(containerID) 114 | cmd := exec.Command("umount", mntPath) 115 | cmd.Stdout = os.Stdout 116 | cmd.Stderr = os.Stderr 117 | log.Infof("umountOverlayFS,cmd:%v", cmd.String()) 118 | if err := cmd.Run(); err != nil { 119 | log.Errorf("%v", err) 120 | } 121 | } 122 | 123 | func deleteDirs(containerID string) { 124 | dirs := []string{ 125 | utils.GetMerged(containerID), 126 | utils.GetUpper(containerID), 127 | utils.GetWorker(containerID), 128 | utils.GetLower(containerID), 129 | utils.GetRoot(containerID), // root 目录也要删除 130 | } 131 | 132 | for _, dir := range dirs { 133 | if err := os.RemoveAll(dir); err != nil { 134 | log.Errorf("Remove dir %s error %v", dir, err) 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /container/volume.go: -------------------------------------------------------------------------------- 1 | package container 2 | 3 | import ( 4 | "fmt" 5 | log "github.com/sirupsen/logrus" 6 | "mydocker/constant" 7 | "os" 8 | "os/exec" 9 | "path" 10 | "strings" 11 | ) 12 | 13 | // volume 相关工具 14 | 15 | // mountVolume 使用 bind mount 挂载 volume 16 | func mountVolume(mntPath, hostPath, containerPath string) { 17 | // 创建宿主机目录 18 | if err := os.Mkdir(hostPath, constant.Perm0777); err != nil { 19 | log.Infof("mkdir parent dir %s error. %v", hostPath, err) 20 | } 21 | // 拼接出对应的容器目录在宿主机上的的位置,并创建对应目录 22 | containerPathInHost := path.Join(mntPath, containerPath) 23 | if err := os.Mkdir(containerPathInHost, constant.Perm0777); err != nil { 24 | log.Infof("mkdir container dir %s error. %v", containerPathInHost, err) 25 | } 26 | // 通过bind mount 将宿主机目录挂载到容器目录 27 | // mount -o bind /hostPath /containerPath 28 | cmd := exec.Command("mount", "-o", "bind", hostPath, containerPathInHost) 29 | cmd.Stdout = os.Stdout 30 | cmd.Stderr = os.Stderr 31 | if err := cmd.Run(); err != nil { 32 | log.Errorf("mount volume failed. %v", err) 33 | } 34 | } 35 | 36 | func umountVolume(mntPath, containerPath string) { 37 | // mntPath 为容器在宿主机上的挂载点,例如 /root/merged 38 | // containerPath 为 volume 在容器中对应的目录,例如 /root/tmp 39 | // containerPathInHost 则是容器中目录在宿主机上的具体位置,例如 /root/merged/root/tmp 40 | containerPathInHost := path.Join(mntPath, containerPath) 41 | cmd := exec.Command("umount", containerPathInHost) 42 | cmd.Stdout = os.Stdout 43 | cmd.Stderr = os.Stderr 44 | if err := cmd.Run(); err != nil { 45 | log.Errorf("Umount volume failed. %v", err) 46 | } 47 | } 48 | 49 | // volumeExtract 通过冒号分割解析volume目录,比如 -v /tmp:/tmp 50 | func volumeExtract(volume string) (sourcePath, destinationPath string, err error) { 51 | parts := strings.Split(volume, ":") 52 | if len(parts) != 2 { 53 | return "", "", fmt.Errorf("invalid volume [%s], must split by `:`", volume) 54 | } 55 | 56 | sourcePath, destinationPath = parts[0], parts[1] 57 | if sourcePath == "" || destinationPath == "" { 58 | return "", "", fmt.Errorf("invalid volume [%s], path can't be empty", volume) 59 | } 60 | 61 | return sourcePath, destinationPath, nil 62 | } 63 | -------------------------------------------------------------------------------- /example/cgo/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | /* 4 | #include 5 | */ 6 | import "C" 7 | import ( 8 | "fmt" 9 | ) 10 | 11 | func main() { 12 | Seed(123) 13 | // Output:Random: 128959393 14 | fmt.Println("Random: ", Random()) 15 | } 16 | 17 | // Seed 初始化随机数产生器 18 | func Seed(i int) { 19 | C.srandom(C.uint(i)) 20 | } 21 | 22 | // Random 产生一个随机数 23 | func Random() int { 24 | return int(C.random()) 25 | } 26 | -------------------------------------------------------------------------------- /example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "os/exec" 8 | "path/filepath" 9 | "strconv" 10 | "syscall" 11 | 12 | "mydocker/constant" 13 | 14 | "github.com/urfave/cli" 15 | ) 16 | 17 | /* 18 | 一个 Go 中调用 namespace 和 Cgroups 的例子。 19 | 注: 运行时需要 root 权限。 20 | */ 21 | 22 | func main() { 23 | // namespace() 24 | // cgroups() 25 | urfaveCli() 26 | } 27 | 28 | // namespace 如何在Go中新建Namespace 29 | func namespace() { 30 | cmd := exec.Command("bash") 31 | cmd.SysProcAttr = &syscall.SysProcAttr{ 32 | Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS | 33 | syscall.CLONE_NEWUSER | syscall.CLONE_NEWNET, 34 | } 35 | 36 | cmd.Stdin = os.Stdin 37 | cmd.Stdout = os.Stdout 38 | cmd.Stderr = os.Stderr 39 | 40 | if err := cmd.Run(); err != nil { 41 | log.Fatalln(err) 42 | } 43 | } 44 | 45 | // cgroupMemoryHierarchyMount 挂载了memory subsystem的hierarchy的根目录位置 46 | const cgroupMemoryHierarchyMount = "/sys/fs/cgroup/memory" 47 | 48 | // cgroups cgroups初体验 49 | func cgroups() { 50 | // /proc/self/exe是一个符号链接,代表当前程序的绝对路径 51 | if os.Args[0] == "/proc/self/exe" { 52 | // 第一个参数就是当前执行的文件名,所以只有fork出的容器进程才会进入该分支 53 | fmt.Printf("容器进程内部 PID %d\n", syscall.Getpid()) 54 | // 需要先在宿主机上安装 stress 比如 apt-get install stress 55 | cmd := exec.Command("sh", "-c", `stress --vm-bytes 200m --vm-keep -m 1`) 56 | cmd.SysProcAttr = &syscall.SysProcAttr{} 57 | cmd.Stdin = os.Stdin 58 | cmd.Stdout = os.Stdout 59 | cmd.Stderr = os.Stderr 60 | if err := cmd.Run(); err != nil { 61 | fmt.Println(err) 62 | os.Exit(1) 63 | } 64 | } else { 65 | // 主进程会走这个分支 66 | cmd := exec.Command("/proc/self/exe") 67 | cmd.SysProcAttr = &syscall.SysProcAttr{Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWNS | syscall.CLONE_NEWPID} 68 | cmd.Stdin = os.Stdin 69 | cmd.Stdout = os.Stdout 70 | cmd.Stderr = os.Stderr 71 | if err := cmd.Start(); err != nil { 72 | fmt.Println(err) 73 | os.Exit(1) 74 | } 75 | // 得到 fork 出来的进程在外部namespace 的 pid 76 | fmt.Println("fork 进程 PID:", cmd.Process.Pid) 77 | // 在默认的 memory cgroup 下创建子目录,即创建一个子 cgroup 78 | err := os.Mkdir(filepath.Join(cgroupMemoryHierarchyMount, "testmemorylimit"), constant.Perm0755) 79 | if err != nil { 80 | fmt.Println(err) 81 | } 82 | // 将容器加入到这个 cgroup 中,即将进程PID加入到cgroup下的 cgroup.procs 文件中 83 | err = os.WriteFile(filepath.Join(cgroupMemoryHierarchyMount, "testmemorylimit", "cgroup.procs"), 84 | []byte(strconv.Itoa(cmd.Process.Pid)), constant.Perm0644) 85 | if err != nil { 86 | fmt.Println(err) 87 | os.Exit(1) 88 | } 89 | // 限制进程的内存使用,往 memory.limit_in_bytes 文件中写入数据 90 | err = os.WriteFile(filepath.Join(cgroupMemoryHierarchyMount, "testmemorylimit", "memory.limit_in_bytes"), 91 | []byte("100m"), constant.Perm0644) 92 | if err != nil { 93 | fmt.Println(err) 94 | os.Exit(1) 95 | } 96 | _, _ = cmd.Process.Wait() 97 | } 98 | } 99 | 100 | // urfaveCli cli 包简单使用,具体可以参考官方文档 101 | func urfaveCli() { 102 | app := cli.NewApp() 103 | 104 | // 指定全局参数 105 | app.Flags = []cli.Flag{ 106 | cli.StringFlag{ 107 | Name: "lang, l", 108 | Value: "english", 109 | Usage: "Language for the greeting", 110 | }, 111 | cli.StringFlag{ 112 | Name: "config, c", 113 | Usage: "Load configuration from `FILE`", 114 | }, 115 | } 116 | // 指定支持的命令列表 117 | app.Commands = []cli.Command{ 118 | { 119 | Name: "complete", 120 | Aliases: []string{"c"}, 121 | Usage: "complete a task on the list", 122 | Action: func(c *cli.Context) error { 123 | log.Println("run command complete") 124 | for i, v := range c.Args() { 125 | log.Printf("args i:%v v:%v\n", i, v) 126 | } 127 | return nil 128 | }, 129 | }, 130 | { 131 | Name: "add", 132 | Aliases: []string{"a"}, 133 | // 每个命令下面还可以指定自己的参数 134 | Flags: []cli.Flag{cli.Int64Flag{ 135 | Name: "priority", 136 | Value: 1, 137 | Usage: "priority for the task", 138 | }}, 139 | Usage: "add a task to the list", 140 | Action: func(c *cli.Context) error { 141 | log.Println("run command add") 142 | for i, v := range c.Args() { 143 | log.Printf("args i:%v v:%v\n", i, v) 144 | } 145 | return nil 146 | }, 147 | }, 148 | } 149 | 150 | err := app.Run(os.Args) 151 | if err != nil { 152 | log.Fatal(err) 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /exec.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | "path" 9 | "strings" 10 | 11 | "mydocker/container" 12 | // 需要导入nsenter包,以触发C代码 13 | _ "mydocker/nsenter" 14 | 15 | log "github.com/sirupsen/logrus" 16 | ) 17 | 18 | // nsenter里的C代码里已经出现mydocker_pid和mydocker_cmd这两个Key,主要是为了控制是否执行C代码里面的setns. 19 | const ( 20 | EnvExecPid = "mydocker_pid" 21 | EnvExecCmd = "mydocker_cmd" 22 | ) 23 | 24 | func ExecContainer(containerId string, comArray []string) { 25 | // 根据传进来的容器名获取对应的PID 26 | pid, err := getPidByContainerId(containerId) 27 | if err != nil { 28 | log.Errorf("Exec container getContainerPidByName %s error %v", containerId, err) 29 | return 30 | } 31 | 32 | cmd := exec.Command("/proc/self/exe", "exec") 33 | cmd.Stdin = os.Stdin 34 | cmd.Stdout = os.Stdout 35 | cmd.Stderr = os.Stderr 36 | 37 | // 把命令拼接成字符串,便于传递 38 | cmdStr := strings.Join(comArray, " ") 39 | log.Infof("container pid:%s command:%s", pid, cmdStr) 40 | _ = os.Setenv(EnvExecPid, pid) 41 | _ = os.Setenv(EnvExecCmd, cmdStr) 42 | // 把指定PID进程的环境变量传递给新启动的进程,实现通过exec命令也能查询到容器的环境变量 43 | containerEnvs := getEnvsByPid(pid) 44 | cmd.Env = append(os.Environ(), containerEnvs...) 45 | 46 | if err = cmd.Run(); err != nil { 47 | log.Errorf("Exec container %s error %v", containerId, err) 48 | } 49 | } 50 | 51 | func getPidByContainerId(containerId string) (string, error) { 52 | // 拼接出记录容器信息的文件路径 53 | dirPath := fmt.Sprintf(container.InfoLocFormat, containerId) 54 | configFilePath := path.Join(dirPath, container.ConfigName) 55 | // 读取内容并解析 56 | contentBytes, err := os.ReadFile(configFilePath) 57 | if err != nil { 58 | return "", err 59 | } 60 | var containerInfo container.Info 61 | if err = json.Unmarshal(contentBytes, &containerInfo); err != nil { 62 | return "", err 63 | } 64 | return containerInfo.Pid, nil 65 | } 66 | 67 | // getEnvsByPid 读取指定PID进程的环境变量 68 | func getEnvsByPid(pid string) []string { 69 | path := fmt.Sprintf("/proc/%s/environ", pid) 70 | contentBytes, err := os.ReadFile(path) 71 | if err != nil { 72 | log.Errorf("Read file %s error %v", path, err) 73 | return nil 74 | } 75 | // env split by \u0000 76 | envs := strings.Split(string(contentBytes), "\u0000") 77 | return envs 78 | } 79 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module mydocker 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/pkg/errors v0.9.1 7 | github.com/sirupsen/logrus v1.8.1 8 | github.com/urfave/cli v1.22.5 9 | github.com/vishvananda/netlink v1.1.0 10 | github.com/vishvananda/netns v0.0.4 11 | golang.org/x/sys v0.2.0 12 | ) 13 | 14 | require ( 15 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d // indirect 16 | github.com/russross/blackfriday/v2 v2.0.1 // indirect 17 | github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect 18 | ) 19 | -------------------------------------------------------------------------------- /list.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | "path" 8 | "text/tabwriter" 9 | 10 | "mydocker/container" 11 | 12 | log "github.com/sirupsen/logrus" 13 | ) 14 | 15 | func ListContainers() { 16 | // 读取存放容器信息目录下的所有文件 17 | files, err := os.ReadDir(container.InfoLoc) 18 | if err != nil { 19 | log.Errorf("read dir %s error %v", container.InfoLoc, err) 20 | return 21 | } 22 | containers := make([]*container.Info, 0, len(files)) 23 | for _, file := range files { 24 | tmpContainer, err := getContainerInfo(file) 25 | if err != nil { 26 | log.Errorf("get container info error %v", err) 27 | continue 28 | } 29 | containers = append(containers, tmpContainer) 30 | } 31 | // 使用tabwriter.NewWriter在控制台打印出容器信息 32 | // tabwriter 是引用的text/tabwriter类库,用于在控制台打印对齐的表格 33 | w := tabwriter.NewWriter(os.Stdout, 12, 1, 3, ' ', 0) 34 | _, err = fmt.Fprint(w, "ID\tNAME\tPID\tIP\tSTATUS\tCOMMAND\tCREATED\n") 35 | if err != nil { 36 | log.Errorf("Fprint error %v", err) 37 | } 38 | for _, item := range containers { 39 | _, err = fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%s\n", 40 | item.Id, 41 | item.Name, 42 | item.Pid, 43 | item.IP, 44 | item.Status, 45 | item.Command, 46 | item.CreatedTime) 47 | if err != nil { 48 | log.Errorf("Fprint error %v", err) 49 | } 50 | } 51 | if err = w.Flush(); err != nil { 52 | log.Errorf("Flush error %v", err) 53 | } 54 | } 55 | 56 | func getContainerInfo(file os.DirEntry) (*container.Info, error) { 57 | // 根据文件名拼接出完整路径 58 | configFileDir := fmt.Sprintf(container.InfoLocFormat, file.Name()) 59 | configFileDir = path.Join(configFileDir, container.ConfigName) 60 | // 读取容器配置文件 61 | content, err := os.ReadFile(configFileDir) 62 | if err != nil { 63 | log.Errorf("read file %s error %v", configFileDir, err) 64 | return nil, err 65 | } 66 | info := new(container.Info) 67 | if err = json.Unmarshal(content, info); err != nil { 68 | log.Errorf("json unmarshal error %v", err) 69 | return nil, err 70 | } 71 | 72 | return info, nil 73 | } 74 | -------------------------------------------------------------------------------- /logs.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | 8 | "mydocker/container" 9 | 10 | log "github.com/sirupsen/logrus" 11 | ) 12 | 13 | func logContainer(containerId string) { 14 | logFileLocation := fmt.Sprintf(container.InfoLocFormat, containerId) + container.GetLogfile(containerId) 15 | file, err := os.Open(logFileLocation) 16 | defer file.Close() 17 | if err != nil { 18 | log.Errorf("Log container open file %s error %v", logFileLocation, err) 19 | return 20 | } 21 | content, err := io.ReadAll(file) 22 | if err != nil { 23 | log.Errorf("Log container read file %s error %v", logFileLocation, err) 24 | return 25 | } 26 | _, err = fmt.Fprint(os.Stdout, string(content)) 27 | if err != nil { 28 | log.Errorf("Log container Fprint error %v", err) 29 | return 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | log "github.com/sirupsen/logrus" 7 | 8 | "github.com/urfave/cli" 9 | ) 10 | 11 | const usage = `mydocker is a simple container runtime implementation. 12 | The purpose of this project is to learn how docker works and how to write a docker by ourselves 13 | Enjoy it, just for fun.` 14 | 15 | func main() { 16 | app := cli.NewApp() 17 | app.Name = "mydocker" 18 | app.Usage = usage 19 | 20 | app.Commands = []cli.Command{ 21 | initCommand, 22 | runCommand, 23 | commitCommand, 24 | listCommand, 25 | logCommand, 26 | execCommand, 27 | stopCommand, 28 | removeCommand, 29 | networkCommand, 30 | } 31 | 32 | app.Before = func(context *cli.Context) error { 33 | // Log as JSON instead of the default ASCII formatter. 34 | log.SetFormatter(&log.JSONFormatter{}) 35 | 36 | log.SetOutput(os.Stdout) 37 | return nil 38 | } 39 | 40 | if err := app.Run(os.Args); err != nil { 41 | log.Fatal(err) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /main_command.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "mydocker/cgroups/resource" 6 | "mydocker/network" 7 | "os" 8 | 9 | "mydocker/container" 10 | 11 | log "github.com/sirupsen/logrus" 12 | "github.com/urfave/cli" 13 | ) 14 | 15 | var runCommand = cli.Command{ 16 | Name: "run", 17 | Usage: `Create a container with namespace and cgroups limit 18 | mydocker run -it [command] 19 | mydocker run -d -name [containerName] [imageName] [command]`, 20 | Flags: []cli.Flag{ 21 | cli.BoolFlag{ 22 | Name: "it", // 简单起见,这里把 -i 和 -t 参数合并成一个 23 | Usage: "enable tty", 24 | }, 25 | cli.StringFlag{ 26 | Name: "mem", // 限制进程内存使用量,为了避免和 stress 命令的 -m 参数冲突 这里使用 -mem,到时候可以看下解决冲突的方法 27 | Usage: "memory limit,e.g.: -mem 100m", 28 | }, 29 | cli.StringFlag{ 30 | Name: "cpu", 31 | Usage: "cpu quota,e.g.: -cpu 100", // 限制进程 cpu 使用率 32 | }, 33 | cli.StringFlag{ 34 | Name: "cpuset", 35 | Usage: "cpuset limit,e.g.: -cpuset 2,4", // 限制进程 cpu 使用率 36 | }, 37 | cli.StringFlag{ // 数据卷 38 | Name: "v", 39 | Usage: "volume,e.g.: -v /ect/conf:/etc/conf", 40 | }, 41 | cli.BoolFlag{ 42 | Name: "d", 43 | Usage: "detach container,run background", 44 | }, 45 | // 提供run后面的-name指定容器名字参数 46 | cli.StringFlag{ 47 | Name: "name", 48 | Usage: "container name,e.g.: -name mycontainer", 49 | }, 50 | cli.StringSliceFlag{ 51 | Name: "e", 52 | Usage: "set environment,e.g. -e name=mydocker", 53 | }, 54 | cli.StringFlag{ 55 | Name: "net", 56 | Usage: "container network,e.g. -net testbr", 57 | }, 58 | cli.StringSliceFlag{ 59 | Name: "p", 60 | Usage: "port mapping,e.g. -p 8080:80 -p 30336:3306", 61 | }, 62 | }, 63 | /* 64 | 这里是run命令执行的真正函数。 65 | 1.判断参数是否包含command 66 | 2.获取用户指定的command 67 | 3.调用Run function去准备启动容器: 68 | */ 69 | Action: func(context *cli.Context) error { 70 | if len(context.Args()) < 1 { 71 | return fmt.Errorf("missing container command") 72 | } 73 | 74 | var cmdArray []string 75 | for _, arg := range context.Args() { 76 | cmdArray = append(cmdArray, arg) 77 | } 78 | 79 | // get image name 80 | imageName := cmdArray[0] // 镜像名称 81 | cmdArray = cmdArray[1:] 82 | 83 | tty := context.Bool("it") 84 | detach := context.Bool("d") 85 | 86 | if tty && detach { 87 | return fmt.Errorf("it and d flag can not both provided") 88 | } 89 | if !detach { // 如果不是指定后台运行,就默认前台运行 90 | tty = true 91 | } 92 | log.Infof("createTty %v", tty) 93 | resConf := &resource.ResourceConfig{ 94 | MemoryLimit: context.String("mem"), 95 | CpuSet: context.String("cpuset"), 96 | CpuCfsQuota: context.Int("cpu"), 97 | } 98 | log.Info("resConf:", resConf) 99 | volume := context.String("v") 100 | containerName := context.String("name") 101 | envSlice := context.StringSlice("e") 102 | 103 | network := context.String("net") 104 | portMapping := context.StringSlice("p") 105 | 106 | Run(tty, cmdArray, envSlice, resConf, volume, containerName, imageName, network, portMapping) 107 | return nil 108 | }, 109 | } 110 | var initCommand = cli.Command{ 111 | Name: "init", 112 | Usage: "Init container process run user's process in container. Do not call it outside", 113 | Action: func(context *cli.Context) error { 114 | log.Infof("init come on") 115 | err := container.RunContainerInitProcess() 116 | return err 117 | }, 118 | } 119 | 120 | var commitCommand = cli.Command{ 121 | Name: "commit", 122 | Usage: "commit container to image,e.g. mydocker commit 123456789 myimage", 123 | Action: func(context *cli.Context) error { 124 | if len(context.Args()) < 2 { 125 | return fmt.Errorf("missing container name and image name") 126 | } 127 | containerID := context.Args().Get(0) 128 | imageName := context.Args().Get(1) 129 | return commitContainer(containerID, imageName) 130 | }, 131 | } 132 | 133 | var listCommand = cli.Command{ 134 | Name: "ps", 135 | Usage: "list all the containers", 136 | Action: func(context *cli.Context) error { 137 | ListContainers() 138 | return nil 139 | }, 140 | } 141 | 142 | var logCommand = cli.Command{ 143 | Name: "logs", 144 | Usage: "print logs of a container", 145 | Action: func(context *cli.Context) error { 146 | if len(context.Args()) < 1 { 147 | return fmt.Errorf("please input your container id") 148 | } 149 | containerName := context.Args().Get(0) 150 | logContainer(containerName) 151 | return nil 152 | }, 153 | } 154 | 155 | var execCommand = cli.Command{ 156 | Name: "exec", 157 | Usage: "exec a command into container,mydocker exec 123456789 /bin/sh", 158 | Action: func(context *cli.Context) error { 159 | // 如果环境变量存在,说明C代码已经运行过了,即setns系统调用已经执行了,这里就直接返回,避免重复执行 160 | if os.Getenv(EnvExecPid) != "" { 161 | log.Infof("pid callback pid %v", os.Getgid()) 162 | return nil 163 | } 164 | // 格式:mydocker exec 容器名字 命令,因此至少会有两个参数 165 | if len(context.Args()) < 2 { 166 | return fmt.Errorf("missing container name or command") 167 | } 168 | containerName := context.Args().Get(0) 169 | // 将除了容器名之外的参数作为命令部分 170 | commandArray := context.Args().Tail() 171 | ExecContainer(containerName, commandArray) 172 | return nil 173 | }, 174 | } 175 | 176 | var stopCommand = cli.Command{ 177 | Name: "stop", 178 | Usage: "stop a container,e.g. mydocker stop 1234567890", 179 | Action: func(context *cli.Context) error { 180 | // 期望输入是:mydocker stop 容器Id,如果没有指定参数直接打印错误 181 | if len(context.Args()) < 1 { 182 | return fmt.Errorf("missing container id") 183 | } 184 | containerId := context.Args().Get(0) 185 | stopContainer(containerId) 186 | return nil 187 | }, 188 | } 189 | 190 | var removeCommand = cli.Command{ 191 | Name: "rm", 192 | Usage: "remove unused containers,e.g. mydocker rm 1234567890", 193 | Flags: []cli.Flag{ 194 | cli.BoolFlag{ 195 | Name: "f", // 强制删除 196 | Usage: "force delete running container,", 197 | }}, 198 | Action: func(context *cli.Context) error { 199 | if len(context.Args()) < 1 { 200 | return fmt.Errorf("missing container id") 201 | } 202 | containerId := context.Args().Get(0) 203 | force := context.Bool("f") 204 | removeContainer(containerId, force) 205 | return nil 206 | }, 207 | } 208 | 209 | var networkCommand = cli.Command{ 210 | Name: "network", 211 | Usage: "container network commands", 212 | Subcommands: []cli.Command{ 213 | { 214 | Name: "create", 215 | Usage: "create a container network", 216 | Flags: []cli.Flag{ 217 | cli.StringFlag{ 218 | Name: "driver", 219 | Usage: "network driver", 220 | }, 221 | cli.StringFlag{ 222 | Name: "subnet", 223 | Usage: "subnet cidr", 224 | }, 225 | }, 226 | Action: func(context *cli.Context) error { 227 | if len(context.Args()) < 1 { 228 | return fmt.Errorf("missing network name") 229 | } 230 | driver := context.String("driver") 231 | subnet := context.String("subnet") 232 | name := context.Args()[0] 233 | 234 | err := network.CreateNetwork(driver, subnet, name) 235 | if err != nil { 236 | return fmt.Errorf("create network error: %+v", err) 237 | } 238 | return nil 239 | }, 240 | }, 241 | { 242 | Name: "list", 243 | Usage: "list container network", 244 | Action: func(context *cli.Context) error { 245 | network.ListNetwork() 246 | return nil 247 | }, 248 | }, 249 | { 250 | Name: "remove", 251 | Usage: "remove container network", 252 | Action: func(context *cli.Context) error { 253 | if len(context.Args()) < 1 { 254 | return fmt.Errorf("missing network name") 255 | } 256 | err := network.DeleteNetwork(context.Args()[0]) 257 | if err != nil { 258 | return fmt.Errorf("remove network error: %+v", err) 259 | } 260 | return nil 261 | }, 262 | }, 263 | }, 264 | } 265 | -------------------------------------------------------------------------------- /network/bridge_driver.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "os/exec" 7 | "strings" 8 | "time" 9 | 10 | "github.com/pkg/errors" 11 | log "github.com/sirupsen/logrus" 12 | "github.com/vishvananda/netlink" 13 | ) 14 | 15 | type BridgeNetworkDriver struct { 16 | } 17 | 18 | func (d *BridgeNetworkDriver) Name() string { 19 | return "bridge" 20 | } 21 | 22 | func (d *BridgeNetworkDriver) Create(subnet string, name string) (*Network, error) { 23 | ip, ipRange, _ := net.ParseCIDR(subnet) 24 | ipRange.IP = ip 25 | n := &Network{ 26 | Name: name, 27 | IPRange: ipRange, 28 | Driver: d.Name(), 29 | } 30 | // 配置Linux Bridge 31 | err := d.initBridge(n) 32 | if err != nil { 33 | return nil, errors.Wrapf(err, "Failed to create bridge network") 34 | } 35 | return n, err 36 | } 37 | 38 | // Delete 删除网络 39 | func (d *BridgeNetworkDriver) Delete(network *Network) error { 40 | // 清除路由规则 41 | err := deleteIPRoute(network.Name, network.IPRange.String()) 42 | if err != nil { 43 | return errors.WithMessagef(err, "clean route rule failed after bridge [%s] deleted", network.Name) 44 | } 45 | // 清除 iptables 规则 46 | err = deleteIPTables(network.Name, network.IPRange) 47 | if err != nil { 48 | return errors.WithMessagef(err, "clean snat iptables rule failed after bridge [%s] deleted", network.Name) 49 | } 50 | // 删除网桥 51 | err = d.deleteBridge(network) 52 | if err != nil { 53 | return errors.WithMessagef(err, "delete bridge [%s] failed", network.Name) 54 | } 55 | return nil 56 | } 57 | 58 | // Connect 连接一个网络和网络端点 59 | // 内部会修改 endpoint.Device,必修传指针 60 | func (d *BridgeNetworkDriver) Connect(networkName string, endpoint *Endpoint) error { 61 | bridgeName := networkName 62 | // 通过接口名获取到 Linux Bridge 接口的对象和接口属性 63 | br, err := netlink.LinkByName(bridgeName) 64 | if err != nil { 65 | return err 66 | } 67 | // 创建 Veth 接口的配置 68 | la := netlink.NewLinkAttrs() 69 | // 由于 Linux 接口名的限制,取 endpointID 的前 70 | la.Name = endpoint.ID[:5] 71 | // 通过设置 Veth 接口 master 属性,设置这个Veth的一端挂载到网络对应的 Linux Bridge 72 | la.MasterIndex = br.Attrs().Index 73 | // 创建 Veth 对象,通过 PeerNarne 配置 Veth 另外 端的接口名 74 | // 配置 Veth 另外 端的名字 cif {endpoint ID 的前 位} 75 | endpoint.Device = netlink.Veth{ 76 | LinkAttrs: la, 77 | PeerName: "cif-" + endpoint.ID[:5], 78 | } 79 | // 调用netlink的LinkAdd方法创建出这个Veth接口 80 | // 因为上面指定了link的MasterIndex是网络对应的Linux Bridge 81 | // 所以Veth的一端就已经挂载到了网络对应的LinuxBridge.上 82 | if err = netlink.LinkAdd(&endpoint.Device); err != nil { 83 | return fmt.Errorf("error Add Endpoint Device: %v", err) 84 | } 85 | // 调用netlink的LinkSetUp方法,设置Veth启动 86 | // 相当于ip link set xxx up命令 87 | if err = netlink.LinkSetUp(&endpoint.Device); err != nil { 88 | return fmt.Errorf("error Add Endpoint Device: %v", err) 89 | } 90 | return nil 91 | } 92 | 93 | func (d *BridgeNetworkDriver) Disconnect(endpointID string) error { 94 | // 根据名字找到对应的 Veth 设备 95 | vethNme := endpointID[:5] // 由于 Linux 接口名的限制,取 endpointID 的前 5 位 96 | veth, err := netlink.LinkByName(vethNme) 97 | if err != nil { 98 | return err 99 | } 100 | // 从网桥解绑 101 | err = netlink.LinkSetNoMaster(veth) 102 | if err != nil { 103 | return errors.WithMessagef(err, "find veth [%s] failed", vethNme) 104 | } 105 | // 删除 veth-pair 106 | // 一端为 xxx,另一端为 cif-xxx 107 | err = netlink.LinkDel(veth) 108 | if err != nil { 109 | return errors.WithMessagef(err, "delete veth [%s] failed", vethNme) 110 | } 111 | veth2Name := "cif-" + vethNme 112 | veth2, err := netlink.LinkByName(veth2Name) 113 | if err != nil { 114 | return errors.WithMessagef(err, "find veth [%s] failed", veth2Name) 115 | } 116 | err = netlink.LinkDel(veth2) 117 | if err != nil { 118 | return errors.WithMessagef(err, "delete veth [%s] failed", veth2Name) 119 | } 120 | 121 | return nil 122 | } 123 | 124 | // initBridge 初始化Linux Bridge 125 | /* 126 | Linux Bridge 初始化流程如下: 127 | * 1)创建 Bridge 虚拟设备 128 | * 2)设置 Bridge 设备地址和路由 129 | * 3)启动 Bridge 设备 130 | * 4)设置 iptables SNAT 规则 131 | */ 132 | func (d *BridgeNetworkDriver) initBridge(n *Network) error { 133 | bridgeName := n.Name 134 | // 1)创建 Bridge 虚拟设备 135 | if err := createBridgeInterface(bridgeName); err != nil { 136 | return errors.Wrapf(err, "Failed to create bridge %s", bridgeName) 137 | } 138 | 139 | // 2)设置 Bridge 设备地址和路由 140 | gatewayIP := *n.IPRange 141 | gatewayIP.IP = n.IPRange.IP 142 | 143 | if err := setInterfaceIP(bridgeName, gatewayIP.String()); err != nil { 144 | return errors.Wrapf(err, "Error set bridge ip: %s on bridge: %s", gatewayIP.String(), bridgeName) 145 | } 146 | // 3)启动 Bridge 设备 147 | if err := setInterfaceUP(bridgeName); err != nil { 148 | return errors.Wrapf(err, "Failed to set %s up", bridgeName) 149 | } 150 | 151 | // 4)设置 iptables SNAT 规则 152 | if err := setupIPTables(bridgeName, n.IPRange); err != nil { 153 | return errors.Wrapf(err, "Failed to set up iptables for %s", bridgeName) 154 | } 155 | 156 | return nil 157 | } 158 | 159 | // deleteBridge deletes the bridge 160 | func (d *BridgeNetworkDriver) deleteBridge(n *Network) error { 161 | bridgeName := n.Name 162 | 163 | // get the link 164 | l, err := netlink.LinkByName(bridgeName) 165 | if err != nil { 166 | return fmt.Errorf("getting link with name %s failed: %v", bridgeName, err) 167 | } 168 | 169 | // delete the link 170 | if err = netlink.LinkDel(l); err != nil { 171 | return fmt.Errorf("failed to remove bridge interface %s delete: %v", bridgeName, err) 172 | } 173 | 174 | return nil 175 | } 176 | 177 | // createBridgeInterface 创建Bridge设备 178 | // ip link add xxxx 179 | func createBridgeInterface(bridgeName string) error { 180 | // 先检查是否己经存在了这个同名的Bridge设备 181 | _, err := net.InterfaceByName(bridgeName) 182 | // 如果已经存在或者报错则返回创建错 183 | // errNoSuchInterface这个错误未导出也没提供判断方法,只能判断字符串了。。 184 | if err == nil || !strings.Contains(err.Error(), "no such network interface") { 185 | return err 186 | } 187 | 188 | // create *netlink.Bridge object 189 | la := netlink.NewLinkAttrs() 190 | la.Name = bridgeName 191 | // 使用刚才创建的Link的属性创netlink Bridge对象 192 | br := &netlink.Bridge{LinkAttrs: la} 193 | // 调用 net link Linkadd 方法,创 Bridge 虚拟网络设备 194 | // netlink.LinkAdd 方法是用来创建虚拟网络设备的,相当于 ip link add xxxx 195 | if err = netlink.LinkAdd(br); err != nil { 196 | return errors.Wrapf(err, "create bridge %s error", bridgeName) 197 | } 198 | return nil 199 | } 200 | 201 | // Set the IP addr of a netlink interface 202 | // ip addr add xxx命令 203 | func setInterfaceIP(name string, rawIP string) error { 204 | retries := 2 205 | var iface netlink.Link 206 | var err error 207 | for i := 0; i < retries; i++ { 208 | // 通过LinkByName方法找到需要设置的网络接口 209 | iface, err = netlink.LinkByName(name) 210 | if err == nil { 211 | break 212 | } 213 | log.Debugf("error retrieving new bridge netlink link [ %s ]... retrying", name) 214 | time.Sleep(2 * time.Second) 215 | } 216 | if err != nil { 217 | return errors.Wrap(err, "abandoning retrieving the new bridge link from netlink, Run [ ip link ] to troubleshoot") 218 | } 219 | // 由于 netlink.ParseIPNet 是对 net.ParseCIDR一个封装,因此可以将 net.PareCIDR中返回的IP进行整合 220 | // 返回值中的 ipNet 既包含了网段的信息,192 168.0.0/24 ,也包含了原始的IP 192.168.0.1 221 | ipNet, err := netlink.ParseIPNet(rawIP) 222 | if err != nil { 223 | return err 224 | } 225 | // 通过 netlink.AddrAdd给网络接口配置地址,相当于ip addr add xxx命令 226 | // 同时如果配置了地址所在网段的信息,例如 192.168.0.0/24 227 | // 还会配置路由表 192.168.0.0/24 转发到这 testbridge 的网络接口上 228 | addr := &netlink.Addr{IPNet: ipNet} 229 | return netlink.AddrAdd(iface, addr) 230 | } 231 | 232 | // 删除路由,ip addr del xxx命令 233 | func deleteIPRoute(name string, rawIP string) error { 234 | retries := 2 235 | var iface netlink.Link 236 | var err error 237 | for i := 0; i < retries; i++ { 238 | // 通过LinkByName方法找到需要设置的网络接口 239 | iface, err = netlink.LinkByName(name) 240 | if err == nil { 241 | break 242 | } 243 | log.Debugf("error retrieving new bridge netlink link [ %s ]... retrying", name) 244 | time.Sleep(2 * time.Second) 245 | } 246 | if err != nil { 247 | return errors.Wrap(err, "abandoning retrieving the new bridge link from netlink, Run [ ip link ] to troubleshoot") 248 | } 249 | // 查询对应设备的路由并全部删除 250 | list, err := netlink.RouteList(iface, netlink.FAMILY_V4) 251 | if err != nil { 252 | return err 253 | } 254 | for _, route := range list { 255 | if route.Dst.String() == rawIP { // 根据子网进行匹配 256 | err = netlink.RouteDel(&route) 257 | if err != nil { 258 | log.Errorf("route [%v] del failed,detail:%v", route, err) 259 | continue 260 | } 261 | } 262 | } 263 | return nil 264 | } 265 | 266 | // setInterfaceUP 启动Bridge设备 267 | // 等价于 ip link set xxx up 命令 268 | func setInterfaceUP(interfaceName string) error { 269 | link, err := netlink.LinkByName(interfaceName) 270 | if err != nil { 271 | return errors.Wrapf(err, "error retrieving a link named [ %s ]:", link.Attrs().Name) 272 | } 273 | // 等价于 ip link set xxx up 命令 274 | if err = netlink.LinkSetUp(link); err != nil { 275 | return errors.Wrapf(err, "nabling interface for %s", interfaceName) 276 | } 277 | return nil 278 | } 279 | 280 | // setupIPTables 设置 iptables 对应 bridge MASQUERADE 规则 281 | // iptables -t nat -A POSTROUTING -s 172.18.0.0/24 -o eth0 -j MASQUERADE 282 | // iptables -t nat -A POSTROUTING -s {subnet} -o {deviceName} -j MASQUERADE 283 | func setupIPTables(bridgeName string, subnet *net.IPNet) error { 284 | return configIPTables(bridgeName, subnet, false) 285 | } 286 | 287 | func deleteIPTables(bridgeName string, subnet *net.IPNet) error { 288 | return configIPTables(bridgeName, subnet, true) 289 | } 290 | 291 | func configIPTables(bridgeName string, subnet *net.IPNet, isDelete bool) error { 292 | action := "-A" 293 | if isDelete { 294 | action = "-D" 295 | } 296 | // 拼接命令 297 | iptablesCmd := fmt.Sprintf("-t nat %s POSTROUTING -s %s ! -o %s -j MASQUERADE", action, subnet.String(), bridgeName) 298 | cmd := exec.Command("iptables", strings.Split(iptablesCmd, " ")...) 299 | log.Infof("配置 SNAT cmd:%v", cmd.String()) 300 | // 执行该命令 301 | output, err := cmd.Output() 302 | if err != nil { 303 | log.Errorf("iptables Output, %v", output) 304 | } 305 | return err 306 | } 307 | -------------------------------------------------------------------------------- /network/bridge_driver_test.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import ( 4 | "net" 5 | "testing" 6 | ) 7 | 8 | var testName = "testbridge" 9 | 10 | func TestBridgeCreate(t *testing.T) { 11 | d := BridgeNetworkDriver{} 12 | n, err := d.Create("192.168.0.1/24", testName) 13 | if err != nil { 14 | t.Fatal(err) 15 | } 16 | t.Logf("create network :%v", n) 17 | } 18 | 19 | func TestBridgeDelete(t *testing.T) { 20 | d := BridgeNetworkDriver{} 21 | _, ipRange, _ := net.ParseCIDR("192.168.0.1/24") 22 | n := &Network{ 23 | Name: testName, 24 | IPRange: ipRange, 25 | } 26 | err := d.Delete(n) 27 | if err != nil { 28 | t.Fatal(err) 29 | } 30 | t.Logf("delete network :%v", testName) 31 | } 32 | 33 | func TestBridgeConnect(t *testing.T) { 34 | ep := &Endpoint{ 35 | ID: "testcontainer", 36 | } 37 | 38 | n := Network{ 39 | Name: testName, 40 | } 41 | 42 | d := BridgeNetworkDriver{} 43 | err := d.Connect(n.Name, ep) 44 | if err != nil { 45 | t.Fatal(err) 46 | } 47 | } 48 | 49 | func TestBridgeDisconnect(t *testing.T) { 50 | ep := Endpoint{ 51 | ID: "testcontainer", 52 | } 53 | 54 | d := BridgeNetworkDriver{} 55 | err := d.Disconnect(ep.ID) 56 | if err != nil { 57 | t.Fatal(err) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /network/ipam.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | log "github.com/sirupsen/logrus" 7 | "mydocker/constant" 8 | "net" 9 | "os" 10 | "path" 11 | "strings" 12 | 13 | "github.com/pkg/errors" 14 | ) 15 | 16 | const ipamDefaultAllocatorPath = "/var/lib/mydocker/network/ipam/subnet.json" 17 | 18 | type IPAM struct { 19 | SubnetAllocatorPath string // 分配文件存放位置 20 | Subnets *map[string]string // 网段和位图算法的数组 map, key 是网段, value 是分配的位图数组 21 | } 22 | 23 | // 初始化一个IPAM的对象,默认使用/var/run/mydocker/network/ipam/subnet.json作为分配信息存储位置 24 | var ipAllocator = &IPAM{ 25 | SubnetAllocatorPath: ipamDefaultAllocatorPath, 26 | } 27 | 28 | // Allocate 在网段中分配一个可用的 IP 地址 29 | func (ipam *IPAM) Allocate(subnet *net.IPNet) (ip net.IP, err error) { 30 | // 存放网段中地址分配信息的数组 31 | ipam.Subnets = &map[string]string{} 32 | 33 | // 从文件中加载已经分配的网段信息 34 | err = ipam.load() 35 | if err != nil { 36 | return nil, errors.Wrap(err, "load subnet allocation info error") 37 | } 38 | // net.IPNet.Mask.Size函数会返回网段的子网掩码的总长度和网段前面的固定位的长度 39 | // 比如“127.0.0.0/8”网段的子网掩码是“255.0.0.0” 40 | // 那么subnet.Mask.Size()的返回值就是前面255所对应的位数和总位数,即8和24 41 | _, subnet, _ = net.ParseCIDR(subnet.String()) 42 | one, size := subnet.Mask.Size() 43 | // 如果之前没有分配过这个网段,则初始化网段的分配配置 44 | if _, exist := (*ipam.Subnets)[subnet.String()]; !exist { 45 | // /用“0”填满这个网段的配置,uint8(size - one )表示这个网段中有多少个可用地址 46 | // size - one是子网掩码后面的网络位数,2^(size - one)表示网段中的可用IP数 47 | // 而2^(size - one)等价于1 << uint8(size - one) 48 | // 左移一位就是扩大两倍 49 | ipCount := 1 << uint8(size-one) 50 | ipalloc := strings.Repeat("0", ipCount-2) // 减去 0和 255这俩个不可分配地址,所有可分配的 IP 地址 51 | // 初始化分配配置,标记 .0 和 .255 位置为不可分配,直接置为 1 52 | (*ipam.Subnets)[subnet.String()] = fmt.Sprintf("1%s1", ipalloc) 53 | } 54 | // 遍历网段的位图数组 55 | for c := range (*ipam.Subnets)[subnet.String()] { 56 | // 找到数组中为“0”的项和数组序号,即可以分配的 IP 57 | if (*ipam.Subnets)[subnet.String()][c] == '0' { 58 | // 设置这个为“0”的序号值为“1” 即标记这个IP已经分配过了 59 | // Go 的字符串,创建之后就不能修改 所以通过转换成 byte 数组,修改后再转换成字符串赋值 60 | ipalloc := []byte((*ipam.Subnets)[subnet.String()]) 61 | ipalloc[c] = '1' 62 | (*ipam.Subnets)[subnet.String()] = string(ipalloc) 63 | // 这里的 subnet.IP只是初始IP,比如对于网段192 168.0.0/16 ,这里就是192.168.0.0 64 | ip = subnet.IP 65 | /* 66 | 还需要通过网段的IP与上面的偏移相加计算出分配的IP地址,由于IP地址是uint的一个数组, 67 | 需要通过数组中的每一项加所需要的值,比如网段是172.16.0.0/12,数组序号是65555, 68 | 那么在[172,16,0,0] 上依次加[uint8(65555 >> 24)、uint8(65555 >> 16)、 69 | uint8(65555 >> 8)、uint8(65555 >> 0)], 即[0, 1, 0, 19], 那么获得的IP就 70 | 是172.17.0.19. 71 | */ 72 | for t := uint(4); t > 0; t -= 1 { 73 | []byte(ip)[4-t] += uint8(c >> ((t - 1) * 8)) 74 | } 75 | break 76 | } 77 | } 78 | 79 | if ip == nil { 80 | return nil, errors.New("no available ip in subnet") 81 | } 82 | // 最后调用dump将分配结果保存到文件中 83 | err = ipam.dump() 84 | if err != nil { 85 | log.Error("Allocate:dump ipam error", err) 86 | } 87 | return 88 | } 89 | 90 | func (ipam *IPAM) Release(subnet *net.IPNet, ipaddr *net.IP) error { 91 | ipam.Subnets = &map[string]string{} 92 | _, subnet, _ = net.ParseCIDR(subnet.String()) 93 | 94 | err := ipam.load() 95 | if err != nil { 96 | return errors.Wrap(err, "load subnet allocation info error") 97 | } 98 | // 和分配一样的算法,反过来根据IP找到位图数组中的对应索引位置 99 | c := 0 100 | releaseIP := ipaddr.To4() 101 | for t := uint(4); t > 0; t -= 1 { 102 | c += int(releaseIP[t-1]-subnet.IP[t-1]) << ((4 - t) * 8) 103 | } 104 | // 然后将对应位置0 105 | ipalloc := []byte((*ipam.Subnets)[subnet.String()]) 106 | ipalloc[c] = '0' 107 | (*ipam.Subnets)[subnet.String()] = string(ipalloc) 108 | 109 | // 最后调用dump将分配结果保存到文件中 110 | err = ipam.dump() 111 | if err != nil { 112 | log.Error("Allocate:dump ipam error", err) 113 | } 114 | return nil 115 | } 116 | 117 | // load 加载网段地址分配信息 118 | func (ipam *IPAM) load() error { 119 | // 检查存储文件状态,如果不存在,则说明之前没有分配,则不需要加载 120 | if _, err := os.Stat(ipam.SubnetAllocatorPath); err != nil { 121 | if !os.IsNotExist(err) { 122 | return err 123 | } 124 | return nil 125 | } 126 | // 读取文件,加载配置信息 127 | subnetConfigFile, err := os.Open(ipam.SubnetAllocatorPath) 128 | if err != nil { 129 | return err 130 | } 131 | defer subnetConfigFile.Close() 132 | subnetJson := make([]byte, 2000) 133 | n, err := subnetConfigFile.Read(subnetJson) 134 | if err != nil { 135 | return errors.Wrap(err, "read subnet config file error") 136 | } 137 | err = json.Unmarshal(subnetJson[:n], ipam.Subnets) 138 | return errors.Wrap(err, "err dump allocation info") 139 | } 140 | 141 | // dump 存储网段地址分配信息 142 | func (ipam *IPAM) dump() error { 143 | ipamConfigFileDir, _ := path.Split(ipam.SubnetAllocatorPath) 144 | if _, err := os.Stat(ipamConfigFileDir); err != nil { 145 | if !os.IsNotExist(err) { 146 | return err 147 | } 148 | if err = os.MkdirAll(ipamConfigFileDir, constant.Perm0644); err != nil { 149 | return err 150 | } 151 | } 152 | // 打开存储文件 O_TRUNC 表示如果存在则消空, os O_CREATE 表示如果不存在则创建 153 | subnetConfigFile, err := os.OpenFile(ipam.SubnetAllocatorPath, os.O_TRUNC|os.O_WRONLY|os.O_CREATE, constant.Perm0644) 154 | if err != nil { 155 | return err 156 | } 157 | defer subnetConfigFile.Close() 158 | ipamConfigJson, err := json.Marshal(ipam.Subnets) 159 | if err != nil { 160 | return err 161 | } 162 | _, err = subnetConfigFile.Write(ipamConfigJson) 163 | return err 164 | } 165 | -------------------------------------------------------------------------------- /network/ipam_test.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import ( 4 | "net" 5 | "testing" 6 | ) 7 | 8 | func TestAllocate(t *testing.T) { 9 | _, ipNet, _ := net.ParseCIDR("192.168.0.1/24") 10 | ip, err := ipAllocator.Allocate(ipNet) 11 | if err != nil { 12 | t.Fatal(err) 13 | } 14 | t.Logf("alloc ip: %v", ip) 15 | } 16 | 17 | func TestRelease(t *testing.T) { 18 | ip, ipNet, _ := net.ParseCIDR("192.168.0.1/24") 19 | err := ipAllocator.Release(ipNet, &ip) 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | } 24 | 25 | func TestAllAllocate(t *testing.T) { 26 | /* 27 | ipam_test.go:44: alloc ip: 192.168.0.1 28 | ipam_test.go:44: alloc ip: 192.168.0.2 29 | ipam_test.go:44: alloc ip: 192.168.0.3 30 | ipam_test.go:44: alloc ip: 192.168.0.252 31 | ipam_test.go:44: alloc ip: 192.168.0.253 32 | ipam_test.go:44: alloc ip: 192.168.0.254 33 | ipam_test.go:42: no available ip in subnet 34 | */ 35 | // 测试是否溢出 36 | for i := 0; i < 256; i++ { 37 | _, ipNet, _ := net.ParseCIDR("192.168.0.1/24") 38 | ip, err := ipAllocator.Allocate(ipNet) 39 | if err != nil { 40 | t.Fatal(err) 41 | } 42 | t.Logf("alloc ip: %v", ip) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /network/model.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import ( 4 | "github.com/vishvananda/netlink" 5 | "net" 6 | ) 7 | 8 | type Network struct { 9 | Name string // 网络名 10 | IPRange *net.IPNet // 地址段 11 | Driver string // 网络驱动名 12 | } 13 | 14 | type Endpoint struct { 15 | ID string `json:"id"` 16 | Device netlink.Veth `json:"dev"` 17 | IPAddress net.IP `json:"ip"` 18 | MacAddress net.HardwareAddr `json:"mac"` 19 | Network *Network 20 | PortMapping []string 21 | } 22 | 23 | type Driver interface { 24 | Name() string 25 | Create(subnet string, name string) (*Network, error) 26 | Delete(network *Network) error 27 | Connect(networkName string, endpoint *Endpoint) error // 内部会修改 endpoint.Device,必修传指针 28 | Disconnect(endpointID string) error 29 | } 30 | 31 | type IPAMer interface { 32 | Allocate(subnet *net.IPNet) (ip net.IP, err error) // 从指定的 subnet 网段中分配 IP 地址 33 | Release(subnet *net.IPNet, ipaddr *net.IP) error // 从指定的 subnet 网段中释放掉指定的 IP 地址。 34 | } 35 | -------------------------------------------------------------------------------- /network/network.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net" 7 | "os" 8 | "os/exec" 9 | "path" 10 | "path/filepath" 11 | "runtime" 12 | "strings" 13 | "text/tabwriter" 14 | 15 | "mydocker/constant" 16 | "mydocker/container" 17 | 18 | "github.com/pkg/errors" 19 | 20 | "github.com/vishvananda/netlink" 21 | "github.com/vishvananda/netns" 22 | 23 | "github.com/sirupsen/logrus" 24 | ) 25 | 26 | var ( 27 | defaultNetworkPath = "/var/lib/mydocker/network/network/" 28 | drivers = map[string]Driver{} 29 | ) 30 | 31 | func init() { 32 | // 加载网络驱动 33 | var bridgeDriver = BridgeNetworkDriver{} 34 | drivers[bridgeDriver.Name()] = &bridgeDriver 35 | 36 | // 文件不存在则创建 37 | if _, err := os.Stat(defaultNetworkPath); err != nil { 38 | if !os.IsNotExist(err) { 39 | logrus.Errorf("check %s is exist failed,detail:%v", defaultNetworkPath, err) 40 | return 41 | } 42 | if err = os.MkdirAll(defaultNetworkPath, constant.Perm0644); err != nil { 43 | logrus.Errorf("create %s failed,detail:%v", defaultNetworkPath, err) 44 | return 45 | } 46 | } 47 | } 48 | 49 | func (net *Network) dump(dumpPath string) error { 50 | // 检查保存的目录是否存在,不存在则创建 51 | if _, err := os.Stat(dumpPath); err != nil { 52 | if !os.IsNotExist(err) { 53 | return err 54 | } 55 | if err = os.MkdirAll(dumpPath, constant.Perm0644); err != nil { 56 | return errors.Wrapf(err, "create network dump path %s failed", dumpPath) 57 | } 58 | } 59 | // 保存的文件名是网络的名字 60 | netPath := path.Join(dumpPath, net.Name) 61 | // 打开保存的文件用于写入,后面打开的模式参数分别是存在内容则清空、只写入、不存在则创建 62 | netFile, err := os.OpenFile(netPath, os.O_TRUNC|os.O_WRONLY|os.O_CREATE, constant.Perm0644) 63 | if err != nil { 64 | return errors.Wrapf(err, "open file %s failed", dumpPath) 65 | } 66 | defer netFile.Close() 67 | 68 | netJson, err := json.Marshal(net) 69 | if err != nil { 70 | return errors.Wrapf(err, "Marshal %v failed", net) 71 | } 72 | 73 | _, err = netFile.Write(netJson) 74 | return errors.Wrapf(err, "write %s failed", netJson) 75 | } 76 | 77 | func (net *Network) remove(dumpPath string) error { 78 | // 检查网络对应的配置文件状态,如果文件己经不存在就直接返回 79 | fullPath := path.Join(dumpPath, net.Name) 80 | if _, err := os.Stat(fullPath); err != nil { 81 | if !os.IsNotExist(err) { 82 | return err 83 | } 84 | return nil 85 | } 86 | // 否则删除这个网络对应的配置文件 87 | return os.Remove(fullPath) 88 | } 89 | 90 | func (net *Network) load(dumpPath string) error { 91 | // 打开配置文件 92 | netConfigFile, err := os.Open(dumpPath) 93 | if err != nil { 94 | return err 95 | } 96 | defer netConfigFile.Close() 97 | // 从配置文件中读取网络 配置 json 符串 98 | netJson := make([]byte, 2000) 99 | n, err := netConfigFile.Read(netJson) 100 | if err != nil { 101 | return err 102 | } 103 | 104 | err = json.Unmarshal(netJson[:n], net) 105 | return errors.Wrapf(err, "unmarshal %s failed", netJson[:n]) 106 | } 107 | 108 | // LoadFromFile 读取 defaultNetworkPath 目录下的 Network 信息存放到内存中,便于使用 109 | func loadNetwork() (map[string]*Network, error) { 110 | networks := map[string]*Network{} 111 | 112 | // 检查网络配置目录中的所有文件,并执行第二个参数中的函数指针去处理目录下的每一个文件 113 | err := filepath.Walk(defaultNetworkPath, func(netPath string, info os.FileInfo, err error) error { 114 | // 如果是目录则跳过 115 | if info.IsDir() { 116 | return nil 117 | } 118 | // if strings.HasSuffix(netPath, "/") { 119 | // return nil 120 | // } 121 | // 加载文件名作为网络名 122 | _, netName := path.Split(netPath) 123 | net := &Network{ 124 | Name: netName, 125 | } 126 | // 调用前面介绍的 Network.load 方法加载网络的配置信息 127 | if err = net.load(netPath); err != nil { 128 | logrus.Errorf("error load network: %s", err) 129 | } 130 | // 将网络的配置信息加入到 networks 字典中 131 | networks[netName] = net 132 | return nil 133 | }) 134 | return networks, err 135 | } 136 | 137 | // CreateNetwork 根据不同 driver 创建 Network 138 | func CreateNetwork(driver, subnet, name string) error { 139 | // 将网段的字符串转换成net. IPNet的对象 140 | _, cidr, _ := net.ParseCIDR(subnet) 141 | // 通过IPAM分配网关IP,获取到网段中第一个IP作为网关的IP 142 | ip, err := ipAllocator.Allocate(cidr) 143 | if err != nil { 144 | return err 145 | } 146 | cidr.IP = ip 147 | // 调用指定的网络驱动创建网络,这里的 drivers 字典是各个网络驱动的实例字典 通过调用网络驱动 148 | // Create 方法创建网络,后面会以 Bridge 驱动为例介绍它的实现 149 | net, err := drivers[driver].Create(cidr.String(), name) 150 | if err != nil { 151 | return err 152 | } 153 | // 保存网络信息,将网络的信息保存在文件系统中,以便查询和在网络上连接网络端点 154 | return net.dump(defaultNetworkPath) 155 | } 156 | 157 | // ListNetwork 打印出当前全部 Network 信息 158 | func ListNetwork() { 159 | networks, err := loadNetwork() 160 | if err != nil { 161 | logrus.Errorf("load network from file failed,detail: %v", err) 162 | return 163 | } 164 | // 通过tabwriter库把信息打印到屏幕上 165 | w := tabwriter.NewWriter(os.Stdout, 12, 1, 3, ' ', 0) 166 | fmt.Fprint(w, "NAME\tIpRange\tDriver\n") 167 | for _, net := range networks { 168 | fmt.Fprintf(w, "%s\t%s\t%s\n", 169 | net.Name, 170 | net.IPRange.String(), 171 | net.Driver, 172 | ) 173 | } 174 | if err = w.Flush(); err != nil { 175 | logrus.Errorf("Flush error %v", err) 176 | return 177 | } 178 | } 179 | 180 | // DeleteNetwork 根据名字删除 Network 181 | func DeleteNetwork(networkName string) error { 182 | networks, err := loadNetwork() 183 | if err != nil { 184 | return errors.WithMessage(err, "load network from file failed") 185 | } 186 | // 网络不存在直接返回一个error 187 | net, ok := networks[networkName] 188 | if !ok { 189 | return fmt.Errorf("no Such Network: %s", networkName) 190 | } 191 | // 调用IPAM的实例ipAllocator释放网络网关的IP 192 | if err = ipAllocator.Release(net.IPRange, &net.IPRange.IP); err != nil { 193 | return errors.Wrap(err, "remove Network gateway ip failed") 194 | } 195 | // 调用网络驱动删除网络创建的设备与配置 后面会以 Bridge 驱动删除网络为例子介绍如何实现网络驱动删除网络 196 | if err = drivers[net.Driver].Delete(net); err != nil { 197 | return errors.Wrap(err, "remove Network DriverError failed") 198 | } 199 | // 最后从网络的配直目录中删除该网络对应的配置文件 200 | return net.remove(defaultNetworkPath) 201 | } 202 | 203 | // Connect 连接容器到之前创建的网络 mydocker run -net testnet -p 8080:80 xxxx 204 | func Connect(networkName string, info *container.Info) (net.IP, error) { 205 | networks, err := loadNetwork() 206 | if err != nil { 207 | return nil, errors.WithMessage(err, "load network from file failed") 208 | } 209 | // 从networks字典中取到容器连接的网络的信息,networks字典中保存了当前己经创建的网络 210 | network, ok := networks[networkName] 211 | if !ok { 212 | return nil, fmt.Errorf("no Such Network: %s", networkName) 213 | } 214 | 215 | // 分配容器IP地址 216 | ip, err := ipAllocator.Allocate(network.IPRange) 217 | if err != nil { 218 | return ip, errors.Wrapf(err, "allocate ip") 219 | } 220 | // 创建网络端点 221 | ep := &Endpoint{ 222 | ID: fmt.Sprintf("%s-%s", info.Id, networkName), 223 | IPAddress: ip, 224 | Network: network, 225 | PortMapping: info.PortMapping, 226 | } 227 | // 调用网络驱动挂载和配置网络端点 228 | if err = drivers[network.Driver].Connect(network.Name, ep); err != nil { 229 | return ip, err 230 | } 231 | // 到容器的namespace配置容器网络设备IP地址 232 | if err = configEndpointIpAddressAndRoute(ep, info); err != nil { 233 | return ip, err 234 | } 235 | // 配置端口映射信息,例如 mydocker run -p 8080:80 236 | return ip, addPortMapping(ep) 237 | } 238 | 239 | // Disconnect 将容器中指定网络中移除 240 | func Disconnect(networkName string, info *container.Info) error { 241 | networks, err := loadNetwork() 242 | if err != nil { 243 | return errors.WithMessage(err, "load network from file failed") 244 | } 245 | // 从networks字典中取到容器连接的网络的信息,networks字典中保存了当前己经创建的网络 246 | network, ok := networks[networkName] 247 | if !ok { 248 | return fmt.Errorf("no Such Network: %s", networkName) 249 | } 250 | // veth 从 bridge 解绑并删除 veth-pair 设备对 251 | drivers[network.Driver].Disconnect(fmt.Sprintf("%s-%s", info.Id, networkName)) 252 | 253 | // 清理端口映射添加的 iptables 规则 254 | ep := &Endpoint{ 255 | ID: fmt.Sprintf("%s-%s", info.Id, networkName), 256 | IPAddress: net.ParseIP(info.IP), 257 | Network: network, 258 | PortMapping: info.PortMapping, 259 | } 260 | return deletePortMapping(ep) 261 | } 262 | 263 | // enterContainerNetNS 将容器的网络端点加入到容器的网络空间中 264 | // 并锁定当前程序所执行的线程,使当前线程进入到容器的网络空间 265 | // 返回值是一个函数指针,执行这个返回函数才会退出容器的网络空间,回归到宿主机的网络空间 266 | func enterContainerNetNS(enLink *netlink.Link, info *container.Info) func() { 267 | // 找到容器的Net Namespace 268 | // /proc/[pid]/ns/net 打开这个文件的文件描述符就可以来操作Net Namespace 269 | // 而ContainerInfo中的PID,即容器在宿主机上映射的进程ID 270 | // 它对应的/proc/[pid]/ns/net就是容器内部的Net Namespace 271 | f, err := os.OpenFile(fmt.Sprintf("/proc/%s/ns/net", info.Pid), os.O_RDONLY, 0) 272 | if err != nil { 273 | logrus.Errorf("error get container net namespace, %v", err) 274 | } 275 | 276 | nsFD := f.Fd() 277 | // 锁定当前程序所执行的线程,如果不锁定操作系统线程的话 278 | // Go语言的goroutine可能会被调度到别的线程上去 279 | // 就不能保证一直在所需要的网络空间中了 280 | // 所以先调用runtime.LockOSThread()锁定当前程序执行的线程 281 | runtime.LockOSThread() 282 | 283 | // 修改网络端点Veth的另外一端,将其移动到容器的Net Namespace 中 284 | if err = netlink.LinkSetNsFd(*enLink, int(nsFD)); err != nil { 285 | logrus.Errorf("error set link netns , %v", err) 286 | } 287 | 288 | // 获取当前的网络namespace 289 | origns, err := netns.Get() 290 | if err != nil { 291 | logrus.Errorf("error get current netns, %v", err) 292 | } 293 | 294 | // 调用 netns.Set方法,将当前进程加入容器的Net Namespace 295 | if err = netns.Set(netns.NsHandle(nsFD)); err != nil { 296 | logrus.Errorf("error set netns, %v", err) 297 | } 298 | // 返回之前Net Namespace的函数 299 | // 在容器的网络空间中执行完容器配置之后调用此函数就可以将程序恢复到原生的Net Namespace 300 | return func() { 301 | // 恢复到上面获取到的之前的 Net Namespace 302 | netns.Set(origns) 303 | origns.Close() 304 | // 取消对当附程序的线程锁定 305 | runtime.UnlockOSThread() 306 | f.Close() 307 | } 308 | } 309 | 310 | // configEndpointIpAddressAndRoute 配置容器网络端点的地址和路由 311 | func configEndpointIpAddressAndRoute(ep *Endpoint, info *container.Info) error { 312 | // 根据名字找到对应Veth设备 313 | peerLink, err := netlink.LinkByName(ep.Device.PeerName) 314 | if err != nil { 315 | return errors.WithMessagef(err, "found veth [%s] failed", ep.Device.PeerName) 316 | } 317 | // 将容器的网络端点加入到容器的网络空间中 318 | // 并使这个函数下面的操作都在这个网络空间中进行 319 | // 执行完函数后,恢复为默认的网络空间,具体实现下面再做介绍 320 | 321 | defer enterContainerNetNS(&peerLink, info)() 322 | // 获取到容器的IP地址及网段,用于配置容器内部接口地址 323 | // 比如容器IP是192.168.1.2, 而网络的网段是192.168.1.0/24 324 | // 那么这里产出的IP字符串就是192.168.1.2/24,用于容器内Veth端点配置 325 | 326 | interfaceIP := *ep.Network.IPRange 327 | interfaceIP.IP = ep.IPAddress 328 | // 设置容器内Veth端点的IP 329 | if err = setInterfaceIP(ep.Device.PeerName, interfaceIP.String()); err != nil { 330 | return fmt.Errorf("%v,%s", ep.Network, err) 331 | } 332 | // 启动容器内的Veth端点 333 | if err = setInterfaceUP(ep.Device.PeerName); err != nil { 334 | return err 335 | } 336 | // Net Namespace 中默认本地地址 127 的勺。”网卡是关闭状态的 337 | // 启动它以保证容器访问自己的请求 338 | if err = setInterfaceUP("lo"); err != nil { 339 | return err 340 | } 341 | // 设置容器内的外部请求都通过容器内的Veth端点访问 342 | // 0.0.0.0/0的网段,表示所有的IP地址段 343 | _, cidr, _ := net.ParseCIDR("0.0.0.0/0") 344 | // 构建要添加的路由数据,包括网络设备、网关IP及目的网段 345 | // 相当于route add -net 0.0.0.0/0 gw (Bridge网桥地址) dev (容器内的Veth端点设备) 346 | 347 | defaultRoute := &netlink.Route{ 348 | LinkIndex: peerLink.Attrs().Index, 349 | Gw: ep.Network.IPRange.IP, 350 | Dst: cidr, 351 | } 352 | // 调用netlink的RouteAdd,添加路由到容器的网络空间 353 | // RouteAdd 函数相当于route add 命令 354 | if err = netlink.RouteAdd(defaultRoute); err != nil { 355 | return err 356 | } 357 | 358 | return nil 359 | } 360 | func addPortMapping(ep *Endpoint) error { 361 | return configPortMapping(ep, false) 362 | } 363 | 364 | func deletePortMapping(ep *Endpoint) error { 365 | return configPortMapping(ep, true) 366 | } 367 | 368 | // configPortMapping 配置端口映射 369 | func configPortMapping(ep *Endpoint, isDelete bool) error { 370 | action := "-A" 371 | if isDelete { 372 | action = "-D" 373 | } 374 | 375 | var err error 376 | // 遍历容器端口映射列表 377 | for _, pm := range ep.PortMapping { 378 | // 分割成宿主机的端口和容器的端口 379 | portMapping := strings.Split(pm, ":") 380 | if len(portMapping) != 2 { 381 | logrus.Errorf("port mapping format error, %v", pm) 382 | continue 383 | } 384 | // 由于iptables没有Go语言版本的实现,所以采用exec.Command的方式直接调用命令配置 385 | // 在iptables的PREROUTING中添加DNAT规则 386 | // 将宿主机的端口请求转发到容器的地址和端口上 387 | // iptables -t nat -A PREROUTING ! -i testbridge -p tcp -m tcp --dport 8080 -j DNAT --to-destination 10.0.0.4:80 388 | iptablesCmd := fmt.Sprintf("-t nat %s PREROUTING ! -i %s -p tcp -m tcp --dport %s -j DNAT --to-destination %s:%s", 389 | action, ep.Network.Name, portMapping[0], ep.IPAddress.String(), portMapping[1]) 390 | cmd := exec.Command("iptables", strings.Split(iptablesCmd, " ")...) 391 | logrus.Infoln("配置端口映射 DNAT cmd:", cmd.String()) 392 | // 执行iptables命令,添加端口映射转发规则 393 | output, err := cmd.Output() 394 | if err != nil { 395 | logrus.Errorf("iptables Output, %v", output) 396 | continue 397 | } 398 | } 399 | return err 400 | } 401 | -------------------------------------------------------------------------------- /nsenter/nsenter.go: -------------------------------------------------------------------------------- 1 | package nsenter 2 | 3 | /* 4 | #define _GNU_SOURCE 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | __attribute__((constructor)) void enter_namespace(void) { 14 | // 这里的代码会在Go运行时启动前执行,它会在单线程的C上下文中运行 15 | char *mydocker_pid; 16 | mydocker_pid = getenv("mydocker_pid"); 17 | if (mydocker_pid) { 18 | fprintf(stdout, "got mydocker_pid=%s\n", mydocker_pid); 19 | } else { 20 | fprintf(stdout, "missing mydocker_pid env skip nsenter"); 21 | // 如果没有指定PID就不需要继续执行,直接退出 22 | return; 23 | } 24 | char *mydocker_cmd; 25 | mydocker_cmd = getenv("mydocker_cmd"); 26 | if (mydocker_cmd) { 27 | fprintf(stdout, "got mydocker_cmd=%s\n", mydocker_cmd); 28 | } else { 29 | fprintf(stdout, "missing mydocker_cmd env skip nsenter"); 30 | // 如果没有指定命令也是直接退出 31 | return; 32 | } 33 | int i; 34 | char nspath[1024]; 35 | // 需要进入的5种namespace 36 | char *namespaces[] = { "ipc", "uts", "net", "pid", "mnt" }; 37 | 38 | for (i=0; i<5; i++) { 39 | // 拼接对应路径,类似于/proc/pid/ns/ipc这样 40 | sprintf(nspath, "/proc/%s/ns/%s", mydocker_pid, namespaces[i]); 41 | int fd = open(nspath, O_RDONLY); 42 | // 执行setns系统调用,进入对应namespace 43 | if (setns(fd, 0) == -1) { 44 | fprintf(stderr, "setns on %s namespace failed: %s\n", namespaces[i], strerror(errno)); 45 | } else { 46 | fprintf(stdout, "setns on %s namespace succeeded\n", namespaces[i]); 47 | } 48 | close(fd); 49 | } 50 | // 在进入的Namespace中执行指定命令,然后退出 51 | int res = system(mydocker_cmd); 52 | exit(0); 53 | return; 54 | } 55 | */ 56 | import "C" 57 | -------------------------------------------------------------------------------- /run.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | log "github.com/sirupsen/logrus" 5 | "mydocker/cgroups" 6 | "mydocker/cgroups/resource" 7 | "mydocker/container" 8 | "mydocker/network" 9 | "os" 10 | "strconv" 11 | "strings" 12 | ) 13 | 14 | // Run 执行具体 command 15 | /* 16 | 这里的Start方法是真正开始执行由NewParentProcess构建好的command的调用,它首先会clone出来一个namespace隔离的 17 | 进程,然后在子进程中,调用/proc/self/exe,也就是调用自己,发送init参数,调用我们写的init方法, 18 | 去初始化容器的一些资源。 19 | */ 20 | func Run(tty bool, comArray, envSlice []string, res *resource.ResourceConfig, volume, containerName, imageName string, 21 | net string, portMapping []string) { 22 | containerId := container.GenerateContainerID() // 生成 10 位容器 id 23 | 24 | // start container 25 | parent, writePipe := container.NewParentProcess(tty, volume, containerId, imageName, envSlice) 26 | if parent == nil { 27 | log.Errorf("New parent process error") 28 | return 29 | } 30 | if err := parent.Start(); err != nil { 31 | log.Errorf("Run parent.Start err:%v", err) 32 | return 33 | } 34 | 35 | // 创建cgroup manager, 并通过调用set和apply设置资源限制并使限制在容器上生效 36 | cgroupManager := cgroups.NewCgroupManager("mydocker-cgroup") 37 | //defer cgroupManager.Destroy() // 由单独的 goroutine 来处理 38 | _ = cgroupManager.Set(res) 39 | _ = cgroupManager.Apply(parent.Process.Pid) 40 | 41 | var containerIP string 42 | // 如果指定了网络信息则进行配置 43 | if net != "" { 44 | // config container network 45 | containerInfo := &container.Info{ 46 | Id: containerId, 47 | Pid: strconv.Itoa(parent.Process.Pid), 48 | Name: containerName, 49 | PortMapping: portMapping, 50 | } 51 | ip, err := network.Connect(net, containerInfo) 52 | if err != nil { 53 | log.Errorf("Error Connect Network %v", err) 54 | return 55 | } 56 | containerIP = ip.String() 57 | } 58 | 59 | // record container info 60 | containerInfo, err := container.RecordContainerInfo(parent.Process.Pid, comArray, containerName, containerId, 61 | volume, net, containerIP, portMapping) 62 | if err != nil { 63 | log.Errorf("Record container info error %v", err) 64 | return 65 | } 66 | 67 | // 在子进程创建后才能通过pipe来发送参数 68 | sendInitCommand(comArray, writePipe) 69 | 70 | if tty { 71 | _ = parent.Wait() // 前台运行,等待容器进程结束 72 | } 73 | // 然后创建一个 goroutine 来处理后台运行的清理工作 74 | go func() { 75 | if !tty { 76 | // 等待子进程退出 77 | _, _ = parent.Process.Wait() 78 | } 79 | 80 | // 清理工作 81 | container.DeleteWorkSpace(containerId, volume) 82 | container.DeleteContainerInfo(containerId) 83 | if net != "" { 84 | network.Disconnect(net, containerInfo) 85 | } 86 | 87 | // 销毁 cgroup 88 | cgroupManager.Destroy() 89 | }() 90 | } 91 | 92 | // sendInitCommand 通过writePipe将指令发送给子进程 93 | func sendInitCommand(comArray []string, writePipe *os.File) { 94 | command := strings.Join(comArray, " ") 95 | log.Infof("command all is %s", command) 96 | _, _ = writePipe.WriteString(command) 97 | _ = writePipe.Close() 98 | } 99 | -------------------------------------------------------------------------------- /stop.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "mydocker/network" 7 | "os" 8 | "path" 9 | "strconv" 10 | "syscall" 11 | 12 | "mydocker/constant" 13 | "mydocker/container" 14 | 15 | "github.com/pkg/errors" 16 | 17 | log "github.com/sirupsen/logrus" 18 | ) 19 | 20 | func stopContainer(containerId string) { 21 | // 1. 根据容器Id查询容器信息 22 | containerInfo, err := getInfoByContainerId(containerId) 23 | if err != nil { 24 | log.Errorf("Get container %s info error %v", containerId, err) 25 | return 26 | } 27 | pidInt, err := strconv.Atoi(containerInfo.Pid) 28 | if err != nil { 29 | log.Errorf("Conver pid from string to int error %v", err) 30 | return 31 | } 32 | // 2.发送SIGTERM信号 33 | if err = syscall.Kill(pidInt, syscall.SIGTERM); err != nil { 34 | log.Errorf("Stop container %s error %v", containerId, err) 35 | return 36 | } 37 | // 3.修改容器信息,将容器置为STOP状态,并清空PID 38 | containerInfo.Status = container.STOP 39 | containerInfo.Pid = " " 40 | newContentBytes, err := json.Marshal(containerInfo) 41 | if err != nil { 42 | log.Errorf("Json marshal %s error %v", containerId, err) 43 | return 44 | } 45 | // 4.重新写回存储容器信息的文件 46 | dirPath := fmt.Sprintf(container.InfoLocFormat, containerId) 47 | configFilePath := path.Join(dirPath, container.ConfigName) 48 | if err = os.WriteFile(configFilePath, newContentBytes, constant.Perm0622); err != nil { 49 | log.Errorf("Write file %s error:%v", configFilePath, err) 50 | } 51 | } 52 | 53 | func getInfoByContainerId(containerId string) (*container.Info, error) { 54 | dirPath := fmt.Sprintf(container.InfoLocFormat, containerId) 55 | configFilePath := path.Join(dirPath, container.ConfigName) 56 | contentBytes, err := os.ReadFile(configFilePath) 57 | if err != nil { 58 | return nil, errors.Wrapf(err, "read file %s", configFilePath) 59 | } 60 | var containerInfo container.Info 61 | if err = json.Unmarshal(contentBytes, &containerInfo); err != nil { 62 | return nil, err 63 | } 64 | return &containerInfo, nil 65 | } 66 | 67 | func removeContainer(containerId string, force bool) { 68 | containerInfo, err := getInfoByContainerId(containerId) 69 | if err != nil { 70 | log.Errorf("Get container %s info error %v", containerId, err) 71 | return 72 | } 73 | 74 | switch containerInfo.Status { 75 | case container.STOP: // STOP 状态容器直接删除即可 76 | // 先删除配置目录,再删除rootfs 目录 77 | if err = container.DeleteContainerInfo(containerId); err != nil { 78 | log.Errorf("Remove container [%s]'s config failed, detail: %v", containerId, err) 79 | return 80 | } 81 | container.DeleteWorkSpace(containerId, containerInfo.Volume) 82 | if containerInfo.NetworkName != "" { // 清理网络资源 83 | if err = network.Disconnect(containerInfo.NetworkName, containerInfo); err != nil { 84 | log.Errorf("Remove container [%s]'s config failed, detail: %v", containerId, err) 85 | return 86 | } 87 | } 88 | case container.RUNNING: // RUNNING 状态容器如果指定了 force 则先 stop 然后再删除 89 | if !force { 90 | log.Errorf("Couldn't remove running container [%s], Stop the container before attempting removal or"+ 91 | " force remove", containerId) 92 | return 93 | } 94 | log.Infof("force delete running container [%s]", containerId) 95 | stopContainer(containerId) 96 | removeContainer(containerId, force) 97 | default: 98 | log.Errorf("Couldn't remove container,invalid status %s", containerInfo.Status) 99 | return 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /utils/file.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "os" 4 | 5 | func PathExists(path string) (bool, error) { 6 | _, err := os.Stat(path) 7 | if err == nil { 8 | return true, nil 9 | } 10 | if os.IsNotExist(err) { 11 | return false, nil 12 | } 13 | return false, err 14 | } 15 | -------------------------------------------------------------------------------- /utils/rootfs.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // 容器相关目录 8 | const ( 9 | ImagePath = "/var/lib/mydocker/image/" 10 | RootPath = "/var/lib/mydocker/overlay2/" 11 | lowerDirFormat = RootPath + "%s/lower" 12 | upperDirFormat = RootPath + "%s/upper" 13 | workDirFormat = RootPath + "%s/work" 14 | mergedDirFormat = RootPath + "%s/merged" 15 | overlayFSFormat = "lowerdir=%s,upperdir=%s,workdir=%s" 16 | ) 17 | 18 | func GetRoot(containerID string) string { return RootPath + containerID } 19 | 20 | func GetImage(imageName string) string { return fmt.Sprintf("%s%s.tar", ImagePath, imageName) } 21 | 22 | func GetLower(containerID string) string { 23 | return fmt.Sprintf(lowerDirFormat, containerID) 24 | } 25 | 26 | func GetUpper(containerID string) string { 27 | return fmt.Sprintf(upperDirFormat, containerID) 28 | } 29 | 30 | func GetWorker(containerID string) string { 31 | return fmt.Sprintf(workDirFormat, containerID) 32 | } 33 | 34 | func GetMerged(containerID string) string { return fmt.Sprintf(mergedDirFormat, containerID) } 35 | 36 | func GetOverlayFSDirs(lower, upper, worker string) string { 37 | return fmt.Sprintf(overlayFSFormat, lower, upper, worker) 38 | } 39 | -------------------------------------------------------------------------------- /utils/volume.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "strings" 4 | 5 | // VolumeUrlExtract 通过冒号分割解析volume目录,比如 -v /tmp:/tmp 6 | func VolumeUrlExtract(volume string) []string { 7 | return strings.Split(volume, ":") 8 | } 9 | --------------------------------------------------------------------------------