在日常开发中,测试是不可避免的,在针对DAO层的代码写测试用例的时候,直接将依赖的存储服务(比如mongodb)的client给mock掉,可能达不到检验代码中语法或数据操作正确性的目的。如果在本地起一个相关的存储服务又会由于不同的项目带来环境的污染,并且测试代码由于依赖本地环境可能导致多人协作困难。在云原生时代,你可能第一想到的就是利用docker container 来解决环境问题,而本文所推荐的就是用 go 语言来操作docker的开源项目。
项目链接:https://github.com/moby/moby
准备
- 准备docker环境:https://www.docker.com/
使用 Docker Daemon 要求版本在 18.09 以上,本地的 Docker 客户端也要求在 19.03 以上,不然之后尝试连接时会报错:
代码语言:javascript复制[] error during connect: Get "http://docker/v1.24/images/json": command [] has exited with exit status 255, please make sure the URL is valid, and Docker 18.09 or later is installed on the remote host: stderr=ssh: connect to host: Connection refused
- docker客户端库
// 安装 docker client
go get github.com/docker/docker/client
连接本地 Docker Daemon
初始化客户端对象
这里我们直接连接本地的 Docker Daemon,不需要过多配置,直接用环境变量的参数初始化客户端即可。
代码语言:javascript复制// NewEnvClient 直接使用环境变量中的 DOCKER_HOST, DOCKER_TLS_VERIFY, DOCKER_CERT_PATH, DOCKER_API_VERSION 配置
cl, err := client.NewEnvClient()
执行命令
Docker SDK 对拉取镜像、运行容器、查看状态等命令都进行了封装,具体可以参考文档,例如想要查看镜像列表则只需要执行如下命令:
代码语言:javascript复制cl.ImageList(context.Background(), types.ImageListOptions{})
完整代码
代码语言:javascript复制// main.go
package main
import (
"context"
"fmt"
"github.com/docker/docker/api/types"
"github.com/docker/docker/client"
)
func main() {
cl, err := client.NewEnvClient()
if err != nil {
fmt.Println("Unable to create docker client")
panic(err)
}
fmt.Println(cl.ImageList(context.Background(), types.ImageListOptions{}))
}
运行结果
代码语言:javascript复制$ go run main.go
[{-1 1614986725 sha256:d1165f2212346b2bab48cb01c1e39ee8ad1be46b87873d9ca7a4e434980a7726 map[] [hello-world@sha256:308866a43596e83578c7dfa15e27a73011bdd402185a84c5cd7f32a88b501a24] [hello-world:latest] -1 13336 13336}] <ni
l>
连接远程 Docker Daemon
连接远程的 Docker Daemon 和本地的类似,只不过需要在初始化客户端对象时指定连接远程的方式。 例如如果要使用 TCP 连接 192.168.64.1:2375 的 Docker Daemon,只需要将初始化客户端对象换成如下语句即可:
代码语言:javascript复制cl, err := client.NewClient("tcp://192.168.64.1:2375", "", nil, nil)
连接需要身份验证的服务器
上面连接远程 Docker Daemon 的方法的前提条件是目标机器开放了 2375 端口。然而在大多数情况下,出于安全考虑,服务器对端口开放有严格的限制,开发者通常需要使用 ssh 登录到服务器后才能操作服务器上的 Docker。对于这种情况,我们可以利用 github.com/docker/cli/cli/connhelper
这个库帮我们完成 ssh 登录验证的工作。
创建连接客户端
代码语言:javascript复制helper, _ := connhelper.GetConnectionHelper("ssh://klew@192.168.64.2:22")
httpClient := &http.Client{
Transport: &http.Transport{
DialContext: helper.Dialer,
},
}
初始化 Docker 客户端
这里我们不再使用环境变量默认的配置,而是通过配置参数的方式初始化 Docker 客户端,同时指定连接的 HTTP 客户端和上下文等信息。这样当我们尝试连接远程服务器的 Docker Daemon 时,connhelper 就会自动帮我们完成 ssh key 的验证操作
代码语言:javascript复制cl, err := client.NewClientWithOpts(
client.WithHTTPClient(httpClient),
client.WithHost(helper.Host),
client.WithDialContext(helper.Dialer),
)
执行命令
依然使用查看镜像列表的方式来验证代码
代码语言:javascript复制fmt.Println(cl.ImageList(context.Background(), types.ImageListOptions{}))
完整代码
代码语言:javascript复制package main
import (
"context"
"fmt"
"net/http"
"github.com/docker/cli/cli/connhelper"
"github.com/docker/docker/api/types"
"github.com/docker/docker/client"
)
func main() {
helper, err := connhelper.GetConnectionHelper("ssh://klew@192.168.64.2:22")
if err != nil {
return
}
httpClient := &http.Client{
Transport: &http.Transport{
DialContext: helper.Dialer,
},
}
cl, err := client.NewClientWithOpts(
client.WithHTTPClient(httpClient),
client.WithHost(helper.Host),
client.WithDialContext(helper.Dialer),
)
if err != nil {
fmt.Println("Unable to create docker client")
panic(err)
}
fmt.Println(cl.ImageList(context.Background(), types.ImageListOptions{}))
}
应用:
测试环节中mock数据库
启动mongodb容器函数封装:
package mongotest
import (
"context"
"fmt"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/client"
"github.com/docker/go-connections/nat"
"testing"
)
const (
mongoExposedPort = "27017/tcp"
)
func RunWithMongo(m *testing.M, mongoURI *string) int{
cli, err := client.NewEnvClient() // 初始化 docker client
if err != nil{
panic(err)
}
ctx := context.Background()
// 创建一个container
resp, err := cli.ContainerCreate(ctx,
&container.Config{
Image: "mongo:latest", // 镜像名:推荐提前下好
ExposedPorts: nat.PortSet{
mongoExposedPort: {}, // 暴露的端口号:可以使用想用命令启动一个,然后通过docker ps 看
},
},
&container.HostConfig{
PortBindings: nat.PortMap{
mongoExposedPort: []nat.PortBinding{ // 端口映射:将容器里的 27017 映射到本机的 27017 端口
{
HostIP: "127.0.0.1",
HostPort: "0", // 这个值如果是0,就会选一个未被占用的端口
},
},
},
},
nil, // 网络配置:默认将可以
nil, // 平台描述:不用传
"", // 容器名:传空会随机分配
)
if err != nil{
panic(err)
}
containerID := resp.ID
defer func() {
err = cli.ContainerRemove(ctx,containerID,types.ContainerRemoveOptions{ // remove container
Force: true, // 强制删除
})
if err != nil{
panic(err)
}
}()
err = cli.ContainerStart(ctx, containerID, types.ContainerStartOptions{}) // start container
if err != nil {
panic(err)
}
result, err := cli.ContainerInspect(ctx, resp.ID)
if err != nil {
panic(err)
}
addr := result.NetworkSettings.Ports[mongoExposedPort][0] // 查看绑定到的地址
*mongoURI = fmt.Sprintf("mongodb://%s:%s", addr.HostIP, addr.HostPort)
return m.Run()
}
**测试逻辑代码:**
package docker_demo
import (
mongotest "github.com/zrruirui/docker-demo/mongo"
"os"
"testing"
)
var mongoURI = ""
func TestMain(m *testing.M){
os.Exit(mongotest.RunWithMongo(m, &mongoURI))
}
func TestA(t *testing.T){
// 写自己的单测代码
t.Log(mongoURI)
// 连接db,进行数据操作
}
总结
Docker SDK 封装了 Docker 客户端会用到的指令,Go client for the Docker Engine API 详细介绍了各个指令的使用方法,充分满足我们使用程序与 Docker Daemon 的需求
https://github.com/moby/moby 这个项目中有很多对容器的操作,在我的demo项目中只用了其中的很小一部分,用来解决单测 mock db问题,其他更复杂的功能有需要可自行查阅。
参考
- Develop with Docker Engine SDKs
- Docker overview