Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/m-barthelemy/DockerSwift
A Swift client library for Docker
https://github.com/m-barthelemy/DockerSwift
docker docker-api docker-client docker-library swift swift-nio
Last synced: 3 months ago
JSON representation
A Swift client library for Docker
- Host: GitHub
- URL: https://github.com/m-barthelemy/DockerSwift
- Owner: m-barthelemy
- License: mit
- Created: 2022-07-01T23:31:34.000Z (over 2 years ago)
- Default Branch: main
- Last Pushed: 2023-12-28T21:55:30.000Z (11 months ago)
- Last Synced: 2024-03-14T22:33:24.310Z (8 months ago)
- Topics: docker, docker-api, docker-client, docker-library, swift, swift-nio
- Language: Swift
- Homepage:
- Size: 483 KB
- Stars: 21
- Watchers: 2
- Forks: 2
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# Docker Client
[![Language](https://img.shields.io/badge/Swift-5.5-brightgreen.svg)](http://swift.org)
[![Docker Engine API](https://img.shields.io/badge/Docker%20Engine%20API-%20%201.41-blue)](https://docs.docker.com/engine/api/v1.41/)
[![Platforms](https://img.shields.io/badge/platform-linux--64%20%7C%20osx--64-blue)]()This is a low-level Docker Client written in Swift. It very closely follows the Docker API.
It fully uses the Swift concurrency features introduced with Swift 5.5 (`async`/`await`).
## Docker API version support
This client library aims at implementing the Docker API version 1.41 (https://docs.docker.com/engine/api/v1.41).
This means that it will work with Docker >= 20.10.## Current implementation status
| Section | Operation | Support | Notes |
|-----------------------------|-------------------------|----------|-------------|
| Client connection | Local Unix socket | ✅ | |
| | HTTP | ✅ | |
| | HTTPS | ✅ | |
| | | | |
| Docker daemon & System info | Ping | ✅ | |
| | Info | ✅ | |
| | Version | ✅ | |
| | Events | ✅ | |
| | Get data usage info | ✅ | |
| | | | |
| Containers | List | ✅ | |
| | Inspect | ✅ | |
| | Create | ✅ | |
| | Update | ✅ | |
| | Rename | ✅ | |
| | Start/Stop/Kill | ✅ | |
| | Pause/Unpause | ✅ | |
| | Get logs | ✅ | |
| | Get stats | ✅ | |
| | Get processes (top) | ✅ | |
| | Delete | ✅ | |
| | Prune | ✅ | |
| | Wait | ✅ | |
| | Filesystem changes | ✅ | untested |
| | Attach | ✅ | basic support 1|
| | Exec | ❌ | unlikely 2|
| | Resize TTY | ❌ | |
| | | | |
| Images | List | ✅ | |
| | Inspect | ✅ | |
| | History | ✅ | |
| | Pull | ✅ | basic support |
| | Build | ✅ | basic support |
| | Tag | ✅ | |
| | Push | ✅ | |
| | Create (container commit)| ✅ | |
| | Delete | ✅ | |
| | Prune | ✅ | |
| | | | |
| Swarm | Init | ✅ | |
| | Join | ✅ | |
| | Inspect | ✅ | |
| | Leave | ✅ | |
| | Update | ✅ | |
| | | | |
| Nodes | List | ✅ | |
| | Inspect | ✅ | |
| | Update | ✅ | |
| | Delete | ✅ | |
| | | | |
| Services | List | ✅ | |
| | Inspect | ✅ | |
| | Create | ✅ | |
| | Get logs | ✅ | |
| | Update | ✅ | |
| | Rollback | ✅ | |
| | Delete | ✅ | |
| | | | |
| Networks | List | ✅ | |
| | Inspect | ✅ | |
| | Create | ✅ | |
| | Delete | ✅ | |
| | Prune | ✅ | |
| | (Dis)connect container | ✅ | |
| | | | |
| Volumes | List | ✅ | |
| | Inspect | ✅ | |
| | Create | ✅ | |
| | Delete | ✅ | |
| | Prune | ✅ | |
| | | | |
| Secrets | List | ✅ | |
| | Inspect | ✅ | |
| | Create | ✅ | |
| | Update | ✅ | |
| | Delete | ✅ | |
| | | | |
| Configs | List | ✅ | |
| | Inspect | ✅ | |
| | Create | ✅ | |
| | Update | ✅ | |
| | Delete | ✅ | |
| | | | |
| Tasks | List | ✅ | |
| | Inspect | ✅ | |
| | Get logs | ✅ | |
| | | | |
| Plugins | List | ✅ | |
| | Inspect | ✅ | |
| | Get Privileges | ✅ | |
| | Install | ✅ | |
| | Remove | ✅ | |
| | Enable/disable | ✅ | |
| | Upgrade | ✅ | untested |
| | Configure | ✅ | untested |
| | Create | ❌ | TBD |
| | Push | ❌ | TBD |
| | | | |
| Registries | Login | ✅ | basic support |
| | | | |
| Docker error responses mgmt | | 🚧 | |✅ : done or _mostly_ done
🚧 : work in progress, partially implemented, might not work
❌ : not implemented/supported at the moment.
Note: various Docker endpoints such as list or prune support *filters*. These are currently not implemented.
1 Attach is currently **not** supported when connecting to Docker via local Unix socket, or when using a proxy. It uses the Websocket protocol.
2 Docker exec is using an unconventional protocol that requires raw access to the TCP socket. Significant work needed in order to support it (https://github.com/swift-server/async-http-client/issues/353).
## Installation
### Package.swift
```Swift
import PackageDescriptionlet 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 Project
To add DockerClientSwift to your existing Xcode project, select File -> Swift Packages -> Add Package Dependancy.
Enter `https://github.com/m-barthelemy/DockerSwift.git` for the URL.## Usage Examples
### Connect to a Docker daemon
Local socket (defaults to `/var/run/docker.sock`):
```swift
import DockerSwiftlet docker = DockerClient()
defer {try! docker.syncShutdown()}
```Remote daemon over HTTP:
```swift
import DockerSwiftlet docker = DockerClient(daemonURL: URL(string: "http://127.0.0.1:2375")!)
defer {try! docker.syncShutdown()}
```Remote daemon over HTTPS, using a client certificate for authentication:
```swift
import DockerSwiftvar 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 = .noHostnameVerificationlet docker = DockerClient(
daemonURL: .init(string: "https://your.docker.daemon:2376")!,
tlsConfig: tlsConfig
)
defer {try! docker.syncShutdown()}
```### Docker system info
Get detailed information about the Docker daemon
```swift
let info = try await docker.info()
print("• Docker daemon info: \(info)")
```Get versions information about the Docker daemon
```swift
let version = try await docker.version()
print("• Docker API version: \(version.apiVersion)")
```Listen for Docker daemon events
We start by listening for docker events, then we create a container:
```swift
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()
)
)
```
Now, we should get an event whose `action` is "create" and whose `type` is "container".
```swift
for try await event in try await events {
print("\n••• event: \(event)")
}
```
### ContainersList containers
Add `all: true` to also return stopped containers.
```swift
let containers = try await docker.containers.list()
```Get a container details
```swift
let container = try await docker.containers.get("nameOrId")
```Create a container
> Note: you will also need to start it for the container to actually run.
The simplest way of creating a new container is to only specify the image to run:
```swift
let spec = ContainerSpec(
config: .init(image: "hello-world:latest")
)
let container = try await docker.containers.create(name: "test", spec: spec)
```
Docker allows customizing many parameters:
```swift
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)
```Update a container
Let's update the memory limits for an existing container:
```swift
let newConfig = ContainerUpdate(memoryLimit: .mb(64), memorySwap: .mb(64))
try await docker.containers.update("nameOrId", spec: newConfig)
```Start a container
```swift
try await docker.containers.start("nameOrId")
```Stop a container
```swift
try await docker.containers.stop("nameOrId")
```Rename a container
```swift
try await docker.containers.rename("nameOrId", to: "hahi")
```Delete a container
If the container is running, deletion can be forced by passing `force: true`
```swift
try await docker.containers.remove("nameOrId")
```Get container logs
> Logs are streamed progressively in an asynchronous way.
Get all logs:
```swift
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")
}
```
Wait for future log messages:
```swift
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")
}
```
Only the last 100 messages:
```swift
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")
}
```Attach to a container
Let's create a container that defaults to running a shell, and attach to it:
```swift
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")
```
### Images
List the Docker images
```swift
let images = try await docker.images.list()
```Get an image details
```swift
let image = try await docker.images.get("nameOrId")
```Pull an image
Pull an image from a public repository:
```swift
let image = try await docker.images.pull(byIdentifier: "hello-world:latest")
```Pull an image from a registry that requires authentication:
```swift
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)
```
> NOTE: `RegistryAuth` also accepts a `serverAddress` parameter in order to use a custom registry.
> Creating images from a remote URL or from the standard input is currently not supported.Push an image
Supposing that the Docker daemon has an image named "my-private-image:latest":
```swift
var credentials = RegistryAuth(username: "myUsername", password: "....")
try await docker.registries.login(credentials: &credentials)
try await docker.images.push("my-private-image:latest", credentials: credentials)
```
> NOTE: `RegistryAuth` also accepts a `serverAddress` parameter in order to use a custom registry.Build an image
> The current implementation of this library is very bare-bones.
> The Docker build context, containing the Dockerfile and any other resources required during the build, must be passed as a TAR archive.
Supposing we already have a TAR archive of the build context:
```swift
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)")
```
You can use external libraries to create TAR archives of your build context.
Example with [Tarscape](https://github.com/kayembi/Tarscape) (only available on macOS):
```swift
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")!
)
```### Networks
List networks
```swift
let networks = try await docker.networks.list()
```Get a network details
```swift
let network = try await docker.networks.get("nameOrId")
```Create a network
Create a new network without any custom options:
```swift
let network = try await docker.networks.create(
spec: .init(name: "my-network")
)
```
Create a new network with custom IPs range:
```swift
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")]
)
)
)
```Delete a network
```swift
try await docker.networks.remove("nameOrId")
```Connect an existing Container to a Network
```swift
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)
```### Volumes
List volumes
```swift
let volumes = try await docker.volumes.list()
```Get a volume details
```swift
let volume = try await docker.volumes.get("nameOrId")
```Create a volume
```swift
let volume = try await docker.volumes.create(
spec: .init(name: "myVolume", labels: ["myLabel": "value"])
)
```Delete a volume
```swift
try await docker.volumes.remove("nameOrId")
```### Swarm
Initialize Swarm mode
```swift
let swarmId = try await docker.swarm.initSwarm()
```Get Swarm cluster details (inspect)
> The client must be connected to a Swarm manager node.
```swift
let swarm = try await docker.swarm.get()
```Make the Docker daemon to join an existing Swarm cluster
```swift
// 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"]
)
)
```Remove the current Node from the Swarm
> Note: `force` is needed if the node is a manager
```swift
try await docker.swarm.leave(force: true)
```### Nodes
> This requires a Docker daemon with Swarm mode enabled.
> Additionally, the client must be connected to a manager node.List the Swarm nodes
```swift
let nodes = try await docker.nodes.list()
```Remove a Node from a Swarm
> Note: `force` is needed if the node is a manager
```swift
try await docker.nodes.delete(id: "xxxxxx", force: true)
```### Services
> This requires a Docker daemon with Swarm mode enabled.
> Additionally, the client must be connected to a manager node.List services
```swift
let services = try await docker.services.list()
```Get a service details
```swift
let service = try await docker.services.get("nameOrId")
```Create a service
Simplest possible example, we only specify the name of the service and the image to use:
```swift
let spec = ServiceSpec(
name: "my-nginx",
taskTemplate: .init(
containerSpec: .init(image: "nginx:latest")
)
)
let service = try await docker.services.create(spec: spec)
```
Let's specify a number of replicas, a published port and a memory limit of 64MB for our service:
```swift
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)
```
What if we then want to know when our service is fully running?
```swift
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!")
```
What if we want to create a one-off job instead of a service?
```swift
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)
```
Something more advanced? Let's create a Service:
- connected to a custom Network
- storing data into a custom Volume, for each container
- requiring a Secret
- publishing the port 80 of the containers to the port 8000 of each Docker Swarm node
- getting restarted automatically in case of failure
```swift
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)
```
Update a service
Let's scale an existing service up to 3 replicas:
```swift
let service = try await docker.services.get("nameOrId")
var updatedSpec = service.spec
updatedSpec.mode = .replicated(3)
try await docker.services.update("nameOrId", spec: updatedSpec)
```
Get service logs
> Logs are streamed progressively in an asynchronous way.
Get all logs:
```swift
let service = try await docker.services.get("nameOrId")
for try await line in try await docker.services.logs(service: service) {
print(line.message + "\n")
}
```
Wait for future log messages:
```swift
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")
}
```
Only the last 100 messages:
```swift
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")
}
```
Rollback a service
Suppose that we updated our existing service configuration, and something is not working properly.
We want to revert back to the previous, working version.
```swift
try await docker.services.rollback("nameOrId")
```Delete a service
```swift
try await docker.services.remove("nameOrId")
```### Secrets
> This requires a Docker daemon with Swarm mode enabled.
>
> Note: The API for managing Docker Configs is very similar to the Secrets API and the below examples also apply to them.List secrets
```swift
let secrets = try await docker.secrets.list()
```Get a secret details
> Note: The Docker API doesn't return secret data/values.
```swift
let secret = try await docker.secrets.get("nameOrId")
```Create a secret
Create a Secret containing a `String` value:
```swift
let secret = try await docker.secrets.create(
spec: .init(name: "mySecret", value: "test secret value 💥")
)
```
You can also pass a `Data` value to be stored as a Secret:
```swift
let data: Data = ...
let secret = try await docker.secrets.create(
spec: .init(name: "mySecret", data: data)
)
```Update a secret
> Currently, only the `labels` field can be updated (Docker limitation).
```swift
try await docker.secrets.update("nameOrId", labels: ["myKey": "myValue"])
```Delete a secret
```swift
try await docker.secrets.remove("nameOrId")
```### Plugins
List installed plugins
```swift
let plugins = try await docker.plugins.list()
```Install a plugin
> Note: the `install()` method can be passed a `credentials` parameter containing credentials for a private registry.
> See "Pull an image" for more information.
```swift
// 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")
```## Credits
This is a fork of the great work at https://github.com/alexsteinerde/docker-client-swift## License
This project is released under the MIT license. See [LICENSE](LICENSE) for details.## Contribute
You can contribute to this project by submitting a detailed issue or by forking this project and sending a pull request. Contributions of any kind are very welcome :)