Skip to content

Latest commit

 

History

History
947 lines (769 loc) · 30.8 KB

README.md

File metadata and controls

947 lines (769 loc) · 30.8 KB

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