这是一个用 Swift 编写的底层 Docker 客户端。它非常紧密地遵循 Docker API。
它完全使用了 Swift 5.5 引入的 Swift 并发特性 (async
/await
)。
此客户端库旨在实现 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)。
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")
]),
...
]
)
要将 DockerClientSwift 添加到您现有的 Xcode 项目,请选择 File -> Swift Packages -> Add Package Dependancy。输入 https://github.com/m-barthelemy/DockerSwift.git
作为 URL。
本地套接字(默认为 /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()}
let info = try await docker.info()
print("• Docker daemon info: \(info)")
let version = try await docker.version()
print("• Docker API version: \(version.apiVersion)")
我们首先监听 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")
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")
let swarmId = try await docker.swarm.initSwarm()
客户端必须连接到 Swarm 管理器节点。
let swarm = try await docker.swarm.get()
// 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"]
)
)
注意:如果节点是管理器,则需要
force
try await docker.swarm.leave(force: true)
这需要启用 Swarm 模式的 Docker 守护进程。此外,客户端必须连接到管理器节点。
let nodes = try await docker.nodes.list()
注意:如果节点是管理器,则需要
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 来为此项目做出贡献。欢迎任何形式的贡献 :)