Alexander Steiner
Home
Blog

Docker Client Package with Swift

In this article, I'm going to write about my DockerClientSwift Package. But what's Docker? Probably you have already heard of Docker. Otherwise here is a short summary: Docker is a set of technologies to containerize software by utilizing virtualization. Nowadays a lot of software is deployed in containers to enable scalability. But Docker can also be very useful for development. Developers can boot up databases or other services in a container environment and test their software against that. As a developer, you can interact with a command-line interface with the Docker engine and execute commands or fetch information. If you haven't heard of Docker, especially as a backend developer, I recommend checking it out (Docker website).

This technologies, as they are focused on developers, provide APIs that are capable of controlling a Docker environment and can automate steps and processes. My Swift package is a wrapper around these APIs which are REST APIs that are called on a socket the Docker engine exposes. The execution of the requests is fully handled by NIO, the non-blocking IO framework provided by Apple. This allows the package to function in an asynchronous environment like on the server perfectly. That's also the main purpose of the Package: automating processes on the server. But as it's written in Swift it can also be used to write UI clients for the Desktop or even for mobile devices.

Usage with SwiftUI

One target platform for this package can be the Mac. In the following, I'll explain a bit about how it can be used with SwiftUI on the Mac.

I published a demo application, fully written in SwiftUI. But as a small disclaimer: This project is not a demonstration on how to use SwiftUI in the best way but instead on how it is possible to integrate the Docker client into a SwiftUI project.

The first thing to do is to create a new instance of the DockerClient. When using the AppDelegate lifecycle this is normally done in the applicationDidFinishLaunching(_: Notification) method. Calling the default initializer will create a new HTTPClient and a new Logger the DockerClient will use over the lifetime of the object. If we want to modify any of these parameters, we can pass customized objects into the .init() function. We can also pass a different path to the Docker socket if we are using a non-default one.

import DockerClientSwift

// Store a reference to the instance in the `AppDelegate` object.
let dockerClient = DockerClient()

// Get log output of the client to debug potential bugs
LoggingSystem.bootstrap(StreamLogHandler.standardOutput(label:))

// When creating the `ContentView` inject the client as an environment object.
let contentView = ContentView()
    .environmentObject(dockerClient)

The following example code shows how we can list all created, running, stopped and exited containers in the system. When the ContentView appears we load all containers asynchronously. When the loading is completed, we store the result in a State variable so our SwiftUI layout refreshes automatically.

import SwiftUI
import DockerClientSwift
import NIO // This import is needed as the `DockerClientSwift` exposes `EventLoopFuture` objects that are defined in the `NIO` package.
import Combine

struct ContentView: View {
    @EnvironmentObject var dockerClient: DockerClient
    @State private var containers: [Container]? = []
    
    var body: some View {
        Group {
            if let containers = self.containers {
                List(0..<containers.count, id: \.self) { index in
                    Text(containers[index].id.value)
                }
            } else {
                Text("Loading")
            }
        }
        .onAppear {
            try! dockerClient.containers.list(all: true)
                .whenComplete({ result in
                    DispatchQueue.main.async {
                        switch result {
                        case .failure:
                            self.containers = []
                        case .success(let containers):
                            self.containers = containers
                        }
                    }
                })
        }
    }
}

This is a simple example of how to use the DockerClient to load data from Docker and displaying it in SwiftUI. If you want to do more in SwiftUI, I recommend checking out the demo Mac app project. It contains AppKit-style table views where containers and images are displayed. And it contains code on how to use actions from within SwiftUI/AppKit to start, stop or delete containers or remove unused images.

I think a Mac app is the most often use-case for using this package with SwiftUI. But there is also a possible use case for a mobile application for iPhone or iPad. If you have Docker running on a server and you want to expose the socket to the public (which is not recommended due to security reasons) you could also connect to it from a mobile device and control it from there.

Usage with Vapor

Instead of using the package for looking into a Docker system that is running on our own development Mac, we can use it to run a server application on an external system that exposes some specific endpoints, for example to manage running services there. In the following, we will build a web server that can update the image of a Docker service. This can be useful if we want to update a specific service to a newer image when using continous delivery for example. We are going to use the Vapor packages to expose a REST API that can update services with other images.

Vapor is probably the most popular web framework for Swift but we could also use any other package. Vapor is built on top of NIO which makes it even easier to use our DockerClient because, as mentioned above, it is also built on top of that. So the interaction becomes more seamlessly than we have seen it in the SwiftUI example.

Again I provided a GitHub repo with the demo server application based on Vapor 4.

A basic POST route that accepts a service and a new image name is seen below. If you have already developed something with Vapor this should look familiar. Otherwise I recommend checking out the Vapor documentation. The calls to the Docker client are the same as in SwiftUI. I created namespaces for each group of APIs. So we can see the images and services namespaces used in this example.

func routes(_ app: Application) throws {
    app.post("deploy") { req throws -> EventLoopFuture<DeployResponse> in
        let deploy = try req.content.decode(DeployRequest.self)
        
        return try req.dockerClient.images.pullImage(byName: deploy.imageName, tag: deploy.imageTag ?? "latest")
            .and(try req.dockerClient.services.get(serviceByNameOrId: deploy.serviceName))
            .hop(to: req.eventLoop)
            .flatMap({ image, service in
                try req.dockerClient.services.update(service: service, newImage: image)
            })
            .map({ (service) in
                DeployResponse(success: true, imageDigest: service.image.digest?.rawValue)
            })
    }
    
    struct DeployRequest: Content {
        let serviceName: String
        let imageName: String
        let imageTag: String?
    }
    
    struct DeployResponse: Content {
        let success: Bool
        let imageDigest: String?
    }
}

The req.dockerClient attribute comes from a custom extension which can be found in the demo project as well. It returns an instance of the client that is equipt with the HTTPClient of the application and the logger instance of the request:

extension Request {
    var dockerClient: DockerClient {
        DockerClient(client: self.application.http.client.shared, logger: self.logger)
    }
}

If we run the application we should have a running web server at http://localhost:8080. Now we just need a Docker service. One way of creating a Docker service is to initialize a Docker Swarm and deploy a docker-compose.yml file as a new stack. If you are not familiar with stacks you can read about it on the official Docker documentation.

The demo project comes with an example of such a docker-compose.yml file. We open a new terminal, go into the demo-swarm directory and execute the following two commands:

docker swarm init
docker stack deploy --compose-file docker-compose.yml demo

This creates a service called demo_nging with an Nginx web server based on the nginx:latest Docker image.

Now if we want to update the service to another image (for example nginx:alpine) we can call our Vapor server to do that. The following curl command does this for us:

curl --request POST \
  --url http://localhost:8080/deploy \
  --header 'Content-Type: application/json' \
  --data '{
    "serviceName": "demo_nginx",
    "imageName": "nginx",
    "imageTag": "alpine"
}'

Of course we can automate much more with the Docker APIs. We can trigger recurrent actions by creating new containers every hour or store images as a backup on a remote server.

Future of the package

In my opinion, the package covers the basic APIs already. But as Docker is such a huge system with many many APIs, not all APIs are covered yet. The package is still in an alpha stage and is a small subset of all possibilities that could be done with the API. Everyone is invited to make changes and extend the package to their needs. Feel free to create a GitHub issue or a pull request if you have a specific use case.

Conclusion

Controlling and managing your Docker environment with Swift can enable a lot of automation. The DockerClientSwift package should not be a tailored API for a specific use case but instead should provide a generic API into the Docker engine.

I think both demo applications provide a good look on how to use the DockerClientSwift package. The API is far from complete. So if you have a use case that is not covered yet, feel free to create an issue or a pull request on GitHub.

What experiences do you have with Docker? Have you somehow build an API around your Docker environment to call into it and manage a server for example? Let me know your thoughts on Twitter or via email.

Tagged with: