Skip to content

Latest commit

 

History

358 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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 (swift-server/async-http-client#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
let info = try await docker.info()
print("• Docker daemon info: \(info)")
Get versions information about the Docker daemon
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:

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".

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

Containers

List containers

Add all: true to also return stopped containers.

let containers = try await docker.containers.list()
Get a container details
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:

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:

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:

let newConfig = ContainerUpdate(memoryLimit: .mb(64), memorySwap: .mb(64))
try await docker.containers.update("nameOrId", spec: newConfig)
Start a container
try await docker.containers.start("nameOrId")
Stop a container
try await docker.containers.stop("nameOrId")
Rename a container
try await docker.containers.rename("nameOrId", to: "hahi")
Delete a container

If the container is running, deletion can be forced by passing force: true

try await docker.containers.remove("nameOrId")
Get container logs

Logs are streamed progressively in an asynchronous way.

Get all logs:

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:

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:

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:

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
let images = try await docker.images.list()
Get an image details
let image = try await docker.images.get("nameOrId")
Pull an image

Pull an image from a public repository:

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

Pull an image from a registry that requires authentication:

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

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:

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 (only available on 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")!
)

Networks

List networks
let networks = try await docker.networks.list()
Get a network details
let network = try await docker.networks.get("nameOrId")
Create a network

Create a new network without any custom options:

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

Create a new network with custom IPs range:

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
try await docker.networks.remove("nameOrId")
Connect an existing Container to a Network
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
let volumes = try await docker.volumes.list()
Get a volume details
let volume = try await docker.volumes.get("nameOrId")
Create a volume
let volume = try await docker.volumes.create(
  spec: .init(name: "myVolume", labels: ["myLabel": "value"])
)
Delete a volume
try await docker.volumes.remove("nameOrId")

Swarm

Initialize Swarm mode
let swarmId = try await docker.swarm.initSwarm()
Get Swarm cluster details (inspect)

The client must be connected to a Swarm manager node.

let swarm = try await docker.swarm.get()
Make the Docker daemon to join an existing Swarm cluster
// 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

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
let nodes = try await docker.nodes.list()
Remove a Node from a Swarm

Note: force is needed if the node is a manager

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
let services = try await docker.services.list()
Get a service details
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:

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:

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?

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?

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

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:

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:

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:

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.

try await docker.services.rollback("nameOrId")
Delete a service
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
let secrets = try await docker.secrets.list()
Get a secret details

Note: The Docker API doesn't return secret data/values.

let secret = try await docker.secrets.get("nameOrId")
Create a secret

Create a Secret containing a String value:

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:

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

try await docker.secrets.update("nameOrId", labels: ["myKey": "myValue"])
Delete a secret
try await docker.secrets.remove("nameOrId")

Plugins

List installed plugins
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.

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

About

A Swift client library for Docker

Topics

Resources

Stars

31 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages