m-barthelemy / DockerSwift

A Swift client library for Docker
MIT License
22 stars 3 forks source link
docker docker-api docker-client docker-library swift swift-nio

Docker Client

Language Docker Engine API [Platforms]()

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

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 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):

import DockerSwift

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

Remote daemon over HTTP:

import DockerSwift

let 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:

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 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)") } ```

Containers

List 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 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 :)