Docker 客户端

Language Docker Engine API Platforms

这是一个用 Swift 编写的底层 Docker 客户端。它非常紧密地遵循 Docker API。

它完全使用了 Swift 5.5 引入的 Swift 并发特性 (async/await)。

Docker API 版本支持

此客户端库旨在实现 Docker API 版本 1.41 (https://docs.dockerd.com.cn/engine/api/v1.41)。这意味着它将与 Docker >= 20.10 版本一起工作。

当前实现状态

章节 操作 支持 备注
客户端连接 本地 Unix 套接字
HTTP
HTTPS
Docker 守护进程 & 系统信息 Ping
信息
版本
事件
获取数据使用信息
容器 列表
检查
创建
更新
重命名
启动/停止/杀死
暂停/取消暂停
获取日志
获取统计信息
获取进程 (top)
删除
清理
等待
文件系统更改 未测试
附加 基本支持 1
Exec 不太可能 2
调整 TTY 大小
镜像 列表
检查
历史记录
拉取 基本支持
构建 基本支持
标签
推送
创建(容器提交)
删除
清理
Swarm 初始化
加入
检查
离开
更新
节点 列表
检查
更新
删除
服务 列表
检查
创建
获取日志
更新
回滚
删除
网络 列表
检查
创建
删除
清理
(断开)连接容器
列表
检查
创建
删除
清理
密钥 列表
检查
创建
更新
删除
配置 列表
检查
创建
更新
删除
任务 列表
检查
获取日志
插件 列表
检查
获取权限
安装
移除
启用/禁用
升级 未测试
配置 未测试
创建 待定
推送 待定
注册表 登录 基本支持
Docker 错误响应管理 🚧

✅ : 已完成或基本完成

🚧 : 工作正在进行中,部分实现,可能无法工作

❌ : 目前未实现/不支持。

注意:各种 Docker 端点(例如 list 或 prune)支持过滤器。这些目前尚未实现。

1 当通过本地 Unix 套接字连接到 Docker 或使用代理时,目前支持附加。它使用 Websocket 协议。

2 Docker exec 使用非常规协议,需要原始访问 TCP 套接字。为了支持它需要大量工作 (swift-server/async-http-client#353)。

安装

Package.swift

import PackageDescription

let package = Package(
    dependencies: [
        .package(url: "https://github.com/m-barthelemy/DockerSwift.git", .branch("main")),
    ],
    targets: [
        .target(name: "App", dependencies: [
            ...
            .product(name: "DockerSwift", package: "DockerSwift")
        ]),
    ...
    ]
)

Xcode 项目

要将 DockerClientSwift 添加到您现有的 Xcode 项目,请选择 File -> Swift Packages -> Add Package Dependancy。输入 https://github.com/m-barthelemy/DockerSwift.git 作为 URL。

使用示例

连接到 Docker 守护进程

本地套接字(默认为 /var/run/docker.sock

import DockerSwift

let docker = DockerClient()
defer {try! docker.syncShutdown()}

通过 HTTP 的远程守护进程

import DockerSwift

let docker = DockerClient(daemonURL: URL(string: "http://127.0.0.1:2375")!)
defer {try! docker.syncShutdown()}

通过 HTTPS 的远程守护进程,使用客户端证书进行身份验证

import DockerSwift

var tlsConfig = TLSConfiguration.makeClientConfiguration()
tlsConfig.privateKey = NIOSSLPrivateKeySource.file("client-key.pem")
tlsConfig.certificateChain.append(NIOSSLCertificateSource.file("client-certificate.pem"))
tlsConfig.additionalTrustRoots.append(.file("docker-daemon-ca.pem"))
tlsConfig.certificateVerification = .noHostnameVerification

let docker = DockerClient(
    daemonURL: .init(string: "https://your.docker.daemon:2376")!,
    tlsConfig: tlsConfig
)
defer {try! docker.syncShutdown()}

Docker 系统信息

获取关于 Docker 守护进程的详细信息
let info = try await docker.info()
print("• Docker daemon info: \(info)")
获取关于 Docker 守护进程的版本信息
let version = try await docker.version()
print("• Docker API version: \(version.apiVersion)")
监听 Docker 守护进程事件

我们首先监听 docker 事件,然后我们创建一个容器

async let events = try await docker.events()

let container = try await docker.containers.create(
    name: "hello",
    spec: .init(
        config: .init(image: "hello-world:latest"),
        hostConfig: .init()
    )
)

现在,我们应该得到一个 action 为 "create" 且 type 为 "container" 的事件。

for try await event in try await events {
    print("\n••• event: \(event)")
}

容器

列出容器

添加 all: true 以同时返回已停止的容器。

let containers = try await docker.containers.list()
获取容器详细信息
let container = try await docker.containers.get("nameOrId")
创建一个容器

注意:您还需要启动它才能使容器实际运行。

创建新容器的最简单方法是仅指定要运行的镜像

let spec = ContainerSpec(
    config: .init(image: "hello-world:latest")
)
let container = try await docker.containers.create(name: "test", spec: spec)

Docker 允许自定义许多参数

let spec = ContainerSpec(
    config: .init(
        // Override the default command of the Image
        command: ["/custom/command", "--option"],
        // Add new environment variables
        environmentVars: ["HELLO=hi"],
        // Expose port 80
        exposedPorts: [.tcp(80)],
        image: "nginx:latest",
        // Set custom container labels
        labels: ["label1": "value1", "label2": "value2"]
    ),
    hostConfig: .init(
        // Memory the container is allocated when starting
        memoryReservation: .mb(64),
        // Maximum memory the container can use
        memoryLimit: .mb(128),
        // Needs to be either disabled (-1) or be equal to, or greater than, `memoryLimit`
        memorySwap: .mb(128),
        // Let's publish the port we exposed in `config`
        portBindings: [.tcp(80): [.publishTo(hostIp: "0.0.0.0", hostPort: 8000)]]
    )
)
let container = try await docker.containers.create(name: "nginx-test", spec: spec)
更新一个容器

让我们更新现有容器的内存限制

let newConfig = ContainerUpdate(memoryLimit: .mb(64), memorySwap: .mb(64))
try await docker.containers.update("nameOrId", spec: newConfig)
启动一个容器
try await docker.containers.start("nameOrId")
停止一个容器
try await docker.containers.stop("nameOrId")
重命名一个容器
try await docker.containers.rename("nameOrId", to: "hahi")
删除一个容器

如果容器正在运行,可以通过传递 force: true 来强制删除

try await docker.containers.remove("nameOrId")
获取容器日志

日志以异步方式逐步流式传输。

获取所有日志

let container = try await docker.containers.get("nameOrId")
      
for try await line in try await docker.containers.logs(container: container, timestamps: true) {
    print(line.message + "\n")
}

等待未来的日志消息

let container = try await docker.containers.get("nameOrId")
      
for try await line in try await docker.containers.logs(container: container, follow: true) {
    print(line.message + "\n")
}

仅最后 100 条消息

let container = try await docker.containers.get("nameOrId")
      
for try await line in try await docker.containers.logs(container: container, tail: 100) {
    print(line.message + "\n")
}
附加到容器

让我们创建一个默认运行 shell 的容器,并附加到它

let _ = try await docker.images.pull(byIdentifier: "alpine:latest")
let spec = ContainerSpec(
    config: .init(
        attachStdin: true,
        attachStdout: true,
        attachStderr: true,
        image: "alpine:latest",
        openStdin: true
    )
)
let container = try await docker.containers.create(spec: spec)
let attach = try await docker.containers.attach(container: container, stream: true, logs: true)

// Let's display any output from the container
Task {
    for try await output in attach.output {
        print("\(output)")
    }
}

// We need to be sure that the container is really running before being able to send commands to it.
try await docker.containers.start(container.id)
try await Task.sleep(for: .seconds(1))

// Now let's send the command; the response will be printed to the screen.
try await attach.send("uname")

镜像

列出 Docker 镜像
let images = try await docker.images.list()
获取镜像详细信息
let image = try await docker.images.get("nameOrId")
拉取镜像

从公共仓库拉取镜像

let image = try await docker.images.pull(byIdentifier: "hello-world:latest")

从需要身份验证的注册表拉取镜像

var credentials = RegistryAuth(username: "myUsername", password: "....")
try await docker.registries.login(credentials: &credentials)
let image = try await docker.images.pull(byIdentifier: "my-private-image:latest", credentials: credentials)

注意:RegistryAuth 也接受 serverAddress 参数,以便使用自定义注册表。

目前不支持从远程 URL 或标准输入创建镜像。

推送镜像

假设 Docker 守护进程有一个名为 "my-private-image:latest" 的镜像

var credentials = RegistryAuth(username: "myUsername", password: "....")
try await docker.registries.login(credentials: &credentials)
try await docker.images.push("my-private-image:latest", credentials: credentials)

注意:RegistryAuth 也接受 serverAddress 参数,以便使用自定义注册表。

构建镜像

此库的当前实现非常简陋。包含 Dockerfile 和构建期间所需的任何其他资源的 Docker 构建上下文必须作为 TAR 存档传递。

假设我们已经有一个构建上下文的 TAR 存档

let tar = FileManager.default.contents(atPath: "/tmp/docker-build.tar")
let buffer = ByteBuffer.init(data: tar)
let buildOutput = try await docker.images.build(
    config: .init(dockerfile: "./Dockerfile", repoTags: ["build:test"]),
    context: buffer
)
// The built Image ID is returned towards the end of the build output
var imageId: String!
for try await item in buildOutput {
    if item.aux != nil {
        imageId = item.aux!.id
    }
    else {
      print("\n• Build output: \(item.stream)")
    }
}
print("\n• Image ID: \(imageId)")

您可以使用外部库来创建构建上下文的 TAR 存档。使用 Tarscape 的示例(仅在 macOS 上可用)

import Tarscape

let tarContextPath = "/tmp/docker-build.tar"
try FileManager.default.createTar(
    at: URL(fileURLWithPath: tarContextPath),
    from: URL(string: "file:///path/to/your/context/folder")!
)

网络

列出网络
let networks = try await docker.networks.list()
获取网络详细信息
let network = try await docker.networks.get("nameOrId")
创建一个网络

创建一个没有自定义选项的新网络

let network = try await docker.networks.create(
  spec: .init(name: "my-network")
)

创建一个具有自定义 IP 范围的新网络

let network = try await docker.networks.create(
    spec: .init(
        name: "my-network",
        ipam: .init(
            config: [.init(subnet: "192.168.2.0/24", gateway: "192.168.2.1")]
        )
    )
)
删除一个网络
try await docker.networks.remove("nameOrId")
将现有容器连接到网络
let network = try await docker.networks.create(spec: .init(name: "myNetwork"))
var container = try await docker.containers.create(
    name: "myContainer",
    spec: .init(config: .init(image: image.id))
)

try await docker.networks.connect(container: container.id, to: network.id)

列出卷
let volumes = try await docker.volumes.list()
获取卷详细信息
let volume = try await docker.volumes.get("nameOrId")
创建一个卷
let volume = try await docker.volumes.create(
  spec: .init(name: "myVolume", labels: ["myLabel": "value"])
)
删除一个卷
try await docker.volumes.remove("nameOrId")

Swarm

初始化 Swarm 模式
let swarmId = try await docker.swarm.initSwarm()
获取 Swarm 集群详细信息(检查)

客户端必须连接到 Swarm 管理器节点。

let swarm = try await docker.swarm.get()
使 Docker 守护进程加入现有的 Swarm 集群
// This first client points to an existing Swarm cluster manager
let swarmClient = Dockerclient(...)
let swarm = try await swarmClient.swarm.get()

// This client is the docker daemon we want to add to the Swarm cluster
let client = Dockerclient(...)
try await client.swarm.join(
    config: .init(
        // To join the Swarm cluster as a Manager node
        joinToken: swarmClient.joinTokens.manager,
        // IP/Host of the existing Swarm managers
        remoteAddrs: ["10.0.0.1"]
    )
)
从 Swarm 中移除当前节点

注意:如果节点是管理器,则需要 force

try await docker.swarm.leave(force: true)

节点

这需要启用 Swarm 模式的 Docker 守护进程。此外,客户端必须连接到管理器节点。

列出 Swarm 节点
let nodes = try await docker.nodes.list()
从 Swarm 中移除一个节点

注意:如果节点是管理器,则需要 force

try await docker.nodes.delete(id: "xxxxxx", force: true)

服务

这需要启用 Swarm 模式的 Docker 守护进程。此外,客户端必须连接到管理器节点。

列出服务
let services = try await docker.services.list()
获取服务详细信息
let service = try await docker.services.get("nameOrId")
创建一个服务

最简单的示例,我们只指定服务的名称和要使用的镜像

let spec = ServiceSpec(
    name: "my-nginx",
    taskTemplate: .init(
        containerSpec: .init(image: "nginx:latest")
    )
)
let service = try await docker.services.create(spec: spec)

让我们为我们的服务指定副本数量、发布的端口和 64MB 的内存限制

let spec = ServiceSpec(
    name: "my-nginx",
    taskTemplate: .init(
        containerSpec: .init(image: "nginx:latest"),
        resources: .init(
            limits: .init(memoryBytes: .mb(64))
        ),
        // Uses default Docker routing mesh mode
        endpointSpec: .init(ports: [.init(name: "HTTP", targetPort: 80, publishedPort: 8000)])
    ),
    mode: .replicated(2)
)
let service = try await docker.services.create(spec: spec)

如果我们想知道我们的服务何时完全运行怎么办?

var index = 0 // Keep track of how long we've been waiting
repeat {
    try await Task.sleep(for: .seconds(1))
    print("\n Service still not fully running!")
    index += 1
} while try await docker.tasks.list()
      .filter({$0.serviceId == service.id && $0.status.state == .running})
      .count < 1 /* number of replicas */ && index < 15
print("\n Service is fully running!")

如果我们想创建一个一次性作业而不是服务怎么办?

let spec = ServiceSpec(
    name: "hello-world-job",
    taskTemplate: .init(
        containerSpec: .init(image: "hello-world:latest"),
        ...
    ),
    mode: .job(1)
)
let job = try await docker.services.create(spec: spec)

更高级的功能?让我们创建一个服务

let network = try await docker.networks.create(spec: .init(name: "myNet", driver: "overlay"))
let secret = try await docker.secrets.create(spec: .init(name: "myPassword", value: "blublublu"))
let spec = ServiceSpec(
    name: "my-nginx",
    taskTemplate: .init(
        containerSpec: .init(
            image: "nginx:latest",
            // Create and mount a dedicated Volume named "myStorage" on each running container. 
            mounts: [.volume(name: "myVolume", to: "/mnt")],
            // Add our Secret. Will appear as `/run/secrets/myPassword` in the containers.
            secrets: [.init(secret)]
        ),
        resources: .init(
            limits: .init(memoryBytes: .mb(64))
        ),
        // If a container exits or crashes, replace it with a new one.
        restartPolicy: .init(condition: .any, delay: .seconds(2), maxAttempts: 2)
    ),
    mode: .replicated(1),
    // Add our custom Network
    networks: [.init(target: network.id)],
    // Publish our Nginx image port 80 to 8000 on the Docker Swarm nodes
    endpointSpec: .init(ports: [.init(name: "HTTP", targetPort: 80, publishedPort: 8000)])
)
  
let service = try await docker.services.create(spec: spec)
更新一个服务

让我们将现有服务扩展到 3 个副本

let service = try await docker.services.get("nameOrId")
var updatedSpec = service.spec
updatedSpec.mode = .replicated(3)
try await docker.services.update("nameOrId", spec: updatedSpec)
获取服务日志

日志以异步方式逐步流式传输。

获取所有日志

let service = try await docker.services.get("nameOrId")
      
for try await line in try await docker.services.logs(service: service) {
    print(line.message + "\n")
}

等待未来的日志消息

let service = try await docker.services.get("nameOrId")
      
for try await line in try await docker.services.logs(service: service, follow: true) {
    print(line.message + "\n")
}

仅最后 100 条消息

let service = try await docker.services.get("nameOrId")
      
for try await line in try await docker.services.logs(service: service, tail: 100) {
    print(line.message + "\n")
}
回滚一个服务

假设我们更新了现有的服务配置,并且某些功能无法正常工作。我们想恢复到之前的、可工作的版本。

try await docker.services.rollback("nameOrId")
删除一个服务
try await docker.services.remove("nameOrId")

密钥

这需要启用 Swarm 模式的 Docker 守护进程。

注意:用于管理 Docker 配置的 API 与 Secrets API 非常相似,以下示例也适用于它们。

列出密钥
let secrets = try await docker.secrets.list()
获取密钥详细信息

注意:Docker API 不返回密钥数据/值。

let secret = try await docker.secrets.get("nameOrId")
创建一个密钥

创建一个包含 String 值的密钥

let secret = try await docker.secrets.create(
  spec: .init(name: "mySecret", value: "test secret value 💥")
)

您也可以传递一个 Data 值以存储为密钥

let data: Data = ...
let secret = try await docker.secrets.create(
  spec: .init(name: "mySecret", data: data)
)
更新一个密钥

目前,只有 labels 字段可以更新(Docker 限制)。

try await docker.secrets.update("nameOrId", labels: ["myKey": "myValue"])
删除一个密钥
try await docker.secrets.remove("nameOrId")

插件

列出已安装的插件
let plugins = try await docker.plugins.list()
安装一个插件

注意:install() 方法可以传递一个 credentials 参数,其中包含私有注册表的凭据。有关更多信息,请参阅“拉取镜像”。

// First, we fetch the privileges required by the plugin:
let privileges = try await docker.plugins.getPrivileges("vieux/sshfs:latest")

// Now, we can install it
try await docker.plugins.install(remote: "vieux/sshfs:latest", privileges: privileges)

// finally, we need to enable it before using it
try await docker.plugins.enable("vieux/sshfs:latest")

鸣谢

这是 https://github.com/alexsteinerde/docker-client-swift 伟大工作的分支

许可证

本项目根据 MIT 许可证发布。有关详细信息,请参阅 LICENSE

贡献

您可以通过提交详细的问题或 fork 此项目并发送 pull request 来为此项目做出贡献。欢迎任何形式的贡献 :)